Skip to content

feat(bin): add captain inbox board - #3

Merged
ItsFlow merged 7 commits into
mainfrom
fm/fork-main-upstream-sync-20260728
Jul 28, 2026
Merged

feat(bin): add captain inbox board#3
ItsFlow merged 7 commits into
mainfrom
fm/fork-main-upstream-sync-20260728

Conversation

@ItsFlow

@ItsFlow ItsFlow commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Intent

Prepare a fork-only upstream sync for ItsFlow/firstmate: integrate current kunchenguid/firstmate upstream main into the captain fork's main history while preserving the fork-only inbox-board commit and all of its behavior. Use a normal merge-based sync so neither side is rewritten or discarded, never push to or open a PR against kunchenguid/firstmate, and validate through the required no-mistakes delivery path targeting ItsFlow/firstmate only. The merge resolution must preserve the inbox-board scripts, docs, test selection hooks, and relay verification evidence deliberately, prove the inbox-board behavior still passes after integration, and keep upstream-deleted obsolete files deleted only when they are unrelated to the inbox-board behavior.

What Changed

  • Added the captain inbox board scripts under bin/ to render, serve, view, and arm a local relay for inbox cards and captain answers.
  • Hardened inbox relay persistence and escaped shipped-item metadata in the rendered board.
  • Documented the inbox board workflow and added focused tests for the inbox scripts, changed-test selection, and merge-resolution adjustments.

Risk Assessment

✅ Low: The previously reported relay persistence, duplicate hook, and shipped-row escaping issues are fixed, and the remaining source changes are localized to the fork-only inbox-board preservation with no source-verifiable intent contradictions found.

Testing

I inspected the merge history, diff surface, and remotes; captured the changed-test selection; ran the focused inbox, relay, test-runner, session-start, and subagent-hook regression tests; generated a rendered HTML inbox board using an ItsFlow-only fixture; armed and executed the answer relay to prove a captain answer is persisted; and verified the worktree remained clean. All exercised checks passed.

Evidence: Rendered captain inbox board
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="generator" content="fm-inbox-board.v1">
<title>Decisions and reviews firstmate needs from you</title>
<style>
*,*::before,*::after{box-sizing:border-box}
:root{
  color-scheme:light dark;
  --bg:#fbfaf8; --panel:#ffffff; --ink:#1d1f21; --muted:#5f6672; --faint:#8b929c;
  --line:#e4e2dd; --line-soft:#efeee9; --accent:#8a5a2b; --accent-soft:#f5efe6;
  --flagbg:#fdf6e6; --flagline:#e3d3ac; --ok:#2f6b48; --warn:#8a5a2b;
  --radius:10px;
}
@media (prefers-color-scheme:dark){
  :root{
    --bg:#16181b; --panel:#1d2024; --ink:#e8e6e2; --muted:#a3a9b2; --faint:#7d848d;
    --line:#2e3238; --line-soft:#25282d; --accent:#d9a86a; --accent-soft:#2a2620;
    --flagbg:#2a2620; --flagline:#4a4030; --ok:#7fb894; --warn:#d9a86a;
  }
}
:root[data-theme="dark"]{
  --bg:#16181b; --panel:#1d2024; --ink:#e8e6e2; --muted:#a3a9b2; --faint:#7d848d;
  --line:#2e3238; --line-soft:#25282d; --accent:#d9a86a; --accent-soft:#2a2620;
  --flagbg:#2a2620; --flagline:#4a4030; --ok:#7fb894; --warn:#d9a86a;
}
:root[data-theme="light"]{
  --bg:#fbfaf8; --panel:#ffffff; --ink:#1d1f21; --muted:#5f6672; --faint:#8b929c;
  --line:#e4e2dd; --line-soft:#efeee9; --accent:#8a5a2b; --accent-soft:#f5efe6;
  --flagbg:#fdf6e6; --flagline:#e3d3ac; --ok:#2f6b48; --warn:#8a5a2b;
}
body{
  margin:0; background:var(--bg); color:var(--ink);
  font:16px/1.6 ui-sans-serif,-apple-system,"Segoe UI",Inter,Helvetica,Arial,sans-serif;
  -webkit-font-smoothing:antialiased;
}
html,body{max-width:100%; overflow-x:hidden}
.wrap{max-width:56rem; margin:0 auto; padding:2.5rem 1.25rem 5rem}
/* A long title, id, or URL must wrap rather than widen the page at any nesting
   level. Lavish's layout audit flags horizontal overflow as an error, so every
   text container wraps unbreakable strings and every flex/grid child gets a
   min-width:0 track so it can actually shrink. Genuinely wide blocks scroll
   inside their own container instead of stretching the page. */
*{min-width:0}
a{color:var(--accent); overflow-wrap:anywhere}
.card,.field,.field p,.flag,form.answer,form.discuss,
.card h3,.tag,li.row,li.row .what,li.row .what .t,li.row .what .sub{
  overflow-wrap:anywhere; word-break:break-word;
}
h1{font-size:1.55rem; line-height:1.25; margin:0 0 .4rem; letter-spacing:-.01em}
.lede{margin:0 0 .35rem; color:var(--muted); max-width:44rem}
.meta{margin:0; color:var(--faint); font-size:.82rem}
header.board{border-bottom:1px solid var(--line); padding-bottom:1.5rem; margin-bottom:1.5rem}

.controls{
  display:flex; flex-wrap:wrap; gap:.75rem 1.25rem; align-items:flex-end;
  margin-bottom:2.25rem;
}
.control{display:flex; flex-direction:column; gap:.35rem; min-width:0}
.control > span{font-size:.72rem; letter-spacing:.08em; text-transform:uppercase; color:var(--faint)}
.segmented{display:flex; border:1px solid var(--line); border-radius:var(--radius); overflow:hidden}
.segmented button{
  appearance:none; border:0; background:var(--panel); color:var(--muted);
  font:inherit; font-size:.88rem; padding:.42rem .85rem; cursor:pointer;
  border-right:1px solid var(--line);
}
.segmented button:last-child{border-right:0}
.segmented button[aria-pressed="true"]{background:var(--accent-soft); color:var(--ink); font-weight:600}
select{
  font:inherit; font-size:.88rem; padding:.42rem .6rem; border-radius:var(--radius);
  border:1px solid var(--line); background:var(--panel); color:var(--ink); max-width:100%;
}
.tally{margin-left:auto; color:var(--faint); font-size:.82rem; align-self:flex-end}

section.block{margin:0 0 2.75rem}
section.block > h2{
  font-size:.78rem; letter-spacing:.11em; text-transform:uppercase; color:var(--faint);
  margin:0 0 .25rem; font-weight:600;
}
section.block > .blurb{margin:0 0 1rem; color:var(--muted); font-size:.88rem}
.empty{color:var(--faint); font-size:.9rem; font-style:italic; margin:0}

.card{
  background:var(--panel); border:1px solid var(--line); border-radius:var(--radius);
  padding:1.4rem 1.5rem; margin-bottom:1.1rem;
}
.card > h3{font-size:1.12rem; line-height:1.35; margin:0 0 .6rem; letter-spacing:-.005em}
.tags{display:flex; flex-wrap:wrap; gap:.4rem; margin:0 0 1rem}
.tag{
  font-size:.72rem; letter-spacing:.03em; color:var(--muted); background:var(--line-soft);
  border-radius:99px; padding:.16rem .55rem; white-space:nowrap;
}
.tag.warn{background:var(--flagbg); color:var(--warn); border:1px solid var(--flagline)}

.field{margin:0 0 .95rem}
.field > .label{
  font-size:.74rem; letter-spacing:.06em; text-transform:uppercase; color:var(--faint);
  margin:0 0 .18rem; font-weight:600;
}
.field p{margin:0 0 .5rem}
.field p:last-child{margin-bottom:0}
.field.take p{color:var(--ink)}

.flag{
  background:var(--flagbg); border:1px solid var(--flagline); border-radius:var(--radius);
  padding:.8rem 1rem; margin:0 0 1rem;
}
.flag .label{color:var(--warn)}

details{border-top:1px solid var(--line-soft); margin-top:1.1rem; padding-top:.85rem}
details summary{
  cursor:pointer; font-size:.85rem; color:var(--muted); list-style:none;
  display:inline-flex; align-items:center; gap:.4rem;
}
details summary::-webkit-details-marker{display:none}
details summary::before{content:"+"; color:var(--faint); font-weight:600}
details[open] summary::before{content:"\2212"}
details .inner{padding-top:.85rem; font-size:.92rem; color:var(--muted); overflow-x:auto}
details .inner code, details .inner .src{
  font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:.84rem;
}

form.answer{border-top:1px solid var(--line-soft); margin-top:1.1rem; padding-top:1rem}
form.answer .label{
  font-size:.74rem; letter-spacing:.06em; text-transform:uppercase; color:var(--faint);
  margin:0 0 .5rem; font-weight:600;
}
form.answer label.opt{
  display:flex; gap:.6rem; align-items:flex-start; padding:.38rem 0; cursor:pointer;
  font-size:.95rem; overflow-wrap:anywhere;
}
form.answer input[type=radio]{margin-top:.35rem; flex:none}
form.answer .clear{
  appearance:none; background:none; border:0; padding:.2rem 0; margin-top:.15rem;
  font:inherit; font-size:.8rem; color:var(--muted); cursor:pointer; text-decoration:underline;
}
form.answer textarea,form.discuss textarea{
  width:100%; margin-top:.7rem; min-height:3.6rem; font:inherit; font-size:.92rem;
  padding:.55rem .7rem; border-radius:var(--radius); border:1px solid var(--line);
  background:var(--bg); color:var(--ink); resize:vertical; max-width:100%;
}
form.answer .send,form.discuss .send{
  display:flex; align-items:center; gap:.75rem; margin-top:.75rem; flex-wrap:wrap;
}
form.answer button[type=submit],form.discuss button[type=submit]{
  appearance:none; font:inherit; font-size:.9rem; font-weight:600; cursor:pointer;
  padding:.45rem 1rem; border-radius:var(--radius); border:1px solid var(--accent);
  background:var(--accent); color:var(--panel);
}
form.answer .status,form.discuss .status{font-size:.84rem; color:var(--muted); overflow-wrap:anywhere}
form.answer .status.done,form.discuss .status.done{color:var(--ok); font-weight:600}
form.discuss{margin-top:.4rem}
form.discuss summary{
  cursor:pointer; font-size:.85rem; color:var(--muted); list-style:none;
  display:inline-flex; align-items:center; gap:.4rem;
}
form.discuss summary::-webkit-details-marker{display:none}
form.discuss summary::before{content:"?"; color:var(--faint); font-weight:700}

ul.rows{list-style:none; margin:0; padding:0}
li.row{
  display:flex; gap:.75rem; align-items:baseline; padding:.6rem 0;
  border-bottom:1px solid var(--line-soft); min-width:0;
}
li.row:last-child{border-bottom:0}
li.row .who{flex:none; width:8.5rem; color:var(--faint); font-size:.78rem; overflow-wrap:anywhere}
li.row .what{flex:1 1 auto; min-width:0}
li.row .what .t{display:block; overflow-wrap:anywhere}
li.row .what .sub{display:block; color:var(--faint); font-size:.82rem; overflow-wrap:anywhere}
li.row .what .sub a{overflow-wrap:anywhere}
@media (max-width:34rem){
  .wrap{padding:1.75rem 1rem 4rem}
  .card{padding:1.15rem 1.1rem}
  li.row{flex-direction:column; gap:.15rem}
  li.row .who{width:auto}
  .tally{margin-left:0; width:100%}
}
</style>
</head>
<body>
<div class="

... [5264 bytes truncated] ...

on="Full title for plain-decision" onsubmit="event.preventDefault();fmInboxAnswer(event.currentTarget);"><p class="label">Your answer</p><textarea name="note" placeholder="Write your answer here."></textarea><div class="send"><button type="submit">Send answer to firstmate</button><span class="status" role="status"></span></div></form><form class="discuss" data-lavish-question="plain-decision-discuss" data-question="Full title for plain-decision" onsubmit="event.preventDefault();fmInboxDiscuss(event.currentTarget);"><details><summary>Not ready to answer? Ask a question instead</summary><textarea name="question" placeholder="What is unclear? firstmate will reply here, it will not treat this as your decision."></textarea><div class="send"><button type="submit">Send question to firstmate</button><span class="status" role="status"></span></div></details></form></article><p class="empty empty-filtered" hidden>Nothing here for this filter.</p></section>
<section class="block" data-block="review"><h2>Review and merge</h2><p class="blurb">Finished work waiting for your yes. These links were read from local records, so a closed or already merged pull request can still appear here.</p><ul class="rows"><li class="row" data-project="alpha"><span class="who">alpha</span><span class="what"><span class="t">PR Task (repo: alpha) (kind: ship) (mode: no-mistakes)</span><span class="sub"><a href="https://github.com/ItsFlow/firstmate/pull/1213" rel="noreferrer">https://github.com/ItsFlow/firstmate/pull/1213</a> &middot; not checked, it may already be closed or merged</span></span></li></ul><p class="empty empty-filtered" hidden>Nothing here for this filter.</p></section>
<section class="block" data-block="underway"><h2>Underway</h2><p class="blurb">Being worked on right now. Nothing for you to do.</p><ul class="rows"><li class="row" data-project="alpha"><span class="who">alpha</span><span class="what"><span class="t">Running Task</span><span class="sub">in progress, started 2026-07-27</span></span></li></ul><p class="empty empty-filtered" hidden>Nothing here for this filter.</p></section>
<section class="block" data-block="queued"><h2>Queued</h2><p class="blurb">Lined up behind something else, or parked on purpose.</p><ul class="rows"><li class="row" data-project="alpha"><span class="who">alpha</span><span class="what"><span class="t">Blocked Decision</span><span class="sub">waiting for Running Task</span></span></li><li class="row" data-project="alpha"><span class="who">alpha</span><span class="what"><span class="t">PR Task (repo: alpha) (kind: ship) (mode: no-mistakes)</span><span class="sub">queued since 2026-07-28</span></span></li></ul><p class="empty empty-filtered" hidden>Nothing here for this filter.</p></section>
<section class="block" data-block="shipped"><h2>Recently built and shipped</h2><p class="blurb">Landed work, most recent first.</p><ul class="rows"><li class="row" data-project="alpha"><span class="who">alpha</span><span class="what"><span class="t">Shipped Task (repo: alpha) (kind: ship) (mode: no-mistakes)</span><span class="sub">merged 2026-07-28&lt;b&gt;unsafe&lt;/b&gt; &middot; <a href="https://github.com/ItsFlow/firstmate/pull/1200" rel="noreferrer">https://github.com/ItsFlow/firstmate/pull/1200</a></span></span></li></ul><p class="empty empty-filtered" hidden>Nothing here for this filter.</p></section>
<section class="block" data-block="stuck"><h2>Stuck</h2><p class="blurb">Stopped and not going to move on its own.</p><p class="empty">Nothing is stuck.</p><p class="empty empty-filtered" hidden>Nothing here for this filter.</p></section>
</div>
<script>
// Clear the quick-pick radios so a selection is never a trap.
function fmInboxClear(button){
  var form=button.closest('form');
  if(!form){return;}
  form.querySelectorAll('input[name=answer]').forEach(function(r){r.checked=false;});
}
// One shared send path. It queues exactly one prompt and sends it immediately
// so an answer never sits waiting on a separate Send button the captain has to
// find - the failure mode where the board looked like it silently ate answers.
function fmInboxSend(form, text, data, confirm){
  var mark=form.querySelector('.status');
  if(window.lavish&&window.lavish.queuePrompt){
    window.lavish.queuePrompt(text,{
      tag:data.tag, queueKey:data.queueKey, element:form, text:data.summary, data:data
    });
    if(window.lavish.sendQueuedPrompts){window.lavish.sendQueuedPrompts();}
    if(mark){mark.textContent=confirm; mark.classList.add('done');}
  }else if(mark){
    mark.textContent='Open this board through Lavish to send.';
  }
}
// The free-text answer stands alone: a picked option is a convenience, never
// required. Submit works with just the note, just an option, or both.
function fmInboxAnswer(form){
  var data=new FormData(form);
  var choice=(data.get('answer')||'').trim();
  var note=(data.get('note')||'').trim();
  var mark=form.querySelector('.status');
  if(!choice&&!note){
    if(mark){mark.textContent='Type your answer, or pick one of the options.'; mark.classList.remove('done');}
    return;
  }
  var id=form.dataset.lavishQuestion;
  var question=form.dataset.question||id;
  var body=note?(choice?(choice+' -- '+note):note):choice;
  fmInboxSend(form,'DECISION '+id+': '+body,{
    tag:'decision', queueKey:id, summary:question+' -> '+body,
    hold:id, question:question, answer:choice, note:note
  },'Sent to firstmate.');
}
// The discuss path asks a question back instead of deciding. firstmate replies;
// it does not treat this as the answer.
function fmInboxDiscuss(form){
  var data=new FormData(form);
  var text=(data.get('question')||'').trim();
  var mark=form.querySelector('.status');
  if(!text){
    if(mark){mark.textContent='Type your question first.'; mark.classList.remove('done');}
    return;
  }
  var id=form.dataset.lavishQuestion;
  var hold=id.replace(/-discuss$/,'');
  var question=form.dataset.question||hold;
  fmInboxSend(form,'DISCUSS '+hold+': '+text,{
    tag:'discuss', queueKey:id, summary:'Question on '+question,
    hold:hold, question:question, message:text
  },'Question sent to firstmate.');
}
</script>
<script>
(function(){
  var root=document.documentElement;
  var typeButtons=[].slice.call(document.querySelectorAll('[data-type-filter]'));
  var projectSelect=document.getElementById('project-filter');
  var tally=document.getElementById('tally');
  var state={type:'all',project:'all'};

  function visibleFor(section){
    if(state.type==='all') return true;
    if(state.type==='decisions') return section.dataset.block==='decide';
    if(state.type==='shipped') return section.dataset.block==='shipped';
    return true;
  }

  function apply(){
    var shown=0;
    [].forEach.call(document.querySelectorAll('section.block'),function(section){
      var allowed=visibleFor(section);
      var live=0;
      [].forEach.call(section.querySelectorAll('[data-project]'),function(item){
        var ok=allowed&&(state.project==='all'||item.dataset.project===state.project);
        item.hidden=!ok;
        if(ok){live++;}
      });
      var none=section.querySelector('.empty-filtered');
      if(none){none.hidden=!(allowed&&live===0);}
      section.hidden=!allowed;
      shown+=live;
      if(section.dataset.block==='decide'){
        var c=section.querySelector('.count');
        if(c){c.textContent=live===1?'1 decision':(live+' decisions');}
      }
    });
    if(tally){tally.textContent=shown+(shown===1?' item shown':' items shown');}
  }

  typeButtons.forEach(function(btn){
    btn.addEventListener('click',function(){
      state.type=btn.dataset.typeFilter;
      typeButtons.forEach(function(b){
        b.setAttribute('aria-pressed',String(b===btn));
      });
      apply();
    });
  });
  if(projectSelect){
    projectSelect.addEventListener('change',function(){
      state.project=projectSelect.value;
      apply();
    });
  }
  var toggle=document.getElementById('theme-toggle');
  if(toggle){
    toggle.addEventListener('click',function(){
      var dark=root.getAttribute('data-theme')==='dark';
      root.setAttribute('data-theme',dark?'light':'dark');
    });
  }
  apply();
})();
</script>
</body>
</html>
Evidence: Board behavior assertions

renders_hidden_decision: True renders_itsflow_review_pr: True marks_unverified_pr: True escapes_shipped_metadata: True does_not_render_unsafe_markup: True sends_answers_immediately: True discuss_path_present: True free_text_standalone: True title: Decisions and reviews firstmate needs from you

renders_hidden_decision: True
renders_itsflow_review_pr: True
marks_unverified_pr: True
escapes_shipped_metadata: True
does_not_render_unsafe_markup: True
sends_answers_immediately: True
discuss_path_present: True
free_text_standalone: True
title: Decisions and reviews firstmate needs from you
Evidence: Relay wake transcript

inbox board: a captain answer is waiting in state/inbox-answers

inbox board: a captain answer is waiting in state/inbox-answers
Evidence: Persisted captain answer sample

status: feedback decision,"DECISION hidden-decision: Preserve the inbox board"

status: feedback
 decision,"DECISION hidden-decision: Preserve the inbox board"
Evidence: Focused regression test transcript
FM_TEST_BEGIN 2026-07-28T20:47:28Z tests/fm-inbox-view.test.sh family=snapshot-bearings expected_gate_skip=optional-binary
ok - --help prints the header and exits 0
ok - Decide selects on the captain hold alone and excludes blocked holds
ok - full decision text is read through tasks-axi and can be opted out of
ok - recorded pull requests render as unverified and landed ones stay out
ok - unknown PR verification stays in Review and merge
ok - shipped completion metadata escapes while PR links render
ok - decision cards render in full and missing cards are declared
ok - answers queue exactly once per question, on submit
ok - free text answers stand alone and options are optional quick-picks
ok - every decision offers a discuss/clarify path
ok - a submitted answer is sent immediately with visible confirmation
ok - no-project items get readable area names instead of a generic badge
ok - long titles wrap instead of overflowing the card
ok - type and project filters are rendered for every section
ok - board generation leaves state/ and data/ byte-identical
ok - a missing explicit cards file refuses instead of rendering a silent gap
FM_TEST_END 2026-07-28T20:47:38Z tests/fm-inbox-view.test.sh exit=0 duration_ms=9338 gate_skip=false
FM_TEST_BEGIN 2026-07-28T20:47:38Z tests/fm-inbox-arm.test.sh family=snapshot-bearings expected_gate_skip=optional-binary
ok - --help prints the header and exits 0
ok - a missing board path refuses
ok - arming writes a mode-0700 relay and registers it for the watcher
ok - editing the relay after registration invalidates it
ok - the relay stays silent and starts no server while the board is unserved
ok - the relay is bound to its board and a durable answer drop
ok - arming refuses linked relay paths without writing through them
ok - the relay shell-escapes paths and exports its target port
ok - the relay emits one fixed wake line for feedback
ok - the relay refuses linked answer drops with a diagnostic wake
ok - serve passes its resolved port into the armed relay
FM_TEST_END 2026-07-28T20:47:40Z tests/fm-inbox-arm.test.sh exit=0 duration_ms=2498 gate_skip=false
FM_TEST_BEGIN 2026-07-28T20:47:40Z tests/fm-test-run.test.sh family=pure-contract-unit expected_gate_skip=none
ok - exact suite coverage: --all lists every tests/*.test.sh once
ok - family selection returns a proper subset of the suite
ok - single-script selection lists exactly that path
ok - changed-file selection stays conservative (never silent full suite)
ok - changed selection covers dependents and fails closed for unmapped source
ok - empty changed selection emits deterministic text and JSON summaries
ok - timing markers and JSON artifact are valid
ok - aggregate exit reflects any script failure
ok - gate-skip accounting is honest and non-failing
ok - fail-on-gate-skip converts herdr-not-found into a hard failure
ok - exclude-family drops the named primary family after selection
ok - CI and CONTRIBUTING call the one-owner runner; no full-suite local Test
ok - portable shard union, disjointness, and coverage guard hold
ok - portable shard documentation matches lane counts and timing sums
ok - --jobs refuses non-proven / stateful selections
ok - jobs scheduler runs proven scripts; failure propagates; non-proven refused
ok - aggregate-json merges lane timing artifacts
FM_TEST_END 2026-07-28T20:47:48Z tests/fm-test-run.test.sh exit=0 duration_ms=7616 gate_skip=false
FM_TEST_BEGIN 2026-07-28T20:47:48Z tests/fm-session-start.test.sh family=session-bootstrap expected_gate_skip=none
ok - context digest distinguishes ABSENT, empty-but-present, and populated files
ok - a lock refusal prints a loud read-only banner, skips every mutating step, and still completes the digest
ok - session start stays read-only when lock ownership cannot be published
ok - concurrent session-lock acquisition admits exactly one live harness
ok - digest sections are ordered diagnostics-first, bulk-context-last
ok - session start: configured and auto-detected Herdr homes never require tmux
ok - session start: an absent recorded tmux window relaunches its Pi secondmate exactly once
ok - session start: an existing ambiguous Pi process prevents duplicate recovery
ok - session start: transient tmux unreadability never licenses a relaunch
ok - session start: the proven bare-shell recovery path remains intact
ok - session start: a confirmed Herdr husk is closed and relaunched
ok - status tail is bounded to the configured line count, with the full log path always printed
ok - orphan status logs are printed once with bounded tails
ok - tmux endpoint liveness is reported per task: alive for a live window, dead for a gone one
ok - herdr endpoint liveness is reported per task: alive for a live pane, dead for a gone one
ok - fm-session-start.sh composes the real fm-lock.sh, fm-bootstrap.sh, and fm-wake-drain.sh output verbatim
ok - compatible tasks-axi backlog rendering is compact, bounded, and preserves recovery metadata
ok - manual backlog rendering prints only title lines with hold and blocker metadata
ok - unavailable or incompatible tasks-axi falls back to compact manual backlog rendering
ok - an empty fleet reports (none) for in-flight tasks and an absent AFK flag
ok - session start emits X-mode cadence guidance in the harness supervision block
ok - next step delegates watcher ownership to the AFK daemon
ok - session start emits exactly one detected harness block and reports Pi extension load state
ok - session start preserves pi-signed primary identity while applying Pi extension guarantees
ok - session start rejects stale Pi loaded markers
ok - session start accepts current Pi markers written before lock acquisition
ok - session start rejects Pi sessions missing the turn-end guard marker
ok - session start rejects Pi loaded markers from previous sessions
FM_TEST_END 2026-07-28T20:48:29Z tests/fm-session-start.test.sh exit=0 duration_ms=40849 gate_skip=false
FM_TEST_BEGIN 2026-07-28T20:48:29Z tests/fm-subagent-pretool-check.test.sh family=pure-contract-unit expected_gate_skip=none
ok - tracked Claude settings do not ship permissions.deny
ok - the guard independently denies every work-creating delegation tool by shape
ok - the guard denies delegation-shaped tools that no deny list knows about yet
ok - the guard leaves ordinary tools and observe-or-stop operations alone
ok - MCP tool names are never classified as harness delegation
ok - deny defers to intake classification and degrades gracefully without fm-scout.sh
ok - the single documented escape hatch releases the guard only on the exact opt-in value
ok - the guard is inert in a crewmate task worktree and in a non-firstmate repo
ok - a marked secondmate home is guarded even though it is a linked worktree
ok - both stdin transports classify correctly and Claude's deny keeps stdout empty
ok - malformed, empty, and tool-name-less payloads fail open rather than blocking every tool call
ok - missing jq for stdin transport fails open rather than denying every tool call
ok - Claude wires the delegation guard, retains only non-status Bash seatbelts, and preserves the Stop guard
FM_TEST_END 2026-07-28T20:48:30Z tests/fm-subagent-pretool-check.test.sh exit=0 duration_ms=1244 gate_skip=false
FM_TEST_SUMMARY total=5 failed=0 skipped_gate=0 duration_ms=61701
FM_TEST_SUMMARY_FAMILY family=pure-contract-unit count=2 duration_ms=8860 failed=0
FM_TEST_SUMMARY_FAMILY family=session-bootstrap count=1 duration_ms=40849 failed=0
FM_TEST_SUMMARY_FAMILY family=snapshot-bearings count=2 duration_ms=11836 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-session-start.test.sh duration_ms=40849
FM_TEST_SLOWEST rank=2 script=tests/fm-inbox-view.test.sh duration_ms=9338
FM_TEST_SLOWEST rank=3 script=tests/fm-test-run.test.sh duration_ms=7616
FM_TEST_SLOWEST rank=4 script=tests/fm-inbox-arm.test.sh duration_ms=2498
FM_TEST_SLOWEST rank=5 script=tests/fm-subagent-pretool-check.test.sh duration_ms=1244
Evidence: Changed-test selection transcript
tests/fm-arm-pretool-check.test.sh
tests/fm-ask-user-authority.test.sh
tests/fm-brief.test.sh
tests/fm-calm-pi-extension.test.sh
tests/fm-captain-translation-contract.test.sh
tests/fm-cd-pretool-check.test.sh
tests/fm-composer-ghost.test.sh
tests/fm-composer-lib.test.sh
tests/fm-crew-state.test.sh
tests/fm-decision-hold-lifecycle.test.sh
tests/fm-documentation-audiences.test.sh
tests/fm-ensure-agents-md.test.sh
tests/fm-grok-harness.test.sh
tests/fm-herdr-lab.test.sh
tests/fm-install-herdr.test.sh
tests/fm-instruction-owners.test.sh
tests/fm-kimi-harness.test.sh
tests/fm-lint.test.sh
tests/fm-nm-test-contract.test.sh
tests/fm-no-mistakes-ownership.test.sh
tests/fm-operational-input.test.sh
tests/fm-pi-primary-types.test.sh
tests/fm-send-popup-settle.test.sh
tests/fm-send-settle.test.sh
tests/fm-stow-contract.test.sh
tests/fm-subagent-pretool-check.test.sh
tests/fm-supervision-instructions.test.sh
tests/fm-test-isolation-proof.test.sh
tests/fm-test-run.test.sh
tests/fm-tmux-submit-busy.test.sh
tests/fm-transition-lib.test.sh
tests/fm-inbox-arm.test.sh
tests/fm-inbox-view.test.sh
tests/fm-session-start.test.sh
Evidence: Fork/upstream remote configuration

origin git@github.com:ItsFlow/firstmate.git (fetch) origin git@github.com:ItsFlow/firstmate.git (push) upstream https://github.com/kunchenguid/firstmate (fetch) upstream DISABLED (push)

origin	git@github.com:ItsFlow/firstmate.git (fetch)
origin	git@github.com:ItsFlow/firstmate.git (push)
upstream	https://github.com/kunchenguid/firstmate (fetch)
upstream	DISABLED (push)
Evidence: Merge commit evidence

89783a92af05539a46615e5f70abc9a5e1eefdd5 a23849af17d124ad82edfaa1705d0d2cc280db32 fa0d85d00be145196d60eee5dffcbe18ab3af901 Merge upstream/main into ItsFlow main

89783a92af05539a46615e5f70abc9a5e1eefdd5
a23849af17d124ad82edfaa1705d0d2cc280db32 fa0d85d00be145196d60eee5dffcbe18ab3af901
Merge upstream/main into ItsFlow main

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

⚠️ **Rebase** - 52 warnings
  • ⚠️ .agents/skills/afk/SKILL.md - merge conflict rebasing onto origin/main
  • ⚠️ .agents/skills/bootstrap-diagnostics/SKILL.md - merge conflict rebasing onto origin/main
  • ⚠️ .agents/skills/harness-adapters/SKILL.md - merge conflict rebasing onto origin/main
  • ⚠️ .claude/settings.json - merge conflict rebasing onto origin/main
  • ⚠️ .pi/extensions/fm-calm.ts - merge conflict rebasing onto origin/main
  • ⚠️ .pi/extensions/fm-primary-pi-watch.ts - merge conflict rebasing onto origin/main
  • ⚠️ .pi/extensions/lib/fm-calm-assistant-layout.ts - merge conflict rebasing onto origin/main
  • ⚠️ .pi/extensions/lib/fm-calm-operational-user-layout.ts - merge conflict rebasing onto origin/main
  • ⚠️ AGENTS.md - merge conflict rebasing onto origin/main
  • ⚠️ README.md - merge conflict rebasing onto origin/main
  • ⚠️ bin/backends/tmux.sh - merge conflict rebasing onto origin/main
  • ⚠️ bin/fm-bootstrap.sh - merge conflict rebasing onto origin/main
  • ⚠️ bin/fm-claude-stop-autoarm.sh - merge conflict rebasing onto origin/main
  • ⚠️ bin/fm-continuity-pretool-check.sh - merge conflict rebasing onto origin/main
  • ⚠️ bin/fm-dispatch-select.sh - merge conflict rebasing onto origin/main
  • ⚠️ bin/fm-session-lock-lib.sh - merge conflict rebasing onto origin/main
  • ⚠️ bin/fm-spawn.sh - merge conflict rebasing onto origin/main
  • ⚠️ bin/fm-test-run.sh - merge conflict rebasing onto origin/main
  • ⚠️ bin/fm-turnend-guard.sh - merge conflict rebasing onto origin/main
  • ⚠️ docs/architecture.md - merge conflict rebasing onto origin/main
  • ⚠️ docs/arm-pretool-check.md - merge conflict rebasing onto origin/main
  • ⚠️ docs/calm-mode-feasibility.md - merge conflict rebasing onto origin/main
  • ⚠️ docs/configuration.md - merge conflict rebasing onto origin/main
  • ⚠️ docs/documentation-audiences.json - merge conflict rebasing onto origin/main
  • ⚠️ docs/examples/crew-dispatch.json - merge conflict rebasing onto origin/main
  • ⚠️ docs/herdr-backend.md - merge conflict rebasing onto origin/main
  • ⚠️ docs/scripts.md - merge conflict rebasing onto origin/main
  • ⚠️ docs/sessionstart-nudge.md - merge conflict rebasing onto origin/main
  • ⚠️ docs/subagent-guard.md - merge conflict rebasing onto origin/main
  • ⚠️ docs/supervision-protocols/claude.md - merge conflict rebasing onto origin/main
  • ⚠️ docs/supervision-protocols/pi.md - merge conflict rebasing onto origin/main
  • ⚠️ docs/tmux-backend.md - merge conflict rebasing onto origin/main
  • ⚠️ docs/turnend-guard.md - merge conflict rebasing onto origin/main
  • ⚠️ docs/verification/runtime-backends.md - merge conflict rebasing onto origin/main
  • ⚠️ docs/verification/supervision.md - merge conflict rebasing onto origin/main
  • ⚠️ docs/watcher-continuity.md - merge conflict rebasing onto origin/main
  • ⚠️ docs/zellij-backend.md - merge conflict rebasing onto origin/main
  • ⚠️ tests/fm-backend.test.sh - merge conflict rebasing onto origin/main
  • ⚠️ tests/fm-bootstrap.test.sh - merge conflict rebasing onto origin/main
  • ⚠️ tests/fm-calm-pi-extension.test.sh - merge conflict rebasing onto origin/main
  • ⚠️ tests/fm-claude-continuity-live-e2e.test.sh - merge conflict rebasing onto origin/main
  • ⚠️ tests/fm-claude-stop-autoarm-live-e2e.test.sh - merge conflict rebasing onto origin/main
  • ⚠️ tests/fm-claude-stop-autoarm.test.sh - merge conflict rebasing onto origin/main
  • ⚠️ tests/fm-continuity-pretool-check.test.sh - merge conflict rebasing onto origin/main
  • ⚠️ tests/fm-dispatch-select.test.sh - merge conflict rebasing onto origin/main
  • ⚠️ tests/fm-pr-check-security.test.sh - merge conflict rebasing onto origin/main
  • ⚠️ tests/fm-secondmate-liveness.test.sh - merge conflict rebasing onto origin/main
  • ⚠️ tests/fm-session-start.test.sh - merge conflict rebasing onto origin/main
  • ⚠️ tests/fm-spawn-dispatch-profile.test.sh - merge conflict rebasing onto origin/main
  • ⚠️ tests/fm-subagent-pretool-check.test.sh - merge conflict rebasing onto origin/main
  • ⚠️ tests/fm-supervision-instructions.test.sh - merge conflict rebasing onto origin/main
  • ⚠️ tests/fm-turnend-guard.test.sh - merge conflict rebasing onto origin/main
🔧 **Review** - 2 issues found → auto-fixed (2) ✅
  • 🚨 bin/fm-inbox-arm.sh:113 - The generated relay persists captain feedback with plain mkdir -p and shell redirection, then suppresses persistence failures with exit 0. With a normal umask this can create readable answer drops, it follows a pre-existing state/inbox-answers symlink, and a write failure after feedback leaves the watcher silent instead of surfacing that the answer was not recorded. Publish the answer through the repo's guarded private-artifact pattern or explicitly enforce a 0700 directory/0600 single-link file and emit a diagnostic wake on failure.
  • ⚠️ .claude/settings.json:37 - The merge result registers the same catch-all Claude PreToolUse hook twice, so every tool use runs fm-subagent-pretool-check.sh --claude twice. Remove one duplicate matcher block to avoid duplicated guard latency and duplicated refusal output.

🔧 Fix: Harden inbox relay and hooks
1 warning still open:

  • ⚠️ bin/fm-inbox-render.py:1029 - When a shipped item has a PR URL, sub is passed to render_row as raw HTML so the intentional PR anchor can render, but the completion date prefix is concatenated without escaping. Completion dates come from backlog metadata, so malformed local backlog text can render markup in the served inbox board. Escape the label/date before appending the link, or split render_row into text and trusted HTML parts.

🔧 Fix: Escape inbox shipped metadata
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • git status --short --branch, git log --oneline --decorate --graph --max-count=30, and git diff --stat fa0d85d00be145196d60eee5dffcbe18ab3af901..a644cfdfbf579eb88658a7466636b43c5e4096e4
  • git show --no-patch --pretty=raw 89783a9
  • git remote -v
  • bin/fm-test-run.sh --list --changed --base fa0d85d00be145196d60eee5dffcbe18ab3af901 > /var/folders/tc/llfmckm54ls5tzxm08q758lm0000gn/T/no-mistakes-evidence/01KYN36AHC52C8CH3HEASR7TQS/changed-selection.txt
  • bin/fm-test-run.sh --json /var/folders/tc/llfmckm54ls5tzxm08q758lm0000gn/T/no-mistakes-evidence/01KYN36AHC52C8CH3HEASR7TQS/focused-tests-timing.json tests/fm-inbox-view.test.sh tests/fm-inbox-arm.test.sh tests/fm-test-run.test.sh tests/fm-session-start.test.sh tests/fm-subagent-pretool-check.test.sh
  • Manual evidence fixture: FM_HOME=&lt;evidence&gt;/manual-inbox-home PATH=&lt;evidence&gt;/manual-fakebin:$PATH bin/fm-inbox-view.sh --cards &lt;fixture&gt;/data/inbox-cards.md &lt;evidence&gt;/captain-inbox-board.html
  • Manual relay check: FM_HOME=&lt;evidence&gt;/manual-inbox-home PATH=&lt;evidence&gt;/manual-fakebin:$PATH bin/fm-inbox-arm.sh --port 5173 &lt;evidence&gt;/captain-inbox-board.html, then PATH=&lt;evidence&gt;/manual-fakebin:$PATH bash &lt;fixture&gt;/state/inbox.check.sh
  • python3 - /var/folders/tc/llfmckm54ls5tzxm08q758lm0000gn/T/no-mistakes-evidence/01KYN36AHC52C8CH3HEASR7TQS/captain-inbox-board.html > /var/folders/tc/llfmckm54ls5tzxm08q758lm0000gn/T/no-mistakes-evidence/01KYN36AHC52C8CH3HEASR7TQS/board-assertions.txt
  • git status --short
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

ItsFlow and others added 7 commits July 26, 2026 18:29
* fix: block primary-session delegation outside the fleet (#854)

* feat: fence primary-session delegation outside the fleet

A firstmate primary that delegates through Claude Code's built-in
delegation tools creates work with no state/<id>.meta. Because
fm-supervision-lib.sh counts *.meta and fm-turnend-guard.sh exits
silently at zero, such work does not merely go unsupervised: it makes
the whole guard stack structurally inert, and it dies with the primary
session. On 2026-07-22 that cost two workers mid-flight and left
supervision down for 73 minutes unnoticed.

Layer 1, the primary fix: a permissions.deny list in
.claude/settings.json removes the 18 delegation, scheduling, worktree,
and task-tracking tools from the model's schema, so they are never
offered. This is removal rather than interception, so there is no call
to intercept and no fail-open path. The list is flat and in one file so
its width stays reviewable; the captain owns that width.

Layer 2, bin/fm-subagent-pretool-check.sh: a deny list is fail-open
against tools that do not exist yet, and permissions.allow is a
pre-approval list rather than an availability list, so there is no
fail-closed allowlist to use instead. This backstop classifies the tool
NAME by shape rather than against a fixed list, so a delegation tool
that ships before the deny list is updated is still refused. It excludes
mcp__* names and observe-or-stop operations, scopes itself to a genuine
primary home via the shared fm_primary_scope_matches predicate so a
crewmate's task worktree is unaffected, and offers one deliberate
FM_ALLOW_SUBAGENT=1 escape hatch that must be set at launch.

Verified live against Claude Code 2.1.217, including a deny-key A/B with
a nonsense-name control, layer 2 denying an un-denied Workflow call, the
same call allowed in a linked worktree, and the escape hatch. Corrects a
prior finding: both Task and Agent work as deny keys, so both are
pinned. Codex 0.144.1 verified to expose no delegation tool; grok,
opencode, and pi are inspected and documented as not wired because those
binaries are absent from this host and the repo requires live validation
before trusting a harness hook. Evidence in docs/subagent-guard.md.

* no-mistakes(review): Ship scoped Claude delegation guard

* no-mistakes(test): Ship Claude delegation deny list

* no-mistakes(document): Clarify PreToolUse guard ownership

* no-mistakes(lint): Keep Claude deny list local

* fix: install tasks-axi in portable CI shards (#866)

Reproduction: portable-parallel-2 completed successfully without tasks-axi while fm-decision-hold-lifecycle emitted a gate skip in 30 ms. The pre-shard lane installed tasks-axi and exercised the test fully. Installing tasks-axi is the smallest counterfactual and makes the representative shard execute the test with gate_skip=false in about 20 seconds. Both parallel jobs receive symmetric setup, while the exact 91-test inventory and coverage guard remain unchanged.

* feat(bin): make dispatch profiles quota aware (#867)

* feat: make dispatch profiles quota aware

* no-mistakes(review): Fix quota window and Grok product scoping

* no-mistakes(document): Document implicit quota-aware dispatch accurately

* Add built-in ahoy recap skill (#873)

* fix: preserve trustworthy Bearings data in partial snapshots (#875)

* fix: preserve mixed Bearings projections

* no-mistakes(review): Enforce strict invalidity precedence for partial snapshots

* no-mistakes(review): Enforce ownership for unknown child metadata

* no-mistakes(document): Document partial structured Bearings projections

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* feat(pi): add session-local calm mode (#884)

* Add session-local Pi calm mode

* no-mistakes(review): Preserve Pi HTML exports during calm mode

* no-mistakes(review): Preserve calm exports across submit bindings and share

* no-mistakes(document): Document calm-mode feasibility across supported harnesses

* fix(pi): prevent redundant watcher re-arms (#885)

* fix(pi): limit watcher arm tool to recovery

* no-mistakes(review): Strengthen Pi live re-arm regression coverage

* no-mistakes(document): Document Pi first-cycle and recovery-only watcher arming

* fix(pi): clean up Calm transcript rendering (#895)

* fix(pi): clean up Calm transcript rendering

* no-mistakes(review): Captain, preserve Calm exports and classify Pi launch briefs

* no-mistakes(review): Captain, eliminate Calm gaps and verify exported conversations

* no-mistakes(review): Restore Calm rows received while active

* no-mistakes(review): Preserve diagnostics during Calm restoration

* no-mistakes(document): Clarify Calm transcript behavior and injection paths

* fix: execute every PR body compliance event (#898)

* fix: execute every PR body compliance event

* no-mistakes(document): Document independent PR compliance events

* fix: exclude operational injections from ahoy boundaries (#899)

* fix: distinguish operational input in ahoy

* no-mistakes(review): Handle legacy Ahoy operational boundaries

* no-mistakes(review): Narrow legacy Ahoy boundaries with live regressions

* no-mistakes(document): Document Ahoy operational marker ownership

* no-mistakes(lint): Suppress intentional literal fixture lint warnings

* fix: canonically classify operational inputs across harnesses (#909)

* fix: type canonical operational inputs

* no-mistakes(document): Correct canonical operational-input documentation ownership

* fix: avoid generic secondmate start acknowledgements (#926)

* fix: avoid generic secondmate acknowledgements

* no-mistakes(document): Document sparse secondmate acknowledgement behavior

* no-mistakes: apply CI fixes

* fix(pi): make calm mode persistent and gapless (#927)

* fix(pi): preserve calm presentation across sessions

* no-mistakes(review): Fix Calm home fallback persistence

* no-mistakes(document): Clarify Calm gapless and export contracts

* no-mistakes: apply CI fixes

* fix(watch): retire merged PR polls after durable notification (#932)

* fix: retire merged PR polls after notification

* no-mistakes(review): Decouple PR retirement recovery from template updates

* no-mistakes(review): Recover pending PR retirements before poll migration

* no-mistakes(document): Document merged PR poll retirement contracts

* fix: refine scout intake and parallel dispatch (#934)

* Clarify intake evidence and overlap handling

* no-mistakes(review): Align scout guard with intake classification

* no-mistakes(document): Clarify scout documentation and intake ownership

* fix(pi): prevent duplicate assistant replies in Calm (#936)

* fix(pi): preserve operational follow-up semantics in Calm

* no-mistakes(document): Correct Calm operational-row visibility documentation

* perf(bin): shrink the ShellCheck source graph (#939)

* perf(lint): shrink shell source graph

* no-mistakes(review): Ensure lint workers terminate fully on cancellation

* no-mistakes(document): Repair stale lint documentation ownership

* fix(pi): remove Calm hidden-block gaps (#942)

* fix(pi): remove Calm hidden-block gaps

* no-mistakes(review): Validate Calm geometry against current viewport

* no-mistakes(review): Synchronize Calm geometry checks with reload completion

* fix: enforce contract boundaries for ask-user findings (#945)

* fix: escalate ask-user contract expansion

* no-mistakes(document): Point project management to authority owner

* docs: prefer direct operational paths (#946)

* fix(pi): hide operational user rows in Calm mode (#948)

* fix(pi): hide Calm operational user rows

* no-mistakes(review): Narrow Calm operational input suppression

* no-mistakes(review): Avoid Calm replay classifier subprocesses

* no-mistakes(document): docs: point Pi verification to Calm owner

* fix: relaunch missing second mates at session start (#950)

* fix(session-start): relaunch missing second mates

* fix(test): detect completed parallel workers

* no-mistakes(review): Isolate session-start recovery test cleanup

* no-mistakes(review): Complete backend-safe secondmate session recovery

* no-mistakes(review): Resolve Zellij task ownership before recovery

* no-mistakes(review): Recover relocated Zellij ghost tabs safely

* no-mistakes(review): Restore conservative Zellij recovery boundary

* no-mistakes(review): Reject malformed tmux recovery targets

* no-mistakes(document): Align secondmate recovery documentation

* no-mistakes: apply CI fixes

* fix(herdr): reclaim resumed task projections after restart (#967)

* fix(herdr): reclaim resumed task projections safely

* no-mistakes(review): Enforce safe Herdr reclaim close boundaries

* no-mistakes(document): docs: clarify Herdr restart projection contract

* Teach Ahoy to surface open decisions (#968)

* Require shipshape routine acknowledgement (#969)

* docs: separate current guidance from verification evidence (#994)

* docs: separate current guides from verification

* no-mistakes(review): Restore Herdr 0.7.5 restart-reclaim verification evidence

* fix: preserve Claude watcher continuity across Stop hooks (#997)

* feat(claude): Stop-owned tokenless watcher continuity via asyncRewake auto-arm

Claude primaries (main home and marked secondmate homes) no longer depend
on the model remembering to re-arm the watcher after each wake. A tracked
Stop asyncRewake hook (bin/fm-claude-stop-autoarm.sh, timeout 28800s)
fires on every turn end, claims one home-scoped single-flight owner,
foregrounds bin/fm-watch-arm.sh inside the hook-owned process tree, and
translates an actionable close or typed watcher failure into exactly one
exit-2 rewake. The hook scopes to genuine primary checkouts, requires the
session lock to be held by its own harness ancestor, stays inert while
AFK owns triage or the home is idle, and hands AFK transitions mid-cycle
to the daemon without rewaking.

The synchronous turn-end guard gains a --claude cooperative mode: it
ignores stop_hook_active (true on every post-continuation stop, which is
what re-opened the 2026-07-21 blind window), waits briefly for a watcher
health proof, a live auto-arm owner claim, or a fresh rewake epoch, and
re-blocks only when the auto-arm genuinely failed to establish - bounded
to 3 consecutive blocks per session, safely below Claude Code's 8-block
override, then a degraded allow with a visible systemMessage. Codex
keeps the previous one-block loop guard byte-identically, and Pi,
OpenCode, and Grok adapters are untouched.

Continuity PreToolUse gate and durable wake queue are preserved; the
gate's recovery guidance now names the Stop-owned re-arm and reserves
manual background arms for auto-arm failure. Claude supervision protocol,
harness-adapters facts, architecture, configuration, and continuity docs
updated; docs/turnend-guard.md records the 2026-07-24 Claude 2.1.218
contract revalidation (tokenless multi-cycle rewake, no-dedup, timeout
process-group kill, 8-block cap, interactive non-stall) and the 2.1.219
product live E2Es.

Regression matrix: hermetic tests cover scope, identity, AFK, need,
single-flight, translation, guard cooperation, budget, and registration;
the new live E2E proves two full tokenless auto-arm rewake cycles with
zero model arm commands; Pi and OpenCode Option B live E2Es pass
unchanged.

* no-mistakes(review): Fix Claude X-mode auto-arm continuity backstop

* no-mistakes(review): Remove unsupported Claude contract-lab verification claims

* no-mistakes(document): Update Claude auto-arm continuity documentation

* fix(herdr): clean stale projections at session start (#996)

* Clean stale Herdr projections at session start

* no-mistakes(document): Document stale Herdr session-start projection cleanup

* no-mistakes(review): Enforce locked exact Herdr projection cleanup

* no-mistakes(review): Fail closed on unverified session lock ownership

* no-mistakes(review): Serialize session lock acquisition atomically

* no-mistakes(document): Align session-start and Herdr cleanup documentation

* no-mistakes(document): Generalize lock-refusal diagnostics

* no-mistakes(lint): Avoid reserved keyword in concurrency test

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* feat(inbox): add the captain's decision and review board generator

bin/fm-inbox-view.sh writes one self-contained HTML board from live fleet
state, so every open captain decision, review-ready pull request and stopped
task is answerable from a single surface instead of arriving one chat
interrupt at a time.

It is a read-only projection in the same shape as fm-fleet-view.sh: it shells
out to fm-fleet-snapshot.sh --json and `tasks-axi show --full`, and writes
nothing but the board file. A test proves state/ and data/ stay byte-identical
across a generation.

Three selection defects the existing aggregated surface has are fixed here.
Decisions are selected on the captain hold alone, because the snapshot's own
captain_actionable flag also requires kind == "captain" and therefore hides
every thread gated with `tasks-axi hold --kind captain`, which is where the
oldest waiting decisions live.
Decision text is read through tasks-axi, because the snapshot's metadata
capture stops at the first comma and silently drops the options from any hold
reason that lists them.
A recorded pull request renders as unverified unless --verify-prs live-checks
it, because locally recorded metadata cannot know a pull request was closed
and a closed one must never read as ready to merge.

Decision cards lead with plain English. An optional captain-private cards file
supplies the plain question, what the captain is actually choosing, where it
came from, firstmate's recommendation, a link to research a product or
service, and a collapsed technical section. An item that is firstmate's own
assumption rather than a choice the captain made is flagged as one. A decision
with no card still renders and says so rather than looking complete.

Answers use the Lavish input contract: native radios plus a free-text note,
with exactly one queued prompt per question on submit. This change delivers
the surface only; regenerating on a cadence, relaying answers back, and any
escalation-routing change remain separate captain decisions.

* feat(inbox): free-form answers, discuss path, reliable delivery, overflow fix

Fold in the captain's hard requirements from live use of the board.

Free-form answers are now first-class. A card's predefined options are
optional quick-picks with a clear-selection reset; the answer submits with
just free text and no option selected. Previously the submit required picking
an offered option, so a genuine free-form answer could not be sent at all.

Each card gains a discuss path that sends a question back (DISCUSS <id>: ...)
instead of an answer (DECISION <id>: ...), so a decision can be clarified
rather than forced.

Delivery is made reliable end to end. bin/fm-inbox-serve.sh is one operator
command that regenerates the board, arms the answer relay, serves it on the
Tailscale address, and verifies the link. bin/fm-inbox-arm.sh writes and
registers state/inbox.check.sh, a bounded lavish poll bound by
fm-check-register, so firstmate's supervision cycle relays answers on its
normal check cadence without anyone remembering to poll. The relay fails
closed: when the board is not served the poll errors fast and it stays silent.
Submitting also sends immediately with a visible confirmation, closing the
window where an answer looked silently eaten.

Long titles and ids now wrap instead of overflowing horizontally, which
Lavish's own layout audit flags as an error. No-project items get readable
area names derived from their origin or id instead of a generic badge.

Behavior is covered by tests/fm-inbox-view.test.sh (generator, read-only,
free-form, discuss, immediate send, area names, wrapping) and
tests/fm-inbox-arm.test.sh (the relay is a valid registered fail-closed
check, silent while unserved, rejected after tampering). docs/inbox-board.md
records the design and verification evidence.

* no-mistakes(review): Fix inbox relay safety and PR verification

* no-mistakes(review): Gate inbox relay before polling

* no-mistakes(document): Align inbox board docs

* no-mistakes: apply CI fixes

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
# Conflicts:
#	.agents/skills/afk/SKILL.md
#	.agents/skills/bootstrap-diagnostics/SKILL.md
#	.agents/skills/harness-adapters/SKILL.md
#	.pi/extensions/fm-calm.ts
#	.pi/extensions/fm-primary-pi-watch.ts
#	.pi/extensions/lib/fm-calm-assistant-layout.ts
#	.pi/extensions/lib/fm-calm-operational-user-layout.ts
#	AGENTS.md
#	README.md
#	bin/backends/tmux.sh
#	bin/fm-bootstrap.sh
#	bin/fm-claude-stop-autoarm.sh
#	bin/fm-continuity-pretool-check.sh
#	bin/fm-dispatch-select.sh
#	bin/fm-session-lock-lib.sh
#	bin/fm-spawn.sh
#	bin/fm-test-run.sh
#	docs/architecture.md
#	docs/arm-pretool-check.md
#	docs/calm-mode-feasibility.md
#	docs/configuration.md
#	docs/documentation-audiences.json
#	docs/examples/crew-dispatch.json
#	docs/herdr-backend.md
#	docs/scripts.md
#	docs/sessionstart-nudge.md
#	docs/subagent-guard.md
#	docs/supervision-protocols/claude.md
#	docs/supervision-protocols/pi.md
#	docs/tmux-backend.md
#	docs/turnend-guard.md
#	docs/verification/runtime-backends.md
#	docs/verification/supervision.md
#	docs/watcher-continuity.md
#	docs/zellij-backend.md
#	tests/fm-bootstrap.test.sh
#	tests/fm-calm-pi-extension.test.sh
#	tests/fm-claude-continuity-live-e2e.test.sh
#	tests/fm-claude-stop-autoarm-live-e2e.test.sh
#	tests/fm-claude-stop-autoarm.test.sh
#	tests/fm-continuity-pretool-check.test.sh
#	tests/fm-dispatch-select.test.sh
#	tests/fm-secondmate-liveness.test.sh
#	tests/fm-spawn-dispatch-profile.test.sh
#	tests/fm-subagent-pretool-check.test.sh
#	tests/fm-supervision-instructions.test.sh
@ItsFlow
ItsFlow merged commit 61cd98d into main Jul 28, 2026
10 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.

1 participant