diff --git a/.claude/skills/pm-dispatch/references/execution-duties.md b/.claude/skills/pm-dispatch/references/execution-duties.md index 708a15c08bd..e14518b8267 100644 --- a/.claude/skills/pm-dispatch/references/execution-duties.md +++ b/.claude/skills/pm-dispatch/references/execution-duties.md @@ -12,10 +12,9 @@ - 维护者裁决逐字引入派发词裁决分区作约束,无则写明无裁决。 - 派发前做前提过时检查,裁决同罪,三面都查:动作面、卡引用面、工作项面。 - 动作面:`git log --oneline -20 -- `。 -- 裁决实施卡的动作面再核被点名的动作在 `origin/main` 上还没被做掉。 - 卡引用面:裁决/卡片点名的每张 issue 逐个读当前 open/closed/assignee。 - 引用读到的现状,⛔ 不引用裁决写作时的描述。 -- 工作项面:卡片与分诊评论列的每条工作项逐条对树核验是否仍未完成,⛔ 不对卡核验。 +- 工作项面:卡片、裁决与分诊评论列的工作项逐条对树核验是否仍未完成,⛔ 不对卡核验。 - `batch` 等待不填槽,四态见 SKILL.md 该行;同批独立按文件面不相交判,⛔ 不按包或 check 名。 - 软顶:本席已验收等人批的 Tier H PR 逾 5 ⇒ 停派 Tier H,S/非受管照常,轮报列积压;两周复测。 - single-writer 路径 = `SINGLE_CLAIM_PATHS` 所枚举;共享其它路径是普通并发,后落地方解冲突。 @@ -52,7 +51,8 @@ - 取卡全序:维护者直派插队卡(出处三件)> 契约面卡(判据见 `references/lanes/spec.md`)> 标签序。 - 标签序:`priority:p0` > `pm:blocking` > 功能点位次 > `target:` 板上项 > p1 > p2 > p3 > 无级。 - `pm:blocking` 级内先按解锁扇出(从 `Blocked-by:` 反向索引现算,⛔ 扇出数不落标签)。 -- 全序每级现读/现算,零逐卡维护;同级先 `Bug` 再卡龄;无级/缺 `Path:` 轮报记分诊缺口。 +- 全序每级现读/现算,零逐卡维护;无级/缺 `Path:` 轮报记分诊缺口。 +- 同级先非车队卡(作者非 `Bot` 且首行非席位签名),再 `Bug`、卡龄;⛔ 不升级、不越 p0。 - 一车道多席按全序交替取卡:席 1 奇位、席 2 偶位;只为破交替才发 `split`;认领戳仲裁。 - 空闲杂务:无卡可认领或等分诊的车道席对本车道卡做下四件;⛔ 不定级,自分诊例外照旧。 - ① 交付卫生:交付卡补收尾记录;离手摘 `pm:*` 同笔设下一态;`Blocked-by:` 全关即重验回队。 diff --git a/.claude/skills/pm-dispatch/references/lanes/director.md b/.claude/skills/pm-dispatch/references/lanes/director.md index 42d5814566b..4d0088b3878 100644 --- a/.claude/skills/pm-dispatch/references/lanes/director.md +++ b/.claude/skills/pm-dispatch/references/lanes/director.md @@ -19,6 +19,7 @@ - 开场读一次服役档记入会话审计行,读法见 `../platform-readings.md`;⛔ 不因档位拒裁停三职。 - 双射 carve-out:一张 `pm:seat` 座位贴,四段照旧;状态块逐场一行,各记己锚,⛔ 无占席人。 - 不占 `domain:*` 车道,范围按职责标签跨车道跨仓;永不认领 backlog、永不写码。 +- 改 skill、指令或治理文本归 skills 席;出裁决、清复审标、清点待人工清单归本席。 - 项目内卡线程报 `needs_decision` ⇒ 席位写四维分析加推荐,并入决裁批次呈维护者人工裁。 - 维护者令「必须人工决裁」撤销「有把握自己决定」;⛔ 无裁决线程、代裁、置信门。 - 本席任何形态都纯召唤制,⛔ 无任何 routine;分诊席默认同项目召唤,职责见 `triage.md`。 @@ -70,7 +71,10 @@ ## 职责四:每日摘要、发版板与总表 -- 每日摘要:在飞/待裁/待落地/撞墙、等复审逾 24h 的 PR、发版板、无卡 fork PR。 +- 每日摘要:在飞/待裁/待落地/撞墙、复审逾 24h 的 PR、发版板、无卡 fork PR、外部需求行。 +- 外部需求行三数现查并贴查询;非车队 = 作者 `user.type` 非 `Bot` 且正文首行非席位签名。 +- ① 窗内非车队开卡数;② 窗内关卡中非车队所开占比;③ open 无 `pm:*` 非车队卡逐张带龄。 +- 查法:①② 取 `since` 窗起全态列表,本地 `created_at`/`closed_at` 落窗才计;③ 取 open;皆排 PR。 - 座位贴与在飞活性复查归本摘要,见 `../dispatch-runbook.md` 巡查常设项与 `../triage-duties.md`。 - 发版板 `target:` 主仓归本席;判据二元,⛔ 不做优先级渐变:不修它,RC 能不能发。 - 判阻塞四类:① 用户今天就撞的已发布面缺陷;② 公开契约 declared≠enforced。 @@ -80,7 +84,3 @@ - 见新 tag 族发布 ⇒ 在 cloud 与每个应用仓立平台版本 bump 单;单张封顶,已有则追评刷新。 - bump 单等待卡清单 = hold 索引(`Restart-when:`/`Blocked-by:` 点名该发布的 open 卡),查询落正文。 - 车队总表随本摘要重生成,坐席/接管/收班同笔;一席一行,⛔ 不手改;见 `../project-forms.md`。 - -## 与 skills 席的分界 - -- 改 skill、指令或治理文本归 skills 席;出裁决、清复审标、清点待人工清单归本席。 diff --git a/scripts/pm/check-half-states.mjs b/scripts/pm/check-half-states.mjs index cf1ab28b65c..f483372cb51 100644 --- a/scripts/pm/check-half-states.mjs +++ b/scripts/pm/check-half-states.mjs @@ -17541,14 +17541,19 @@ export function h66ReleasedNonDispatchableRemainder(issue, commentRows) { // force is OLDEST FIRST. The cards a seat will reach for next are exactly the // cards this row must have read. So the plan sorts by the two legs of that // order this file can see for free off the label page — the priority rank, then -// card AGE — and the cap eats the tail a seat reaches LAST. +// the AUTHOR (a human-opened card before a fleet-opened one), then card AGE — +// and the cap eats the tail a seat reaches LAST. // // ⛔ It models the full 取卡全序 only in part, and says so rather than implying // otherwise: `target:` board membership is a board read this file does not -// make, and the same-priority 「先 `Bug`」 tiebreak is not taken. An unreadable -// `created_at` sorts LAST rather than first (#4690's direction on every -// timestamp here), with `number` as a total-order tiebreak so two runs over one -// board agree. +// make, and the same-priority 「先 `Bug`」 tiebreak is not taken. The author leg +// IS taken: within one rank a non-fleet card sorts before a fleet card, and ⛔ +// it never lifts a card out of its rank. A card is the fleet's when an App +// opened it (`user.type` `Bot`, every `[bot]` login) OR its body opens with a +// seat signature (`readSignatureLine`), since seat-operated human accounts file +// cards too. An unreadable author reads as the fleet's, ⛔ never promoted. An unreadable `created_at` sorts LAST rather than first (#4690's +// direction on every timestamp here), with `number` as a total-order tiebreak +// so two runs over one board agree. // --------------------------------------------------------------------------- /** @@ -17656,7 +17661,8 @@ export function h67SpeaksAbout(issue) { /** * Which candidates this run buys a page for, and which it defers — OLDEST - * FIRST, by the two legs of 取卡全序 this file can read for free. + * FIRST, by the legs of 取卡全序 this file can read for free: rank, then a + * non-fleet card before a fleet one (App author or seat-signed body), then age. * * ⛔ The opposite of `h66BuyOrder`, deliberately, and the header carries the * argument: H66 chases a fresh write, this one chases the card a dispatching @@ -17668,7 +17674,7 @@ export function h67SpeaksAbout(issue) { * what gets READ AT ALL is where a silent hole would live, and a plan the * self-test can drive offline is the only kind that can be pinned. * - * @param {{ number?: number, created_at?: string, labels?: object[] }[]} issues + * @param {{ number?: number, created_at?: string, labels?: object[], user?: object }[]} issues * @param {number} [cap] * @returns {{ read: object[], deferred: object[] }} */ @@ -17679,13 +17685,23 @@ export function h67ReadOrder(issues, cap = H67_TIMELINE_READ_CAP) { const at = H67_SELECTION_ORDER_LABELS.findIndex((l) => labels.includes(l)); return at === -1 ? H67_SELECTION_ORDER_LABELS.length : at; }; + // Non-fleet before fleet, inside one rank only; a seat-signed body or an + // unreadable author reads as the fleet's. + const fleet = (issue) => { + if (readSignatureLine(issue?.body) !== null) return 1; + const author = artefactAuthor(issue); + return author && !author.isApp ? 0 : 1; + }; const age = (issue) => { const parsed = Date.parse(issue?.created_at ?? ''); return Number.isFinite(parsed) ? parsed : Infinity; }; const ordered = [...rows].sort( (a, b) => - rank(a) - rank(b) || age(a) - age(b) || Number(a?.number ?? 0) - Number(b?.number ?? 0), + rank(a) - rank(b) || + fleet(a) - fleet(b) || + age(a) - age(b) || + Number(a?.number ?? 0) - Number(b?.number ?? 0), ); const limit = Number.isFinite(cap) && cap > 0 ? Math.floor(cap) : 0; return { read: ordered.slice(0, limit), deferred: ordered.slice(limit) }; @@ -25234,8 +25250,9 @@ export const SELF_TEST_BATTERIES = Object.freeze({ // `Part of` card must never read as a candidate for closing, and an OPEN PR // must stand the row down. A listing whose discriminators quietly stopped // being asserted would still print rows, and every one of them would read as - // 「close this」, which is the one thing the card forbids. - 'H67 queued merged-delivery reading': 142, + // 「close this」, which is the one thing the card forbids. Pinned AT the count + // since the read order took the author leg, so the same-priority pair cannot go. + 'H67 queued merged-delivery reading': 158, // Registered with the decorated ownership-marker reading (#18680); the pin // sits just under the count on its neighbours' grounds. What this battery // floors is a WIDENING of the two markers' READING, so the FIRING CONTROLS @@ -36112,6 +36129,19 @@ Doubles as the fire's **write self-check** (step 0). \`201\` is not the reading. b(BATTERY67, 'H67 order: `priority:p0` is reached FIRST, whatever its age', order67(CAND67, 4), '3,2,1,4'); b(BATTERY67, 'H67 order: …then OLDEST-FIRST among equals, which is the order in force', order67(CAND67.filter((i) => i.number !== 3), 3), '2,1,4'); b(BATTERY67, 'H67 order: ⛔ an unreadable `created_at` sorts LAST, ⛔ never promoted', order67([CAND67[3], CAND67[1]], 2), '2,4'); + const human67 = { login: 'outside-user', type: 'User' }; + const fleet67 = { login: 'objectstack-fleet[bot]', type: 'Bot' }; + const PAIR67 = [ + { number: 21, created_at: '2026-08-01T00:00:00Z', labels: [{ name: 'priority:p2' }], user: fleet67 }, + { number: 22, created_at: '2026-09-20T00:00:00Z', labels: [{ name: 'priority:p2' }], user: human67 }, + ]; + b(BATTERY67, '⭐ H67 order: within ONE priority a human-opened card is reached before an OLDER fleet-opened one', order67(PAIR67, 2), '22,21'); + b(BATTERY67, 'H67 order: …and the author leg is the ONLY reason — two fleet cards of that priority stay oldest-first', order67([PAIR67[0], { ...PAIR67[1], user: fleet67 }], 2), '21,22'); + b(BATTERY67, '⛔ H67 order: the author leg never lifts a card OUT of its priority, and never past p0', order67([{ ...PAIR67[1], labels: [{ name: 'priority:p3' }] }, PAIR67[0], { ...PAIR67[0], number: 23, labels: [{ name: 'priority:p0' }] }], 3), '23,21,22'); + const SIGNED67 = 'domain:skills#1 · huangyiirene · session_01J8VVAnMUWA9rufXzFaEh4J\n\nFiling gate ③ …'; + b(BATTERY67, '⭐ H67 order: a HUMAN account whose body opens with a seat signature is the fleet\'s — it stays behind the older fleet card', order67([{ ...PAIR67[1], body: SIGNED67 }, PAIR67[0]], 2), '21,22'); + b(BATTERY67, 'H67 order: …while the same human card with the signature off its first line is still reached first', order67([{ ...PAIR67[1], body: `A user report.\n\n${SIGNED67}` }, PAIR67[0]], 2), '22,21'); + b(BATTERY67, 'H67 order: every App author is the fleet\'s (`github-actions[bot]` too), and an unreadable author ⛔ is never promoted', order67([{ ...PAIR67[1], user: { login: 'github-actions[bot]', type: 'Bot' } }, { ...PAIR67[1], number: 24, user: undefined }, PAIR67[0]], 3), '21,22,24'); b(BATTERY67, 'H67 order: `pm:blocking` ranks above p1, as the selection order does', order67([{ number: 8, created_at: '2026-09-10T00:00:00Z', labels: [{ name: 'priority:p1' }] }, { number: 9, created_at: '2026-09-11T00:00:00Z', labels: [{ name: 'pm:blocking' }] }], 2), '9,8'); b(BATTERY67, 'H67 order: ⭐ it is the OPPOSITE of H66\'s, and that is the point', order67(CAND67.filter((i) => i.number !== 3 && i.number !== 4), 2) === h66BuyOrder(CAND67.filter((i) => i.number !== 3 && i.number !== 4).map((i) => ({ ...i, updated_at: i.created_at })), 2).buy.map((i) => i.number).join(','), false); b(BATTERY67, 'H67 cap: the rest are DEFERRED, ⛔ not dropped silently', h67ReadOrder(CAND67, 2).deferred.map((i) => i.number).join(','), '1,4');