Skip to content

Commit 135054e

Browse files
fix(pm): fleet-roster reads a shared-account seat's own activity (#28941)
<!-- claude-projects-attribution: {"github_login":"marchtian"} --> _Requested by **田騏** · [project thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSi9paTx1Lu8ENYwwssthfK3h)_ Fixes #28906 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:车队总表的「最近活动」改读席位自己的痕迹(座位贴、在飞卡、本席信箱 PR、近 6 小时署名为本席的评论),同账号多席里的高位席不再被误标「疑似撞墙」。 - **为什么改**:一个账号坐同车道多席是规则允许的并发,但高位席没有自己的在飞卡,总表只剩座位贴时间可看,把 server 车道 2、3、5 席误判为撞墙,真撞墙被淹没。 - **风险与回滚**:只改一个只读视图脚本 `scripts/pm/fleet-roster.mjs`,不发布;每次生成多一个分页读(今日约 4 页);回滚即 revert 本 PR。 - **先看**:`scripts/pm/fleet-roster.mjs` 中 `buildRows` 的 `times` 一行与 `signedActivity`;其次 `SIGNATURE_LINE` 镜像及其等值自测。 - **两分钟验证**:`node scripts/pm/fleet-roster.mjs --self-test`,预期 `✓ fleet-roster self-test: 104 cases in 9 batteries pass.` - **需要你做的**:无,席位落地。 ## What changes `scripts/pm/fleet-roster.mjs` only (+109 / -11). - **Last activity is the newest of the seat's own traces**: the seat post's `updated_at`, its in-flight cards, its mailbox pull's `updated_at` (the mailbox column's pull, already in the `pulls?state=open` read, so no new call), and every comment of the last `STALE_HOURS` whose first non-empty line is a seat signature `LANE#N · ACCOUNT · SESSION` naming THIS row's lane and seat number. The attribution is by seat, never by account, so two seats on one account stay apart. - **The in-flight column is unchanged**: a seat whose account holds a lower seat of its lane still reads `= seat N`. `STALE_HOURS` is unchanged (6). - **One new read per repo**: `GET /repos/R/issues/comments?since=(now − 6 h)`, paged through `gh-pages.mjs`. The footer of the generated body names it with the other reads. - **The signature pattern is mirrored, not imported**: the relay imports `ROSTER_MARKER` from this file, so it must not load `check-half-states.mjs` (the same reason `isEpicParent` mirrors H45). The self-test holds `SIGNATURE_LINE` byte-equal to `SEAT_SIGNATURE_LINE` (source and flags) and checks `signedSeatOf` against `readSignatureLine` over a corpus. ## Tests New self-test battery `one account on two seats of a lane: the second seat reads its own traces` (13 cases, battery floor 8 → 9). The cases: - With no trace of its own, seat 2 reads `= seat 1` and its post alone, and is flagged. - A fresh mailbox pull lifts the flag. A fresh comment signed `domain:server#2` lifts it too. - When the mailbox and the signed comment are both older than `STALE_HOURS`, the seat is flagged. - Seat 1's fresh signed comment and fresh mailbox (same account) leave seat 2 flagged. - These sign nothing for the seat: another lane's seat 2, a signature that is not the first non-empty line, and a misspelled signature. - The newest of the four traces wins. - The rendered row keeps `= seat 1` without the flag. - The read window is `STALE_HOURS`, and the footer names the read. <details><summary>Self-test and suites run on 01dbcd7</summary> ``` node scripts/pm/fleet-roster.mjs --self-test :: exit 0 ✓ fleet-roster self-test: 104 cases in 9 batteries pass. node scripts/pm/fleet-write/dispatch.mjs --self-test :: exit 0 (imports ROSTER_MARKER from this file) ✓ fleet-write/dispatch self-test: 265 cases pass across 24 batteries pnpm check:pm-record-recognisers :: exit 0 (runs fleet-roster --self-test) ``` </details> ## Acceptance notes - **The mailbox share counts inbound doorbells.** A mailbox pull's `updated_at` moves on any comment, including another seat's `To:` line, so a walled seat that keeps receiving doorbells reads fresh. The card's Wanted names the mailbox, so it is in. A signed comment is the stricter trace, because it is attributed to its writer. - **The comment read stops at `STALE_HOURS`.** The flag is exact. For a flagged row, a signed comment older than 6 h is not read, so the age cell can read older than that comment. The read window keeps the cost bounded: 388 comments in 6 h today against a 5000-row `MAX_PAGES` guard. A 24 h window measured 1720 comments and 18 pages. - Only comments on repos in `--repos` are read. A `repo:cloud` seat's signed comments land on the cloud repo, so those rows still read their post alone. - In this session hotcrm answers 403, so the live runs used `--repos objectstack-ai/objectstack,objectstack-ai/objectui`. The default three-repo run fails at hotcrm here before and after this change. ## Verification Live, read-only, before at `origin/main` 9a9823d and after on this branch: | seat | post | before (last activity · flag) | after (last activity · flag) | after: mailbox pull · newest signed comment | |:--|:--|:--|:--|:--| | `domain:server#2` | #20966 | 2026-10-31T02:56Z · 疑似撞墙 | 2026-11-02T04:27Z · none | #28284 04:03Z · 04:27Z | | `domain:server#3` | #26761 | 2026-11-01T06:46Z · 疑似撞墙 | 2026-11-02T04:07Z · none | #28585 03:49Z · 04:07Z | | `domain:server#5` | #28677 | 2026-11-01T13:13Z · 疑似撞墙 | 2026-11-02T04:16Z · none | #28678 03:50Z · 04:16Z | The run flagged 9 rows before and 6 after. The other six flags are unchanged: `repo:cloud` 1 and 2, `repo:docs.objectos.ai` 1, `domain:spec` 3 and 5, and `repo:www.objectos.ai` 1. Each server seat's mailbox and its signed comment are both fresh, so either trace alone lifts its flag. Seat 1 keeps its 9 in-flight cards, and seats 2, 3 and 5 still read `= seat 1`. **API cost**: the run made 23 `gh api` calls after the change and 18 before. The 5 new calls are the comment reads: 4 pages on objectstack and 1 on objectui. <details><summary>Gates on 01dbcd7 (dispatch-gates --commands, 31 derived)</summary> All 31 derived commands exited 0. The `--ran` reconciliation reads: `✓ dispatch-gates --ran: 31 derived famil(ies) accounted for — 31 run, 0 NOT-MEASURED (a DERIVED zero — all 31 recorded an exit code and none of them is 3).` The 31 are `check-closing-keyword-parity` (+ self-test), `check-comment-mask-corpus`, `check-declaration-mirrors` (+ self-test), `check-self-test-wired` (+ self-test), `check-self-test-workflow-commands` (+ self-test), `check-whole-set-label-write` (+ self-test), `plan-test-core-shards --self-test`, `bare-root-worklist --self-test`, and `pnpm check:` agent-test-spelling, bash32-floor, ci-filter-parity, cli-command-ids, cross-package-test-inputs, driver-memory-census, entry-guard, gitlink-declared, nul-bytes, parse-guard, pm-dispatch-gates (fast tier, 1937 cases), pm-record-recognisers, pnpm-filter-targets, pnpm-flag-spellings, ratchet-remedy-authority, refd-timer-probe, scripts-symbol-anchors and watch-hint-literal. Outside the derived set, I ran the rosters under `scripts/`: - Exit 0: `check-published-list-mirrors`, `check:engine-double-contract`, `check:i18n-stale-fill` and `check:pm-label-desc-cap`. - Exit 3, NOT MEASURED: `check:dts-closure` and `check:published-readme-exports`. Their prerequisite is built packages, and this diff touches none. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 62b3ec3 commit 135054e

1 file changed

Lines changed: 109 additions & 11 deletions

File tree

‎scripts/pm/fleet-roster.mjs‎

Lines changed: 109 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,18 @@
6363
* (`LEDGER_EXCLUSION`).
6464
* wall note a line-initial `Wall:` (or `撞墙:`) line in the post
6565
* body, the seat's own record of a usage wall.
66-
* last activity the newest `updated_at` of the post and its in-flight
67-
* cards. A seated seat older than `STALE_HOURS` with no
68-
* wall note is flagged 「疑似撞墙」.
66+
* last activity the newest of the seat's own traces: the post's
67+
* `updated_at`, its in-flight cards', its mailbox pull's
68+
* (the column below), and every comment of the last
69+
* `STALE_HOURS` signed `LANE#N · ACCOUNT · SESSION` on its
70+
* first line with THIS row's lane and seat — by seat, ⛔
71+
* never by account, so a seat whose account also holds a
72+
* lower seat of its lane (in flight `= seat N`) still reads
73+
* its own work. A seated seat older than `STALE_HOURS` with
74+
* no wall note is flagged 「疑似撞墙」. The comment read
75+
* stops at `STALE_HOURS`, so a flagged row's age can read
76+
* older than a signed comment past that window; the flag
77+
* is exact.
6978
* mailbox open `pm:mailbox` pulls of the SEAT, whichever account
7079
* holds it: the seat on the body's first line
7180
* `Mailbox-of: <seat> · <login>`, or the head
@@ -283,6 +292,38 @@ export function epicSeatsHeld(lane, mailboxes) {
283292
}
284293
const isEpicLane = (lane) => lane.startsWith(`${SEAT_EPIC_KIND}:`);
285294

295+
/**
296+
* The seat signature line `LANE#N · ACCOUNT · SESSION`, MIRRORED from `check-half-states.mjs`'s `SEAT_SIGNATURE_LINE`
297+
* (#27113) — not imported, so the relay never loads the patrol; the self-test holds the two patterns byte-equal.
298+
*/
299+
const SIGNATURE_LINE = new RegExp(
300+
'^((?:domain:[a-z][a-z0-9-]*|repo:[a-z][a-z0-9-]*|epic:#[1-9][0-9]*|triage|director|coordinator))#([1-9][0-9]*) · ([A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}) · (session_[A-Za-z0-9]{6,})$',
301+
'u',
302+
);
303+
304+
/** The seat a body is signed by — its FIRST non-empty line, `readSignatureLine`'s reading — as `LANE#N`, or null. Pure. */
305+
export function signedSeatOf(body) {
306+
const line = String(body ?? '').split(/\r?\n/).find((l) => l.trim() !== '');
307+
const m = line === undefined ? null : SIGNATURE_LINE.exec(line.trim());
308+
return m ? `${m[1]}#${m[2]}` : null;
309+
}
310+
311+
/** The newest signed-comment time per seat, `LANE#N` → ISO, over every repo's `recentComments`. Pure. */
312+
export function signedActivity(snapshot) {
313+
const out = new Map();
314+
for (const repo of Object.keys(snapshot?.repos ?? {})) {
315+
for (const c of snapshot.repos[repo]?.recentComments ?? []) {
316+
const seat = signedSeatOf(c?.body);
317+
const at = c?.updated_at ?? c?.created_at ?? null;
318+
if (seat !== null && at && !(out.get(seat) >= at)) out.set(seat, at);
319+
}
320+
}
321+
return out;
322+
}
323+
324+
/** Where the signed-comment read starts: `STALE_HOURS` before `now` — no older comment can lift a flag. Pure. */
325+
export const activitySince = (now) => new Date(Date.parse(now) - STALE_HOURS * 3_600_000).toISOString().replace(/\.\d{3}Z$/, 'Z');
326+
286327
/** A seat row's mailbox slug — its `LANE#N` as the opener spells it in a ref — or null. Pure. */
287328
const seatSlugOf = (row) => seatKey(`${row.lane}#${row.seat}`)?.slug ?? null;
288329

@@ -319,7 +360,7 @@ const keyOf = (r) => `${r.repo}#${r.number}`;
319360
const byRef = (a, b) => (a.repo === b.repo ? a.number - b.number : a.repo < b.repo ? -1 : 1);
320361

321362
/**
322-
* snapshot = { repos: { 'owner/name': { seats, dispatched, rulings, pulls, comments: { '<n>': [...] } } } }
363+
* snapshot = { repos: { 'owner/name': { seats, dispatched, rulings, pulls, comments: { '<n>': [...] }, recentComments } } }
323364
* → the roster rows, deterministically ordered (repo, lane, seat, post). Pure.
324365
*/
325366
export function buildRows(snapshot, { now }) {
@@ -330,6 +371,8 @@ export function buildRows(snapshot, { now }) {
330371
const rulings = [];
331372
const pulls = [];
332373
const mailboxes = readMailboxes(snapshot);
374+
const signed = signedActivity(snapshot);
375+
const pullUpdatedAt = new Map(); // pull key → its updated_at, for the mailbox's share of last activity
333376
for (const repo of repos) {
334377
const s = snapshot.repos[repo] ?? {};
335378
for (const post of s.seats ?? []) {
@@ -350,7 +393,11 @@ export function buildRows(snapshot, { now }) {
350393
const asCard = (i) => ({ repo, number: i.number, labels: labelNames(i), assignees: assigneeLogins(i).map(lc), updatedAt: i.updated_at ?? null });
351394
for (const i of s.dispatched ?? []) if (!i.pull_request && i.state !== 'closed') cards.push(asCard(i));
352395
for (const i of s.rulings ?? []) if (!i.pull_request && i.state !== 'closed') rulings.push(asCard(i));
353-
for (const p of s.pulls ?? []) if (p.state !== 'closed') pulls.push({ repo, pr: p, comments: s.comments?.[String(p.number)] ?? [] });
396+
for (const p of s.pulls ?? []) {
397+
if (p.state === 'closed') continue;
398+
pulls.push({ repo, pr: p, comments: s.comments?.[String(p.number)] ?? [] });
399+
pullUpdatedAt.set(keyOf(ref(repo, p.number)), p.updated_at ?? null);
400+
}
354401
}
355402
rows.sort((a, b) => (a.repo !== b.repo ? (a.repo < b.repo ? -1 : 1) : a.lane !== b.lane ? (a.lane < b.lane ? -1 : 1) : a.seat !== b.seat ? a.seat - b.seat : a.post - b.post));
356403

@@ -368,7 +415,10 @@ export function buildRows(snapshot, { now }) {
368415
const sharesWith = first !== row ? first.seat : null;
369416
const inFlight = row.account && sharesWith === null ? cards.filter((c) => cardLaneMatch(c, row) && c.assignees.includes(acct)) : [];
370417
const awaitingLanding = inFlight.flatMap((c) => landing.get(keyOf(c)) ?? []);
371-
const times = [row.updatedAt, ...inFlight.map((c) => c.updatedAt)].filter(Boolean).sort();
418+
// Per seat, never per account: one account holds several seats, and each seat opens its own.
419+
const mailbox = mailboxes.filter((m) => m.slug !== null && m.slug === seatSlugOf(row)).map((m) => m.ref);
420+
// The seat's own traces: a seat sharing its lane's account with a lower seat has no in-flight cards of its own.
421+
const times = [row.updatedAt, ...inFlight.map((c) => c.updatedAt), ...mailbox.map((m) => pullUpdatedAt.get(keyOf(m))), signed.get(`${row.lane}#${row.seat}`)].filter(Boolean).sort();
372422
const lastActivity = times.at(-1) ?? null;
373423
const ageHours = lastActivity && Number.isFinite(nowMs) ? (nowMs - Date.parse(lastActivity)) / 3_600_000 : null;
374424
return {
@@ -379,8 +429,7 @@ export function buildRows(snapshot, { now }) {
379429
awaitingLanding: awaitingLanding.sort(byRef),
380430
lastActivity,
381431
suspectedWall: row.status === 'seated' && !isEpicLane(row.lane) && row.wallNote === null && ageHours !== null && ageHours > STALE_HOURS,
382-
// Per seat, never per account: one account holds several seats, and each seat opens its own.
383-
mailbox: mailboxes.filter((m) => m.slug !== null && m.slug === seatSlugOf(row)).map((m) => m.ref),
432+
mailbox,
384433
};
385434
});
386435

@@ -414,6 +463,7 @@ export function readsFor(repos) {
414463
`GET /repos/${r}/issues?state=open&labels=pm:dispatched`,
415464
`GET /repos/${r}/issues?state=open&labels=needs-user-decision`,
416465
`GET /repos/${r}/pulls?state=open`,
466+
`GET /repos/${r}/issues/comments?since=(now − ${STALE_HOURS} h), each read for its first-line seat signature`,
417467
`GET /repos/${r}/issues/{n}/comments for each claude/ pull closing an in-flight card (${LEDGER_EXCLUSION} excluded)`,
418468
]);
419469
}
@@ -483,7 +533,7 @@ function ghJson(path, { paginate = false } = {}) {
483533
}
484534

485535
/** Read every repo into the snapshot shape `buildRows` takes; throws on the first failed read. */
486-
function readLive(repos) {
536+
function readLive(repos, { since }) {
487537
const need = (path, paginate = true) => {
488538
const r = ghJson(path, { paginate });
489539
if (!r.ok) throw new Error(`GET ${path} — ${r.error}`);
@@ -498,6 +548,7 @@ function readLive(repos) {
498548
rulings: need(`/repos/${repo}/issues?state=open&labels=needs-user-decision&per_page=100`),
499549
pulls: need(`/repos/${repo}/pulls?state=open&per_page=100`),
500550
comments: {},
551+
recentComments: need(`/repos/${repo}/issues/comments?since=${encodeURIComponent(since)}&per_page=100`),
501552
};
502553
snapshot.repos[repo] = s;
503554
}
@@ -552,7 +603,7 @@ export async function main(argv, { env = process.env } = {}) {
552603
if (opts.fixture !== null) {
553604
snapshot = JSON.parse(readFileSync(opts.fixture, 'utf8'));
554605
repos = Object.keys(snapshot.repos ?? {}).sort();
555-
} else snapshot = readLive(repos);
606+
} else snapshot = readLive(repos, { since: activitySince(opts.now ?? new Date().toISOString()) });
556607
} catch (e) {
557608
console.error(`✗ fleet-roster: ${opts.fixture !== null ? `could not read the fixture ${opts.fixture}` : 'a read failed'} — ${e.message}. Nothing was written.`);
558609
return EXIT_PREREQUISITE;
@@ -606,12 +657,13 @@ const SELF_TEST_BATTERIES = Object.freeze({
606657
'seat titles: lane, seat, repo, status, account, project': 13,
607658
'wall notes, closing refs, verdicts and mailboxes': 13,
608659
'the fixture snapshot: rows for the three repos': 25,
660+
'one account on two seats of a lane: the second seat reads its own traces': 13,
609661
'the rendered body: marker, columns, footer, determinism': 10,
610662
'refusing to overwrite what is not the roster': 6,
611663
'the CLI: fixture runs offline, usage errors are exit 2': 6,
612664
'paging: a three-page read through the loop equals the slurped array': 4,
613665
});
614-
const SELF_TEST_BATTERY_FLOOR = 8;
666+
const SELF_TEST_BATTERY_FLOOR = 9;
615667

616668
async function selfTest() {
617669
let failed = 0;
@@ -732,6 +784,52 @@ async function selfTest() {
732784
t('last activity is the newest of post and in-flight cards', row(6023).lastActivity, '2026-10-23T13:28:00Z');
733785
t('one account on two seats of a lane: cards on the lowest seat, the other reads = seat 1', [row(5560, 'objectstack-ai/objectui').inFlight.length, row(10243, 'objectstack-ai/objectui').sharesWith, row(10243, 'objectstack-ai/objectui').inFlight, row(10243, 'objectstack-ai/objectui').suspectedWall], [1, 1, [], true]);
734786

787+
battery('one account on two seats of a lane: the second seat reads its own traces');
788+
{
789+
const NOW = '2026-11-02T04:00:00Z';
790+
const R = 'objectstack-ai/objectstack';
791+
const fresh = '2026-11-02T03:00:00Z'; // 1 h before NOW
792+
const stale = '2026-11-01T20:00:00Z'; // 8 h before NOW, past STALE_HOURS
793+
const sig = (laneSeat, account = 'os-pm') => `${laneSeat} · ${account} · session_01SharedSeatSess`;
794+
const snap = ({ mailboxAt = null, comments = [], seat1MailboxAt = null } = {}) => ({
795+
repos: {
796+
[R]: {
797+
seats: [
798+
{ number: 100, title: '[PM seat] domain:server — 🟢 os-pm', body: '', labels: ['pm:seat'], assignees: ['os-pm'], updated_at: '2026-10-30T00:00:00Z' },
799+
{ number: 200, title: '[PM seat] domain:server · seat 2 — 🟢 os-pm', body: '', labels: ['pm:seat'], assignees: ['os-pm'], updated_at: '2026-10-31T00:00:00Z' },
800+
],
801+
dispatched: [{ number: 300, labels: ['pm:dispatched', 'domain:server'], assignees: ['os-pm'], updated_at: fresh }],
802+
rulings: [],
803+
pulls: [
804+
...(seat1MailboxAt ? [{ number: 401, state: 'open', head: { ref: 'mailbox/os-pm/server-1' }, body: 'Mailbox-of: domain:server#1 · os-pm', labels: ['pm:mailbox'], updated_at: seat1MailboxAt }] : []),
805+
...(mailboxAt ? [{ number: 402, state: 'open', head: { ref: 'mailbox/os-pm/server-2' }, body: 'Mailbox-of: domain:server#2 · os-pm', labels: ['pm:mailbox'], updated_at: mailboxAt }] : []),
806+
],
807+
comments: {},
808+
recentComments: comments,
809+
},
810+
},
811+
});
812+
const seat2 = (s) => (({ sharesWith, inFlight, mailbox, lastActivity, suspectedWall }) => ({ sharesWith, inFlight, mailbox, lastActivity, suspectedWall }))(buildRows(s, { now: NOW }).find((r) => r.post === 200));
813+
t('no trace of its own: seat 2 reads = seat 1, its post alone, and is flagged', seat2(snap()), { sharesWith: 1, inFlight: [], mailbox: [], lastActivity: '2026-10-31T00:00:00Z', suspectedWall: true });
814+
t('⭐ a fresh mailbox pull lifts the flag, and the in-flight cell still reads = seat 1', seat2(snap({ mailboxAt: fresh })), { sharesWith: 1, inFlight: [], mailbox: [{ repo: R, number: 402 }], lastActivity: fresh, suspectedWall: false });
815+
t('⭐ a fresh comment signed domain:server#2 lifts the flag', seat2(snap({ comments: [{ body: `${sig('domain:server#2')}\n\n## 席位核对 — ACCEPT`, updated_at: fresh }] })), { sharesWith: 1, inFlight: [], mailbox: [], lastActivity: fresh, suspectedWall: false });
816+
t('⛔ mailbox and signed comment both older than STALE_HOURS: flagged, last activity the newer of them', seat2(snap({ mailboxAt: stale, comments: [{ body: sig('domain:server#2'), updated_at: '2026-11-01T19:00:00Z' }] })), { sharesWith: 1, inFlight: [], mailbox: [{ repo: R, number: 402 }], lastActivity: stale, suspectedWall: true });
817+
t('⛔ by seat, never by account: seat 1\'s fresh signed comment and fresh mailbox, same account, leave seat 2 flagged', seat2(snap({ seat1MailboxAt: fresh, comments: [{ body: sig('domain:server#1'), updated_at: fresh }] })).suspectedWall, true);
818+
t('⛔ another lane\'s seat 2, or a seat-2 signature on a repo: lane, is not this seat', seat2(snap({ comments: [{ body: sig('domain:ui#2'), updated_at: fresh }, { body: sig('repo:server#2'), updated_at: fresh }] })).suspectedWall, true);
819+
t('⛔ a signature that is not the FIRST non-empty line, or is spelled wrong, signs nothing', seat2(snap({ comments: [{ body: `Claim: x\n${sig('domain:server#2')}`, updated_at: fresh }, { body: 'domain:server#2 - os-pm - session_01SharedSeatSess', updated_at: fresh }, { body: 'domain:server#2 · os-pm · session_01x', updated_at: fresh }] })).suspectedWall, true);
820+
t('the newest of the four traces wins, and the comment\'s updated_at is its time', seat2(snap({ mailboxAt: '2026-11-02T01:00:00Z', comments: [{ body: sig('domain:server#2'), created_at: '2026-11-02T00:00:00Z', updated_at: '2026-11-02T03:30:00Z' }, { body: sig('domain:server#2'), updated_at: '2026-11-02T02:00:00Z' }] })).lastActivity, '2026-11-02T03:30:00Z');
821+
t('seat 1 keeps its cards and reads its own traces too', (({ inFlight, lastActivity, suspectedWall }) => ({ inFlight, lastActivity, suspectedWall }))(buildRows(snap({ comments: [{ body: sig('domain:server#1'), updated_at: '2026-11-02T03:45:00Z' }] }), { now: NOW }).find((r) => r.post === 100)), { inFlight: [{ repo: R, number: 300 }], lastActivity: '2026-11-02T03:45:00Z', suspectedWall: false });
822+
t('the rendered row keeps = seat 1 and drops the flag once a trace is fresh', (() => {
823+
const line = renderBody(buildRows(snap({ mailboxAt: fresh }), { now: NOW }), { now: NOW, repos: [R] }).split('\n').find((l) => l.includes('/issues/200)'));
824+
return [line.includes('| = seat 1 |'), line.includes(SUSPECTED_WALL)];
825+
})(), [true, false]);
826+
t('the comment read starts STALE_HOURS before now, and the footer names it', [activitySince(NOW), readsFor([R]).some((q) => q.startsWith(`GET /repos/${R}/issues/comments?since=`))], ['2026-11-01T22:00:00Z', true]);
827+
const { SEAT_SIGNATURE_LINE, readSignatureLine } = await import('./check-half-states.mjs');
828+
t('the signature pattern is check-half-states\' SEAT_SIGNATURE_LINE, byte for byte — mirrored, not imported', [SIGNATURE_LINE.source === SEAT_SIGNATURE_LINE.source, SIGNATURE_LINE.flags === SEAT_SIGNATURE_LINE.flags], [true, true]);
829+
const corpus = ['', sig('domain:server#2'), `\n\n ${sig('domain:engine-core#3')}\r\nbody`, `body\n${sig('triage#1')}`, 'epic:#27293#1 · sunhaolin · session_017xRBcVBKAFk1ntXSjvPt7U', 'domain:server#0 · os-pm · session_01SharedSeatSess', 'domain:server#2 · os-pm · cse_01SharedSeatSess'];
830+
t('signedSeatOf reads every body as readSignatureLine does', corpus.map(signedSeatOf), corpus.map((b) => { const r = readSignatureLine(b); return r ? `${r.lane}#${r.seat}` : null; }));
831+
}
832+
735833
battery('the rendered body: marker, columns, footer, determinism');
736834
const body = renderBody(rows, { now: fixture.now, repos: Object.keys(fixture.repos).sort(), session: 'cse_01ABC' });
737835
t('the body opens with the marker', body.split('\n')[0], ROSTER_MARKER);

0 commit comments

Comments
 (0)