Skip to content

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

Merged
ItsFlow merged 34 commits into
mainfrom
fm/inbox-board-v2
Jul 26, 2026
Merged

feat(bin): add captain inbox board#1
ItsFlow merged 34 commits into
mainfrom
fm/inbox-board-v2

Conversation

@ItsFlow

@ItsFlow ItsFlow commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Recovery note: this PR preserves the exact no-mistakes-validated head (fd6b54e2b522683589213e0d3c170c1714be5d97) from the closed upstream PR (kunchenguid/firstmate#1004, closed unmerged by policy). The body below, including the Pipeline section, is the pipeline-written body no-mistakes produced for this same head on that PR, restored verbatim. This fork PR targets ItsFlow/firstmate only.

Intent

Ship the captain's decision-and-review board (firstmate shared tracked material). The board is one Lavish HTML surface that collects everything waiting on the captain (decisions to make, PRs to approve, stopped work) so it arrives in one place instead of scattered chat interruptions. Deliverables: bin/fm-inbox-view.sh (a pure READ-ONLY generator projecting bin/fm-fleet-snapshot.sh --json plus 'tasks-axi show --full'; must mutate nothing under state/ or data/, proven by a test), bin/fm-inbox-render.py (HTML renderer), bin/fm-inbox-arm.sh and bin/fm-inbox-serve.sh (serve + arm the answer relay), tests, and docs/inbox-board.md.

The captain approved the board shape yesterday and, from live use, stressed five HARD requirements folded into this change: (1) FREE-FORM ANSWER IS FIRST-CLASS - every decision card must accept a standalone free-text answer with NO forced option selection; predefined options are optional quick-picks with a clear-selection reset. (2) A per-card discuss/clarify path that sends a question back (DISCUSS : ...) instead of an answer (DECISION : ...). (3) RELIABLE DELIVERY - his prior answers never reached firstmate because nothing polled the board; fm-inbox-serve.sh now serves AND arms a registered watcher check (state/inbox.check.sh via fm-check-register.sh, same pattern as the Workflowy @go channel) that relays answers on the normal supervision cadence with no manual polling, and the submit sends immediately with visible confirmation. The relay fails closed: silent when the board is unserved. (4) Fix horizontal overflow so long titles wrap (Lavish's layout audit flagged it error-severity). (5) Clearer names for no-project items, derived from origin/id.

Deliberate design decisions a reviewer should know: The generator selects open captain decisions on hold_kind==captain ALONE, intentionally wider than the snapshot's captain_actionable flag (which also requires kind==captain and thus hides the captain's oldest holds). Full decision text is read via tasks-axi show --full because the snapshot truncates hold reasons at the first comma. Recorded PRs render as UNVERIFIED unless --verify-prs live-checks them, because local metadata cannot know a PR was closed. Plain-English card copy lives in an optional captain-private cards file (data/inbox-cards.md), not in tracked code. fm-inbox-view stays a pure read-only generator; the stateful arming lives in the separate fm-inbox-serve/fm-inbox-arm so the no-mutation guarantee and its test stay clean. This touches firstmate shared tracked material, so firstmate-coding-guidelines was followed (one sentence per line in Markdown, plain dashes, no agent co-author, shellcheck-clean scripts, colocated tests, evidence-backed doc). A pre-existing unrelated failure in tests/fm-bearings-snapshot.test.sh (an SSHHIP-child warning) exists on the base commit and is out of scope.

What Changed

  • Adds a read-only Lavish inbox board generator and renderer that collects captain decisions, recorded PRs, and stopped work into one HTML surface, with optional plain-English card copy and opt-in live PR verification.
  • Adds serve and arm commands that publish the board, register a bounded state/inbox.check.sh relay, and deliver submitted answers through the normal supervision cadence while staying silent when unserved.
  • Updates docs and focused tests for read-only generation, free-form answers, discuss messages, quick-pick clearing, long-title wrapping, relay safety, and changed-test selection.

Risk Assessment

✅ Low: The follow-up is narrow, source-verifiable, and addresses the prior relay-spawn and wake-count defects without introducing a material remaining risk I can substantiate.

Testing

After fixing an initial changed-test selector gap, I reran the runner and inbox behavior tests, generated and served a fixture board, verified read-only manifests and relay registration, captured desktop/mobile/interaction screenshots, and confirmed free-text answers, discuss prompts, immediate send confirmation, and no horizontal overflow all worked.

  • Evidence: Rendered inbox board desktop (local file: /var/folders/tc/llfmckm54ls5tzxm08q758lm0000gn/T/no-mistakes-evidence/01KYB60J8J5SM9F7Z527TA80M4/inbox-board-rendered.png)
  • Evidence: Mobile wrapping / no-overflow view (local file: /var/folders/tc/llfmckm54ls5tzxm08q758lm0000gn/T/no-mistakes-evidence/01KYB60J8J5SM9F7Z527TA80M4/inbox-board-mobile-wrap.png)
  • Evidence: Free-text answer and discuss confirmation (local file: /var/folders/tc/llfmckm54ls5tzxm08q758lm0000gn/T/no-mistakes-evidence/01KYB60J8J5SM9F7Z527TA80M4/inbox-board-interaction.png)
Evidence: Generated inbox board HTML
<!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="

... [7589 bytes truncated] ...

; tracked as workflowy-partner-idea.</p></div></details><form class="answer" data-lavish-question="workflowy-partner-idea" data-question="Full title for workflowy-partner-idea" 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="workflowy-partner-idea-discuss" data-question="Full title for workflowy-partner-idea" 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</span><span class="sub"><a href="https://github.com/kunchenguid/firstmate/pull/920" rel="noreferrer">https://github.com/kunchenguid/firstmate/pull/920</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-07</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</span><span class="sub">queued since 2026-07-05</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</span><span class="sub">merged 2026-07-06 &middot; <a href="https://github.com/kunchenguid/firstmate/pull/7" rel="noreferrer">https://github.com/kunchenguid/firstmate/pull/7</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: Browser interaction payload and layout check
{
  "prompts": [
    {
      "text": "DECISION hidden-decision: Please ship it from free text only.",
      "tag": "decision",
      "queueKey": "hidden-decision",
      "data": {
        "tag": "decision",
        "queueKey": "hidden-decision",
        "summary": "Should we approve the wrapped-title launch decision? -> Please ship it from free text only.",
        "hold": "hidden-decision",
        "question": "Should we approve the wrapped-title launch decision?",
        "answer": "",
        "note": "Please ship it from free text only."
      }
    },
    {
      "text": "DISCUSS hidden-decision: What risk remains before rollout?",
      "tag": "discuss",
      "queueKey": "hidden-decision-discuss",
      "data": {
        "tag": "discuss",
        "queueKey": "hidden-decision-discuss",
        "summary": "Question on Should we approve the wrapped-title launch decision?",
        "hold": "hidden-decision",
        "question": "Should we approve the wrapped-title launch decision?",
        "message": "What risk remains before rollout?"
      }
    }
  ],
  "sentCalls": 2,
  "answerStatus": "Sent to firstmate.",
  "discussStatus": "Question sent to firstmate.",
  "desktopLayout": {
    "clientWidth": 1280,
    "scrollWidth": 1280
  },
  "mobileLayout": {
    "clientWidth": 390,
    "scrollWidth": 390
  }
}
Evidence: Serve and arm output

armed: state/inbox.check.sh relays answers for /var/folders/tc/llfmckm54ls5tzxm08q758lm0000gn/T/no-mistakes-evidence/01KYB60J8J5SM9F7Z527TA80M4/inbox-board.html board: /var/folders/tc/llfmckm54ls5tzxm08q758lm0000gn/T/no-mistakes-evidence/01KYB60J8J5SM9F7Z527TA80M4/inbox-board.html link: http://127.0.0.1:4891/session/31e87e96a7fed7be&#10;reachable: yes (HTTP 200 over Tailscale)

armed: state/inbox.check.sh relays answers for /var/folders/tc/llfmckm54ls5tzxm08q758lm0000gn/T/no-mistakes-evidence/01KYB60J8J5SM9F7Z527TA80M4/inbox-board.html
board: /var/folders/tc/llfmckm54ls5tzxm08q758lm0000gn/T/no-mistakes-evidence/01KYB60J8J5SM9F7Z527TA80M4/inbox-board.html
link:  http://127.0.0.1:4891/session/31e87e96a7fed7be
reachable: yes (HTTP 200 over Tailscale)
Evidence: Relay registration check

registered=yes mode=700 relay=/var/folders/tc/llfmckm54ls5tzxm08q758lm0000gn/T/no-mistakes-evidence/01KYB60J8J5SM9F7Z527TA80M4/fixture-home/state/inbox.check.sh

registered=yes
mode=700
relay=/var/folders/tc/llfmckm54ls5tzxm08q758lm0000gn/T/no-mistakes-evidence/01KYB60J8J5SM9F7Z527TA80M4/fixture-home/state/inbox.check.sh
Evidence: Read-only manifest check

state/ and data/ manifests are byte-identical after fm-inbox-view.sh

state/ and data/ manifests are byte-identical after fm-inbox-view.sh

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 4 issues found → auto-fixed (2) ✅
  • ⚠️ bin/fm-inbox-render.py:321 - When gh pr view fails, fm-inbox-view.sh records the state as unknown, but this branch treats every verified non-OPEN state as stale. With --verify-prs, a transient auth/network failure can move an open PR out of “Review and merge” and label it “already unknown”; keep unknown in the review list as unchecked instead.
  • ⚠️ bin/fm-inbox-arm.sh:68 - The registered relay runs bare lavish-axi poll, so a board served with a non-default LAVISH_AXI_PORT is verified by fm-inbox-serve.sh but later polled on the watcher's default port unless that environment happens to match. Capture/export the served port in the generated check so non-default-port boards do not silently miss answers.
  • ⚠️ bin/fm-inbox-arm.sh:58 - cat &gt; &#34;$CHECK&#34; opens the predictable state/inbox.check.sh path before destination validation, so an existing symlink or hard link can be overwritten outside the intended state file before fm-check-register.sh rejects it. Publish through a private temp file in state/ and a validated rename, matching the existing check-publication pattern.
  • ⚠️ bin/fm-inbox-arm.sh:65 - The generated check embeds BOARD and ANSWERS inside single quotes without shell escaping. A board/home path containing a single quote or newline can register a syntactically broken or injectable check, making the relay fail silently; generate shell-escaped assignments instead.

🔧 Fix: Fix inbox relay safety and PR verification
2 warnings still open:

  • ⚠️ bin/fm-inbox-arm.sh:106 - The generated check calls lavish-axi poll even when the board is unserved. Current Lavish poll calls ensureServer() first, so an idle watcher can start a Lavish server itself; because the shim does not set LAVISH_AXI_HOST, that server may be localhost-bound and later fm-inbox-serve.sh can reuse it instead of serving on Tailscale. Gate polling on an already-running served session before invoking poll.
  • ⚠️ bin/fm-inbox-arm.sh:111 - The answer-count grep does not match Lavish's prompt rows, which are serialized as rows like decision,&#34;DECISION ...&#34;, not lines beginning with two spaces and a quote. With zero matches, grep -c prints 0 and exits 1, so the fallback appends another 0; real answer wakes can say 0\n0 captain answer(s) even after writing an answer file. Count prompt rows correctly or emit a fixed single-line wake message.

🔧 Fix: Gate inbox relay before polling
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • bin/fm-test-run.sh --list --changed --base 2b7cd66ad09722fec4cf11acdbee4fd8046f06e7 initially exposed the unmapped renderer; after the fix it succeeded and wrote changed-selection-after-fix.txt.
  • bin/fm-test-run.sh tests/fm-test-run.test.sh
  • bin/fm-test-run.sh tests/fm-inbox-view.test.sh tests/fm-inbox-arm.test.sh
  • Generated a fixture board with PATH=&#34;$FAKEBIN:$PATH&#34; FM_HOME=&#34;$EVIDENCE/fixture-home&#34; bin/fm-inbox-view.sh &#34;$EVIDENCE/inbox-board.html&#34; and compared state/ + data/ manifests before/after.
  • Served and armed the fixture board with LAVISH_AXI_PORT=4891 FM_HOME=&#34;$EVIDENCE/fixture-home&#34; bin/fm-inbox-serve.sh --no-generate --link-host 127.0.0.1 &#34;$EVIDENCE/inbox-board.html&#34;.
  • Captured rendered UI with playwright screenshot at desktop and 390px mobile widths.
  • Ran node &#34;$EVIDENCE/inbox-board-interaction.cjs&#34; to clear a quick-pick, submit a free-text-only DECISION, submit a DISCUSS question, verify immediate send calls, and assert no horizontal overflow.
  • Checked relay trust with fm_custom_check_registered &#34;$STATE&#34; inbox and verified state/inbox.check.sh mode 700.
  • Cleaned up the temporary Lavish server with LAVISH_AXI_PORT=4891 lavish-axi stop and confirmed the port no longer answered.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

ItsFlow and others added 30 commits July 22, 2026 18:30
* 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
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: make dispatch profiles quota aware

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

* no-mistakes(document): Document implicit quota-aware dispatch accurately
* 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
* 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): 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

* 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

* no-mistakes(document): Document independent PR compliance events
* 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: type canonical operational inputs

* no-mistakes(document): Correct canonical operational-input documentation ownership
* fix: avoid generic secondmate acknowledgements

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

* no-mistakes: apply CI fixes
* 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: 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
* 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): preserve operational follow-up semantics in Calm

* no-mistakes(document): Correct Calm operational-row visibility documentation
* 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

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

* no-mistakes(review): Synchronize Calm geometry checks with reload completion
* fix: escalate ask-user contract expansion

* no-mistakes(document): Point project management to authority owner
* 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(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 safely

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

* no-mistakes(document): docs: clarify Herdr restart projection contract
* docs: separate current guides from verification

* no-mistakes(review): Restore Herdr 0.7.5 restart-reclaim verification evidence
* 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
* 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
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.
…flow 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.
@ItsFlow
ItsFlow merged commit a23849a into main Jul 26, 2026
10 of 11 checks passed
ItsFlow added a commit that referenced this pull request Jul 28, 2026
* feat(bin): add captain inbox board (#1)

* 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>

* test(session-start): fix fork sync merge artifacts

* no-mistakes(review): Harden inbox relay and hooks

* no-mistakes(review): Escape inbox shipped metadata

* no-mistakes(document): refresh inbox-board documentation

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
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