diff --git a/.claude/agents/os-dev.md b/.claude/agents/os-dev.md index 1b0125d0707..2e6b40bbaae 100644 --- a/.claude/agents/os-dev.md +++ b/.claude/agents/os-dev.md @@ -35,7 +35,7 @@ model: opus - 同批 agents 共用一个 scratchpad 目录,自然命名的文件会被彼此静默覆盖。 2. **卡 assignee 归 PM。** 派发原子对已把它设好;共享身份下该字段答不了是谁。 - 你的身份位是认领评论里的分支;仓 CLAUDE.md 的 claim-first 已由 PM 的认领满足。 - - 恒不写卡的 assignee(空也开工,报 `summary`);PR ⛔ 不带 `assignees`(中继虽收),席位交棒才派。 + - 恒不写卡的 assignee(空也开工,报 `summary`);PR 派认领 `Account:`(`pr_create` 带),交棒归席位。 - 发现与他人在途工作重复,停下报 `blocked`。 - PR 上不是你设置的状态属于另一个 actor:⛔ 永不去纠正;疑问进报告,挡住报 blocked。 - 回退他人的操作(尤其 ready 翻转)永不轮到你;把意外写进 `summary`。 @@ -60,7 +60,7 @@ model: opus - 写工具自读所需变量;⛔ 永不打印环境变量的值,转录是 AGENTS.md 禁落令牌的日志行。 - 拼法不限于 `env`、`printenv`、`set`、`export -p`、`echo $VAR`、`cat /proc/*/environ`。 - 判存在只用不印值形式 `[ -n "${X+x}" ] && echo set`;已印出 ⇒ `deviations` 只点名变量名。 - - 写预算三笔:`git push`、`pr_create`(draft,含 labels、⛔ 无 assignees)、`os-dev-report` 评论。 + - 写预算三笔:`git push`、`pr_create`(draft,含 labels 与 assignees)、`os-dev-report` 评论。 - 卡与线程只走 payload 档(公开仓单卡网页内嵌 JSON,拼写住 platform-readings)或单卡 REST 读。 - 三类发现进报告交席位代立,不 `POST /issues`、⛔ 不静默弃报;PM 去重读数当既有事实用。 - PR 正文 dev 只写一次,在开 PR 那一笔,⛔ 不 `PATCH`;事后要改的报告点名改法,席位代写。 diff --git a/.claude/skills/pm-dispatch/SKILL.md b/.claude/skills/pm-dispatch/SKILL.md index bb2a7ead265..a618044e47d 100644 --- a/.claude/skills/pm-dispatch/SKILL.md +++ b/.claude/skills/pm-dispatch/SKILL.md @@ -195,8 +195,8 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报 - 批准判定单源 = 队列守卫常量 `GOVERNED_APPROVERS`:授权账户 APPROVED 即算,⛔ 不卡 `commit_id`。 - 批准后再推亦不过期;席位落地 = 过落地前检、清标、ready、auto-merge,踢出/变基同法。 - ③ 请审 = 席位交棒:ACCEPT 与现 head 同形 PASS 俱在案,同轮中继 `pr_request_reviewers` 请审批人。 -- 作者是审批人 ⇒ `assign` 它、请其余(请它必 422);⛔ 永不派/请席位自身账号;Tier S 不交人。 -- `pr_create` 不请不派;`pr_ready` 只补交棒漏;非例外 head 移 ⇒ 撤挂回等;脱中继 PR 自读请/派。 +- 作者是审批人 ⇒ `assign` 它、请其余(请它必 422);⛔ 永不请席位自身账号;Tier S 不交人。 +- `pr_create` 派不请;`pr_ready` 只补交棒漏;非例外 head 移 ⇒ 撤挂回等;脱中继 PR 自读请/派。 - 速读「需要你做的」求批点名 PASS 评论 id+`Head-sha`;无 PASS ⇒ 写 contract review owed 不求批。 - ④ 轮报单列 awaiting a human merge 并现读请审人;⛔ 未请审不在 chat 报「等你批」,先补请。 - skills 车道自有 PR:纯代码面如 `scripts/pm/` 由本席按达档自审(清单不减)后落地。 diff --git a/.claude/skills/pm-dispatch/references/dispatch-runbook.md b/.claude/skills/pm-dispatch/references/dispatch-runbook.md index 32b29af0633..8959be61091 100644 --- a/.claude/skills/pm-dispatch/references/dispatch-runbook.md +++ b/.claude/skills/pm-dispatch/references/dispatch-runbook.md @@ -137,7 +137,7 @@ - 席位留认领文本/派发令/契约复核/ACCEPT/落地;线程实施、开 draft PR、驱动 CI 与可合并。 - 线程 ⛔ 不做 harness 开 PR 后指派/请审:「审核通过才会推送给我审核」,请审是席位交棒。 - 派发令由 coordinator 写进线程开场消息,`Writes:` 行逐字在内,⛔ 不落共享目录;卡上留指针。 -- coordinator 开线程即读线程分支交席位(读不到则线程自报);⛔ 线程不认领、不写 assignee。 +- coordinator 开线程即读线程分支交席位(读不到则线程自报);⛔ 线程不认领、不写卡 assignee。 - 席位写 assignee+`pm:dispatched`+带该 `Branch:` 的认领;线程核对最新认领指向本分支方开工。 - 分支约定仍是 `claude/issue--`;线程只能推指定分支是实测平台例外,认领如实写。 - worktree 照建;仓 `.claude/agents` 类型未加载(实测),线程照读 `os-dev.md`。 diff --git a/.claude/skills/pm-dispatch/references/execution-duties.md b/.claude/skills/pm-dispatch/references/execution-duties.md index 2fb53328c0b..dcf2173596f 100644 --- a/.claude/skills/pm-dispatch/references/execution-duties.md +++ b/.claude/skills/pm-dispatch/references/execution-duties.md @@ -66,7 +66,7 @@ ## 认领(先认领后动工) - 归属 = session ID,⛔ 不认作者字段;`Account:` 派生自 assignee,不等即半态、持卡席下笔补正。 -- 卡 assignee 归 PM,dev 恒不写;PR ⛔ 不派席位账号;在飞 PR 读 `head:claude/` 开 PR 正文点名的卡。 +- 卡 assignee 归 PM,dev 恒不写;PR 派认领账号,交棒同笔改;在飞 PR = `head:claude/` 正文点名本卡。 - 部分落地(PR 带 `Refs #N (item k)`,⛔ 不 `Fixes`)即释放:合入同笔回 `pm:queue` + 清 assignee。 - `Release:` 行点名已落与余项;余项整落 `ui/**` 外他席树 ⇒ 点名其改标,拆分加 `pm:retriage`。 - 派发前按序执行原子对:① Assign @me,并把 `pm:dispatched` 与摘 `pm:queue` 放进同一次标签写入。 diff --git a/scripts/pm/check-half-states.mjs b/scripts/pm/check-half-states.mjs index 388bebef01e..d700200db5f 100644 --- a/scripts/pm/check-half-states.mjs +++ b/scripts/pm/check-half-states.mjs @@ -15720,6 +15720,48 @@ export function h72OffTableMailboxLines(reading, nowMs = Date.now()) { ); } +// --------------------------------------------------------------------------- +// H76 — an OPEN PR with NO assignee, a `pm:mailbox` PR excepted. Ruling record +// 6674390671 (letter A): a PR names its owner — the dev's `pr_create` carries +// the claim's `Account:` as its assignee, and a hand-over (sibling takeover, +// release, reassignment) re-assigns the PR in the same stroke as the claim +// change (`references/execution-duties.md` 认领). The same ruling makes this +// row report-only, ⛔ no gate. A mailbox is a seat's doorbell, never a delivery +// (`project-forms.md` 〈信箱 PR〉), so it is exempt; its label is +// `mailbox-open.mjs`'s MAILBOX_LABEL, handed in by the sweep, never spelled +// here. When the sweep could not load that label the row cannot tell a mailbox +// from a delivery, so it fires UNJUDGED, never clean (#4690). Drafts are +// INCLUDED: a dev's PR stays a draft until its seat lands it. Free — it reads +// the open listing already in hand. +// --------------------------------------------------------------------------- + +/** + * H76 — null when clean or out of scope, else the finding sentence. + * + * @param {object} pr — a `/pulls?state=open` list row. + * @param {string} mailboxLabel — `mailbox-open.mjs`'s MAILBOX_LABEL, handed in by the sweep; anything else is unread. + */ +export function h76OpenPrWithoutAssignee(pr, mailboxLabel) { + if (!pr || pr.state === 'closed') return null; + if ((pr.assignees ?? []).length > 0 || pr.assignee) return null; + if (typeof mailboxLabel !== 'string' || mailboxLabel === '') { + return ( + `${UNJUDGED_MARKER} open PR with NO assignee, and the mailbox label could not be read this sweep — ` + + 'whether it is an exempt mailbox PR is UNJUDGED, not settled. Read its labels by hand: a mailbox is ' + + 'exempt, any other open PR owes its owner as assignee.' + ); + } + if (labelNames(pr).includes(mailboxLabel)) return null; + return ( + 'open PR with NO assignee. A PR names its owner (ruling record 6674390671): the dev\'s `pr_create` carries ' + + 'the claim\'s `Account:`, and a hand-over (sibling takeover, release, reassignment) re-assigns the PR in the ' + + 'same stroke as the claim change. Remedy — WHO and HOW: the seat holding the card this PR names assigns that ' + + 'card\'s assignee through the relay (`assign` op, the PR number as `issue`); a bot PR with no card takes its ' + + 'owning lane seat\'s account, or stays unassigned with a note. ⛔ Report-only: nothing is written from this ' + + `script. Out of scope: a \`${mailboxLabel}\` PR (a seat's doorbell, never a delivery).` + ); +} + // --------------------------------------------------------------------------- // H64 (#18069, re-keyed by #18237) — a seat- or dev-signed artefact that names // no session. @@ -19910,6 +19952,11 @@ export const HALF_STATE_FAMILY_BAND = Object.freeze({ // is stopped (not `stall`), and no write on the board repairs a sent comment // (not `state`) — the repair is the next bell, spelled from the table. H72: 'inventory', + + // H76 is a `state`, H1's band for H1's reason: a live PR half-written — the + // owner the ruling makes it name is missing, and the repair is one relay + // `assign` on the board. ⛔ Not `stall`: an unowned PR can still land. + H76: 'state', }); const FAMILY_BAND_RANK = new Map(HALF_STATE_FAMILY_BANDS.map((b) => [b.name, b.rank])); @@ -23797,6 +23844,22 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen } } + // H76 — an open PR with no assignee, off the listing above, free. The mailbox + // label is imported LAZILY for H72's reason (banner above `h72NeedsThread`); + // a failed import leaves it unread, and every unowned PR then fires UNJUDGED. + { + let mailboxLabel = null; + try { + ({ MAILBOX_LABEL: mailboxLabel } = await import('./mailbox-open.mjs')); + } catch { + mailboxLabel = null; + } + for (const pr of seenPrs.values()) { + const unowned = h76OpenPrWithoutAssignee(pr, mailboxLabel); + if (unowned) findings.push([pr, 'H76', unowned]); + } + } + // H16 — the only predicate here whose input no listing carries: // `mergeable_state` lives on the single-PR endpoint alone. One GET per // CANDIDATE (the gathering policy is `h16NeedsDetail`, which answers from @@ -25309,6 +25372,9 @@ export const SELF_TEST_BATTERIES = Object.freeze({ // through this predicate — lives in record-recognisers.mjs's doorbell battery, // because this self-test cannot import that module (H62's banner). 'H72 mailbox off-table bells': 20, + // Registered with the unowned-PR row, pin just under the count: the firing, the assignee/mailbox/closed + // exemptions, the unread-label UNJUDGED leg, the text, the band and the wiring. + 'H76 unowned open PR': 11, // Registered with the ruled-card-in-the-box row (#29386), pin just under the // count: the firing case, the label / fence / anchoring / id controls each // beside its positive twin, the threshold boundary, the governing stamp, the @@ -28656,6 +28722,26 @@ async function selfTest() { b(BATTERY28675, '⭐ H72: the sweep takes the ONE reader and the opener\'s label LAZILY — `mailboxEvents` from record-recognisers.mjs, MAILBOX_LABEL from mailbox-open.mjs — and spells no doorbell key of its own', (() => { const src = readFileSync(fileURLToPath(import.meta.url), 'utf8'); return src.includes("import('./record-recognisers.mjs')") && src.includes("import('./mailbox-open.mjs')") && !/^import [^\n]*from '\.\/(?:record-recognisers|mailbox-open)\.mjs'/mu.test(src) && !src.includes(['MAILBOX_KEYS', '='].join(' ')); })(), true); b(BATTERY28675, 'floor: this battery is DECLARED on the roster, with a positive pin', Object.prototype.hasOwnProperty.call(SELF_TEST_BATTERIES, BATTERY28675) && SELF_TEST_BATTERIES[BATTERY28675] > 0, true); + // -- H76: an open PR with no assignee (ruling record 6674390671, letter A). The fixture is a relay-opened dev + // draft as the card measured them (26 of 27 open PRs unassigned), offline, never a live read. + const BATTERY76 = 'H76 unowned open PR'; + const MAILBOX76 = 'pm:mailbox'; + const pr76 = (extra = {}) => ({ number: 30778, state: 'open', draft: true, assignees: [], assignee: null, labels: [{ name: 'skip-changeset' }], user: { login: 'objectstack-fleet[bot]', type: 'Bot' }, ...extra }); + const row76 = String(h76OpenPrWithoutAssignee(pr76(), MAILBOX76) ?? ''); + b(BATTERY76, 'H76 fires: an open dev draft with no assignee and no mailbox label', row76.length > 0 && !isUnjudgedFinding(row76), true); + b(BATTERY76, 'H76 row: names the ruling, the claim\'s `Account:`, the relay `assign` op, the bot-PR arm and the report-only boundary', ['ruling record 6674390671', 'the claim\'s `Account:`', '`assign` op', 'owning lane seat\'s account', 'Report-only'].every((x) => row76.includes(x)), true); + b(BATTERY76, 'H76: a ready (non-draft) PR fires exactly like a draft — drafts are included, not the only population', typeof h76OpenPrWithoutAssignee(pr76({ draft: false }), MAILBOX76), 'string'); + b(BATTERY76, '⛔ H76: a PR with an assignee is clean — the list field or the legacy single field', [h76OpenPrWithoutAssignee(pr76({ assignees: [{ login: 'os-elon-musk' }] }), MAILBOX76), h76OpenPrWithoutAssignee(pr76({ assignee: { login: 'os-elon-musk' } }), MAILBOX76)].join(), ','); + b(BATTERY76, '⛔ H76: an unassigned `pm:mailbox` PR is exempt, and its unlabelled twin is not', [h76OpenPrWithoutAssignee(pr76({ labels: [{ name: MAILBOX76 }, { name: 'pinned' }] }), MAILBOX76), typeof h76OpenPrWithoutAssignee(pr76({ labels: [{ name: 'pinned' }] }), MAILBOX76)].join(), ',string'); + b(BATTERY76, '⛔ H76: a closed PR and a missing row are silent, never a crash', [h76OpenPrWithoutAssignee(pr76({ state: 'closed' }), MAILBOX76), h76OpenPrWithoutAssignee(undefined, MAILBOX76), h76OpenPrWithoutAssignee(null, MAILBOX76)].join(), ',,'); + const unread76 = [null, undefined, ''].map((label) => String(h76OpenPrWithoutAssignee(pr76(), label) ?? '')); + b(BATTERY76, 'H76: an UNREAD mailbox label fires UNJUDGED, never clean — null, undefined and empty alike', unread76.every((t) => t.startsWith(UNJUDGED_MARKER) && isUnjudgedFinding(t)), true); + b(BATTERY76, '⛔ H76: …but an ASSIGNED PR stays clean with the label unread — the label is only the mailbox exemption', h76OpenPrWithoutAssignee(pr76({ assignees: [{ login: 'os-elon-musk' }] }), null), null); + b(BATTERY76, 'H76: not a loud finding — it never escalates a sweep', [isLoudFinding(row76), isLoudFinding(unread76[0])].join(), 'false,false'); + b(BATTERY76, 'H76 band: registered as `state` (H1\'s), and the sweep really pushes it', `${familyBand('H76')}/${familyRegistryCoverage().emitted.includes('H76')}`, 'state/true'); + b(BATTERY76, 'H76 wiring: every listed open PR is judged with the label taken LAZILY from mailbox-open.mjs, never spelled in the sweep', (() => { const src = readFileSync(fileURLToPath(import.meta.url), 'utf8'); return src.includes("({ MAILBOX_LABEL: mailboxLabel } = await import('./mailbox-open.mjs'));") && src.includes('const unowned = h76OpenPrWithoutAssignee(pr, mailboxLabel);'); })(), true); + b(BATTERY76, 'floor: this battery is DECLARED on the frozen roster with a positive pin', Object.hasOwn(SELF_TEST_BATTERIES, BATTERY76) && SELF_TEST_BATTERIES[BATTERY76] > 0 && Object.isFrozen(SELF_TEST_BATTERIES), true); + // -- H73 (#29386): a RULED decision card still sitting in the decision box. // Positive controls beside every negative one: the label, the fence, the // anchoring, the id, the threshold, the governing stamp, the re-entry line.