Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions .claude/skills/pm-dispatch/references/lanes/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,43 @@
- 席位 `security#N`,首席 `security#1`;签名、座位贴、坐席与收班同他席。
- 无 `domain:security` 标签、无 `pm:queue`、无文件面:安全发现永不成卡,修复落在归属包。
- 坐席者 = 能读写本仓安全公告的账号(管理员或安全经理)的会话;读不了公告 ⛔ 不坐。
- 读公告只经中继 `advisory_read`;建草稿与写公告只经中继,⛔ 永不以自身账号写。
- 公告的读、建与写只照第 3 节。

## 接收与台账

- 接收 = 发现者的一次 `Notify: security · GHSA-ID` 门铃,草稿公告已由其经中继建好。
- 中继拒 ⇒ 机制在会话里交本席;本席空缺 ⇒ 交维护者(第 3 节);⛔ 不换账号重发。
- 台账住座位贴,一份一行:不透明编号 · 会话 · 状态;⛔ 无细节。
- 状态词表:`received` → `fixing` → `merged` → `awaiting publish` → `published`。
- 中继拒或本席空缺照第 3、9 节。
- 云席列不了本仓公告,中继也拒裸集合:编号只来自维护者或门铃。
- 读出的公告明文只印本席进程 stdout(`--reveal`),⛔ 不进文件、日志或后台任务输出。
- 台账住座位贴,一份一行:不透明编号 · 会话 · 状态;⛔ 无细节;修复的认领只记台账。
- 状态词表:`received` → `fixing` → `merged` → `closed`;挑中发布再 `awaiting publish` → `published`。

## 派修与复核

- 修复 = 本席向 `os-dev` 下无卡派发令,`xhigh`;机制只进派发令与公告,⛔ 不进 PR 与评论。
- L/XL、build 重、长跑或浏览器验证的派修走 `mode:cloud`,余 `mode:subagent`(`../execution-duties.md`)。
- 修复 PR 的契约复核归本席:记录停在规则层,机制只在本席本地的 `security-note.md`(第 6 节)。
- 要定契约的修复交维护者;发布公告恒归维护者,本席把状态推到 `awaiting publish` 即止。
- 要定契约的修复交维护者(呈前照下节)。

## 呈维护者

- 任何问题呈维护者前,先由隔离 agent 以 `CONTRACT_REVIEW_TIER` 复核其分析;⛔ 不写模型名。
- 复核者只拿分析与证据,⛔ 不拿本席结论;核四轴、选项无漏、推荐成立、证据为真。
- 问题随附复核判词与全部异议。
- 每次发版前呈发布与否清单:上次发版后 `closed` 的公告,高危、中危在前;先经上条复核。
- 发布归维护者:在公告界面重开并发布,中继推不动已关公告。

## 收口(逐对象)

- PR 由合并队列合入;本席确认 MERGED 后照 `../landing-operations.md` 收口其卡。
- 卡:`Fixes #N` 合入即关,本席补落地记录并摘 `pm:*`;`Part of #N` 卡末段落地时本席自关。
- 公告:修复合入即经中继 `advisory_state` 置 `closed` 并记座位贴,⛔ 不逐份请示。
- 云卡:工作落地即由本席归档;子代理自行结束并删自己的 worktree。

## 误公开的卡(第 7 节)

- 本席接管:挂 `security` 标签,⛔ 不追加细节;是否移入公告并关卡交维护者定,⛔ 不代关。
- 本席巡查自读 open 的 `security` 标签卡,标签即转交;分诊敲 `Notify: security · #N` 只为提速。
- 分诊只即推维护者并转交本席(`triage.md`);本席空缺时分诊照第 7 节处置。

## 收班
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/pm-dispatch/references/project-forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
- 移卡入队者→车道:p0/p1 `Blocked-by` 关闭 `Notify: unblocked`;记裁决席→被改席 `Notify: ruling`。
- 席→兄弟席或他车道:认领文件面相交或须先落 `Notify: serial`,先落者落地 `Notify: landed`。
- 上下线→兄弟席与分诊 `Notify: seat-online`、`Notify: seat-offline`;清单变→兄弟席 `Notify: split`。
- 发现者→`security#1`:中继草稿公告建成即 `Notify: security`,只带不透明编号。
- 持发现的席→`security#1`:草稿公告建成或误公开卡挂标即 `Notify: security`,只带编号。
- ⛔ 不敲:回执、重发、催(逾期归升级);收方订阅 PR 已载者;问维护者走决策卡。
- 安全发现只敲上一键,⛔ 不带机制、路径或对象名;余照 `security-findings.md`。
- 每席每轮 ≤5 条,p0 豁免,超额归下轮,计入写节奏;全拼 `record-recognisers.mjs --mailbox-keys`。
Expand Down
12 changes: 11 additions & 1 deletion scripts/pm/check-skill-line-ratchet.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1515,7 +1515,17 @@ export const CEILINGS = new Map([
// never-public rules stay in security-findings.md. A NEW file, so this is an
// added row and no other row moves; set at the landed line count (headroom 0,
// same convention).
['.claude/skills/pm-dispatch/references/lanes/security.md', 34],
// 34 → 53 (#29726): the maintainer's working rules for the seat — the
// isolated `CONTRACT_REVIEW_TIER` review before any question reaches the
// maintainer, cloud orders for heavy fixes, the per-object close-out, the
// relay `closed` move on merge and the pre-release publish-or-not list, the
// intake and read rules — plus the review's S1 (two restated lines become
// pointers) and S2 (the label is the hand-over). Ruling of record: that card's
// body (rulings 1–3, each verbatim there; the order to write them in is
// 「这些都应该写到你的skills」), and its amendment comment 6595626876 (「目前的修复都关闭」,
// 「你处理完的可以都先关闭」, 「哪些高危要发布应该在我版本发布前决策吧」). Measured
// delta +19 (34 → 53 lines); landed count, headroom 0.
['.claude/skills/pm-dispatch/references/lanes/security.md', 53],
// Project Director seat charter (maintainer ruling 2026-08-27, verbatim:
// 「项目总监 由人工定期指挥,负责处理 contract-review 类别的和需要决裁的。」; the file
// carries it as a rule line since the rules-only rewrite, not as this quotation)
Expand Down
27 changes: 14 additions & 13 deletions scripts/pm/fleet-roster.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ import { ghApi, PER_PAGE, readAllPages } from './gh-pages.mjs';
// seat since #27637); this file reads them, never re-spells them.
import * as mailboxOpen from './mailbox-open.mjs';
import { LEDGER_EXCLUSION, MAILBOX_BRANCH_PREFIX, MAILBOX_LABEL, mailboxOwner, SEAT_EPIC_KIND, seatKey } from './mailbox-open.mjs';
// The seat signature line has ONE spelling, check-half-states' `SEAT_SIGNATURE_LINE`; this file reads it, never
// re-spells it. The relay that imports `ROSTER_MARKER` from here (`fleet-write/dispatch.mjs`) already imports
// check-half-states itself, so this import loads nothing new there. The opener's lane-word lists cannot import it
// (the patrol loads the opener lazily, so that edge would be a cycle); the self-test holds the two equal.
import { SEAT_SIGNATURE_LINE } from './check-half-states.mjs';
// The seat record's heading and verdict have ONE reading, shared with H69 in
// check-half-states; this file asks it, never re-spells the heading.
import * as seatRecord from './seat-record.mjs';
Expand Down Expand Up @@ -292,19 +297,10 @@ export function epicSeatsHeld(lane, mailboxes) {
}
const isEpicLane = (lane) => lane.startsWith(`${SEAT_EPIC_KIND}:`);

/**
* The seat signature line `LANE#N · ACCOUNT · SESSION`, MIRRORED from `check-half-states.mjs`'s `SEAT_SIGNATURE_LINE`
* (#27113) — not imported, so the relay never loads the patrol; the self-test holds the two patterns byte-equal.
*/
const SIGNATURE_LINE = new RegExp(
'^((?:domain:[a-z][a-z0-9-]*|repo:[a-z][a-z0-9-]*|epic:#[1-9][0-9]*|triage|director|coordinator|security))#([1-9][0-9]*) · ([A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}) · (session_[A-Za-z0-9]{6,})$',
'u',
);

/** The seat a body is signed by — its FIRST non-empty line, `readSignatureLine`'s reading — as `LANE#N`, or null. Pure. */
export function signedSeatOf(body) {
const line = String(body ?? '').split(/\r?\n/).find((l) => l.trim() !== '');
const m = line === undefined ? null : SIGNATURE_LINE.exec(line.trim());
const m = line === undefined ? null : SEAT_SIGNATURE_LINE.exec(line.trim());
return m ? `${m[1]}#${m[2]}` : null;
}

Expand Down Expand Up @@ -657,7 +653,8 @@ const SELF_TEST_BATTERIES = Object.freeze({
'seat titles: lane, seat, repo, status, account, project': 13,
'wall notes, closing refs, verdicts and mailboxes': 13,
'the fixture snapshot: rows for the three repos': 25,
'one account on two seats of a lane: the second seat reads its own traces': 13,
// 13 → 14: the lane-grammar pin (the signature alternation equals the opener's lane-word lists, `coordinator` aside).
'one account on two seats of a lane: the second seat reads its own traces': 14,
'the rendered body: marker, columns, footer, determinism': 10,
'refusing to overwrite what is not the roster': 6,
'the CLI: fixture runs offline, usage errors are exit 2': 6,
Expand Down Expand Up @@ -824,8 +821,12 @@ async function selfTest() {
return [line.includes('| = seat 1 |'), line.includes(SUSPECTED_WALL)];
})(), [true, false]);
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]);
const { SEAT_SIGNATURE_LINE, readSignatureLine } = await import('./check-half-states.mjs');
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]);
const { readSignatureLine } = await import('./check-half-states.mjs');
t('the signature pattern is check-half-states\' SEAT_SIGNATURE_LINE, imported — no second spelling of the lane alternation in this file', [/^import \{ SEAT_SIGNATURE_LINE \} from '\.\/check-half-states\.mjs';$/mu.test(readFileSync(SELF_PATH, 'utf8')), readFileSync(SELF_PATH, 'utf8').includes(['triage', 'director', 'coordinator'].join('|'))], [true, false]);
// The lane grammar's two remaining owners, held equal here (the only file that imports both): the signature's
// alternation and the opener's lane-word lists. `coordinator` is the one lane that signs and has no mailbox.
const signatureLanes = (/^\^\(\(\?:([^()]*)\)\)#/u.exec(SEAT_SIGNATURE_LINE.source)?.[1] ?? '').split('|');
t('⭐ the lane grammar is ONE set: the signature\'s kinds are the opener\'s kinds plus `epic`, its bare words the opener\'s bare lanes plus `coordinator` — a lane added to one side alone reds here', [signatureLanes.filter((a) => a.includes(':')).map((a) => a.slice(0, a.indexOf(':'))).sort(), signatureLanes.filter((a) => /^[a-z]+$/u.test(a)).sort(), signatureLanes.length], [[...mailboxOpen.SEAT_LANE_KINDS, SEAT_EPIC_KIND].sort(), [...mailboxOpen.SEAT_BARE_LANES, 'coordinator'].sort(), mailboxOpen.SEAT_LANE_KINDS.length + 1 + mailboxOpen.SEAT_BARE_LANES.length + 1]);
const corpus = ['', sig('domain:server#2'), `\n\n ${sig('domain:engine-core#3')}\r\nbody`, `body\n${sig('triage#1')}`, sig('security#1'), 'epic:#27293#1 · sunhaolin · session_017xRBcVBKAFk1ntXSjvPt7U', 'domain:server#0 · os-pm · session_01SharedSeatSess', 'domain:server#2 · os-pm · cse_01SharedSeatSess'];
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; }));
}
Expand Down
3 changes: 2 additions & 1 deletion scripts/pm/mailbox-open.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ const SESSION_SHAPE = /^(?:session|cse)_[A-Za-z0-9]+$/;
// three, so a login is never read as a seat. The lists below are the ONE spelling of those lane words: the shape is
// built from them, and so is the reserved set a `domain:` lane may not be named after (`seatKey`). The same three
// words close the seat signature's lane alternation (`SIGNATURE_LANE_SOURCE`, check-half-states.mjs), which this
// list does not import; a bare lane admitted there is admitted here in the same PR.
// list cannot import (the patrol loads this file lazily, so that edge would be a cycle); fleet-roster's self-test,
// which imports both, holds the two equal, `coordinator` (it signs and has no mailbox) aside.
/** The lane kinds that carry a name after the colon: `domain:<x>`, `repo:<x>`. `domain` is the default kind the slug drops. */
export const SEAT_LANE_KINDS = Object.freeze(['domain', 'repo']);
/** The bare lanes, a closed set: `triage`, `director`, `security` (the seat that owns security findings, `lanes/security.md`). */
Expand Down
Loading
Loading