Skip to content

Commit 236af73

Browse files
pm-dispatch: the qa#1 Routine seat owns the QA routines and record close; qa-rollup reads page 2 on the /repos path (#31333)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X9MWHhG8KycPJJjaBkrv3xH)_ Fixes #31305 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:新增 `qa#1` 席位章程 `lanes/qa.md`(每周回归四张记录、发版后 npm 冒烟、收口关卡与 supersede、每周跑欠测清单;不派开发、不改产品代码);`lanes/skills.md` 与 checklist-test §6 把两条例行的负责人统一写成 `qa#1`,§5 补一行「收口完成即由 QA 席关记录」;`qa` 加进座位签名与信箱的席位词;`qa-rollup.mjs` 第 2 页改走 `/repos/...` 路径。 - 为什么改:你选了 A(「同意 新设 qa#1」)。过去两条例行一边写「归 skills 席」一边写「派发的 PM 会话建」,结果没人跑;记录收口后没人关;`qa-rollup.mjs` 在云席读到第 2 页就 403。 - 风险与回滚:只动内部指令文本与 PM/QA 脚本,不发布(skip-changeset)。回滚即 revert;不启动任何 routine。 - 先看:`.claude/skills/pm-dispatch/references/lanes/qa.md` 全文;`.claude/skills/checklist-test/SKILL.md` §5 新增的「关闭」段与 §6 首段;`scripts/qa/qa-rollup.mjs` 的 `fetchRunRecords`。 - 两分钟验证:`node scripts/qa/qa-rollup.mjs --self-test`,预期 `✓ … 82 assertions`;云席里 `NODE_USE_ENV_PROXY=1 node scripts/qa/qa-rollup.mjs --json`,预期 `apiCalls: 2`、180 条记录,不再 403。 - 需要你做的:无,席位落地(Tier S,席内契约复审 PASS 后入队);合并后请在 Routines UI 为 `qa#1` 建每周 routine。 ## What changes - **New `.claude/skills/pm-dispatch/references/lanes/qa.md`** (29 lines): the `qa#N` role seat, a Routine seat the maintainer creates in the Routines UI with a weekly fire, plus one round after each release. Each round: `retest-owed.mjs` first (its owed ids scope the `owed:weekly` record), the four weekly records of checklist-test §6, the post-release `smoke:npm-VERSION` record, close-out of every record per §5, close `completed` once close-out is complete, supersede per §5, `qa-rollup.mjs` at round end for the open-record count, and a spend line per round. Boundaries: no `os-dev`, no product code, no PR; a §3 checklist revision is filed as a card with the item's lane like an assertion defect; defect cards go to triage; security findings only through `security-findings.md`; waves stay manual; skill and RUNNER text stays with `domain:skills`. First act once started: `retest-owed.mjs`, then the first `owed:weekly` record. - **Text owed item 1** (one owner): `lanes/skills.md` 「QA 管理循环执行归本席」 now names `qa#1` (`qa.md`) as the executor and keeps the skills seat on the text; checklist-test §6 replaces "the dispatching PM session builds the routines" with: the two routines belong to `qa#1`, and the maintainer builds its routine. - **Text owed item 2**: `lanes/skills.md` now says waves are manual and the two checklist-test §6 routines run on schedule, by `qa#1`, and are not waves. - **Text owed item 3**: checklist-test §5 gains a 「关闭」 paragraph before the Supersede protocol: close-out complete means the QA seat closes the record `completed` that round with a pointer to the close-out comment; residuals stay on their own cards; retests are owed through `retest-owed.mjs`; a record is not held open for fixes. - **Item 4**: `scripts/qa/qa-rollup.mjs` `fetchRunRecords` now carries the `rel="next"` link's query onto the `/repos/OWNER/REPO/...` path it started from (`onPathOf`), instead of following the numeric `/repositories/ID/...` link the egress proxy refuses. `onPathOf` moves from `retest-owed.mjs` into `qa-rollup.mjs` (which `retest-owed.mjs` already imports from), so the two tools share one spelling. `fetchRunRecords` is exported and takes a `fetchImpl` seam for the self-test. `docs/qa/platform-checklist/RUNNER.md`'s note on the page-2 failure is rewritten to the fixed behaviour. - **Rosters the new seat moves** (pin moves, no new gate, label or protocol key): - the seat lane-word grammar: `qa` joins `SEAT_BARE_LANES` (`scripts/pm/mailbox-open.mjs`, its prose and its self-test pins) and the signature alternation `SIGNATURE_LANE_SOURCE` (`scripts/pm/check-half-states.mjs`). Without it a `qa#1 · ACCOUNT · SESSION` signature is refused by `post-stamped.mjs` and `issue-create.mjs`, so the seat could not write a record. `fleet-roster.mjs`'s self-test holds the two lists equal and passes unchanged. This follows the `review#N` and `security#N` seats. - `project-forms.md`: `qa` joins the seat-word list (`pm-dispatch qa`). - `scripts/pm/check-skill-line-ratchet.mjs`: a `CEILINGS` row for `lanes/qa.md` (29, headroom 0, a new file) and its `MAX_TABLE_ROW_BYTES` row (0); the `laneRosterVerdict` census requires both. ## Ratchet raise `.claude/skills/checklist-test/SKILL.md` ceiling 276 → 279. Ruling of record: comment 6715420603 (maintainer letter A, 「同意 新设 qa#1」), whose workload orders the three text fixes and `lanes/qa.md`. Measured line delta **+3** (276 → 279: the two-line 「关闭」 paragraph and its paragraph break; the §6 rewrite is 0 lines). The row moves by exactly +3. `lanes/skills.md` stays at 32 lines (ceiling 33), `project-forms.md` at 59; `lanes/qa.md` is a new row at its landed count. ## Tests - `node scripts/qa/qa-rollup.mjs --self-test`: `✓ qa-rollup --self-test: 82 assertions over 5 canonical shapes and 16 retired ones` (new battery 「a listing stays on the repository path」, 4 cases, floor 11 → 12 batteries). The battery feeds a fake fetch whose page 1 carries GitHub's real next-link shape naming `/repositories/42/...`, and which answers that path 403 the way the proxy does. - **Ablation** (`scripts/ablation-replace.mjs`, on committed `390c0a0e`): `url = next ? onPathOf(url, next[1]) : null;` → `url = next ? next[1] : null; // ABL` (injected count 1, anchor count 0). Self-test exit 1 with 3 failures: 「page 2 is read, not refused: GitHub API 403 Forbidden for …/repositories/42/…」, 「both pages land, in order」, 「the next page is asked on the /repos path, never /repositories/ID」. Restored: blob `b7cb4cd1e861` == HEAD, `git diff HEAD` empty; self-test green again. - `node scripts/qa/retest-owed.mjs --self-test`: `✓ 57 assertions in 9 batteries` (its `onPathOf` battery now exercises the moved helper). - `--self-test` exit 0: `mailbox-open` (87 cases), `fleet-roster` (115), `check-half-states` (5253), `post-stamped` (753), `record-recognisers` (375), `check-skill-line-ratchet` (164); `node scripts/pm/check-skill-line-ratchet.mjs` exit 0. - **Real runs in a cloud container, read-only** (`NODE_USE_ENV_PROXY=1`, on `390c0a0e`): - `qa-rollup.mjs --json`: exit 0, `apiCalls: 2`, corpus 180 (open 0, closed 180, parsed 156, unparseable 24). - Control leg, `qa-rollup.mjs` from `origin/main` `cba7d8ae`: exit 1, `GitHub API 403 Forbidden for https://api.github.com/repositories/1136691870/issues?labels=qa-run&state=all&per_page=100&after=…&page=2`. - `retest-owed.mjs --json`: exit 0, `apiCalls: 260`, 180 records, 21,762 issues and PRs scanned, 356 fix cards, 40 subject shas all resolved; owed 73, re-judged 120, unknown 0, not landed 6, not read 55. ## Acceptance notes - Premise correction on item 4: `retest-owed.mjs` already followed next links on the `/repos` path (`onPathOf`), so only `qa-rollup.mjs` failed on page 2; the card's evaluation thread said the same. The fix reuses that helper rather than rebuilding a `page=N` URL: GitHub's next link for this listing carries both an `after=` cursor and `page=2`, and carrying its whole query keeps both. - `.claude/skills/pm-dispatch/SKILL.md`'s 域车道 table is not changed. Its `security` · `review` row is 315 bytes against the 341-byte `MAX_TABLE_ROW_BYTES` pin; naming `qa` there costs about 45 bytes, which needs a ruled raise or cuts to other rows' text. The triage and director role seats are not in that table either; `project-forms.md`'s seat-word list is where a summons word is read. - Landing tier: `check-governed-merges.mjs --branch` answers **Tier S** (all four governed paths are under `.claude/**`), not Tier H as the card's ruling text expected. Landing is the owning seat's, after an in-seat `CONTRACT_REVIEW_TIER` review PASS on the current head and green checks. - The 15 `qa-run` closes and #29560 were triage's and are not in this PR. The real run above reads 0 open `qa-run` records. ## Verification Gate list from `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on the real diff (57 commands), run on `390c0a0e`: all 57 exit 0; `dispatch-gates --ran` reconciliation 57 derived, 57 run, 0 NOT-MEASURED, 0 UNRUN. `check:doc-formula-expressions` first answered exit 3 (PREREQUISITE NOT MET: formula and lint `dist/` absent in a fresh worktree) and exit 0 after `turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` under `os-verify-lock.sh`. `node scripts/pm/dispatch-gates.mjs --tier`: no path-derived model mandate. `node scripts/pm/check-governed-merges.mjs --branch claude/project-thread-6n4eey`: GOVERNED, Tier S, `.claude/**` ×4; 160 changed lines (+118 / −42), under 3000. Scoped lint: `pnpm exec eslint --no-inline-config` over the five edited scripts, exit 0. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01FEkQ5pHA5xyshr9r7aTiAf --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9447f95 commit 236af73

10 files changed

Lines changed: 118 additions & 42 deletions

File tree

‎.claude/skills/checklist-test/SKILL.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,9 @@ QA-source: #运行记录号 · area.item · 条款
218218
或直接填实值。**唯一例外是 npm 冒烟**(§6):定位字段写冒烟选择器、条款写格号,如
219219
`QA-source: #运行记录号 · smoke:npm-18.2.0 · C3` —— 下一轮冒烟凭这一行重判它。
220220

221+
**关闭** —— 收口完成即由 QA 席当轮关记录 `completed`,一行指向收口评论;遗留项留在各
222+
自卡上,回测经 `retest-owed.mjs` 欠着,⛔ 不为等修复开着。
223+
221224
**Supersede 协议** —— 重跑覆盖了旧记录的地面时,旧记录关闭(`not_planned` + 一行
222225
`Superseded-by:` 指向新记录),但**先把新一轮没有重新判定的判定结转过去**:结转行
223226
(`Carried-forward:`,引旧记录号)落在**新记录**上,supersede 行落在**旧记录**上。⛔
@@ -241,8 +244,8 @@ QA-source: #运行记录号 · area.item · 条款
241244

242245
## 6. 常驻回归 —— 两次发版之间也重跑
243246

244-
维护者所定的两条例行(裁定评论 6501246005);定时任务由派发的 PM 会话建,⛔ 不是仓库代码,也
245-
不是合并门禁。
247+
维护者所定的两条例行(裁定评论 6501246005)归 QA 席 `qa#1`(pm-dispatch `lanes/qa.md`);其定时任务
248+
由维护者为该席建,⛔ 不是仓库代码,也不是合并门禁。
246249

247250
- **每周回归**,对 `main` HEAD,四张 `qa-run` 记录,一个选择器一张:
248251
- 欠测清单:标题选择器写 `owed:weekly`(⛔ 不带冒号的词会被读成区),清单里每个 id
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# 席位章程:QA 席(qa)
2+
见 checklist-test 技能 §4–§6;本文是本席岗位说明,现值状态恒在座位贴,⛔ 不迁入本文。
3+
4+
## 座位与唤醒
5+
6+
- 席位 `qa#N`,首席 `qa#1`;签名、座位贴、信箱同他席;无 `domain:*` 标签、无 `pm:queue`。
7+
- Routine 席:维护者在 Routines UI 建每周 fire,发版后敲一轮;⛔ 本席不自建、不改频率。
8+
- 发版只认维护者一句或总监席摘要的新 tag 族读数;⛔ 不自判发版。
9+
- 开席首轮:先 `retest-owed.mjs`,再立第一张 `owed:weekly` 记录;此后每 fire 一轮。
10+
11+
## 每轮
12+
13+
- 首动作 `node scripts/qa/retest-owed.mjs`(只读);欠测 id 即本轮 `owed:weekly` 记录的范围。
14+
- 每周回归照 §6 四张:`owed:weekly`、`touched:` 上周 subject、`priority:P0`、`surface:api`。
15+
- 发版后 `smoke:npm-<版本>` 照 §6;上轮冒烟抽出的修复卡在此重判。
16+
- 执行照 checklist-test 与 RUNNER;本席即派 runner 的会话,runner 的安全复现私报本席。
17+
- 每张记录当轮照 §5 收口:四分法处置、收口评论机器行、抽取卡带 `QA-source:` 行。
18+
- 收口完即关记录 `completed`,一行指向收口评论;遗留项留在各自卡上,⛔ 不为等修复开着。
19+
- 新记录覆盖旧地面 ⇒ 先结转、再关旧记录,照 §5 Supersede 协议。
20+
- 轮末 `node scripts/qa/qa-rollup.mjs` 读开着的 `qa-run` 数;非零逐张点名欠什么,进轮次记录。
21+
- 每轮记花费行(环境准备与测试墙钟、runner 花费);三周读数进座位贴,频率由维护者定。
22+
23+
## 边界
24+
25+
- ⛔ 不派 `os-dev`、不改产品代码、不开 PR;§3 清单修订同断言缺陷立卡交该项车道。
26+
- 抽出的缺陷卡照常进分诊首触,⛔ 不定级、不入队;安全发现只照 `../security-findings.md`。
27+
- 波次(发版候选全量、维护者点名目标)仍由维护者手动触发;本席只跑 §6 两条例行。
28+
- 技能文本归 `domain:skills`、RUNNER 归 `domain:devx`;本席见文本问题以 `finding` 报,⛔ 不自改。
29+
- GitHub 写只走 checklist-test §4 写入门;推维护者只限需其出手,⛔ 无周报聊天表。

‎.claude/skills/pm-dispatch/references/lanes/skills.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
- PM 循环工具与指令架构文件的归属见 SKILL.md 域车道表 `domain:skills` 行,⛔ 不另抄。
99
- 受管面治理执行文件:`.github/CODEOWNERS` 路由半边 + SUBJECT 是受管面本身的门禁/审计脚本。
1010
- 全量判据在 SKILL.md 域车道表;受管面定义与两层分档见 SKILL.md 〈路径分叉〉,⛔ 不另抄。
11-
- QA 管理循环执行归本席;qa-run 记录是协议载体不是工作。
11+
- QA 循环执行归 `qa#1` 席(`qa.md`),本席只管其技能文本;qa-run 记录是协议载体不是工作。
1212
- 契约复核归派发席席内、决裁勤务归总监席(`director.md`);本席保留 skill 与治理文本。
1313

1414
## 常设承诺
@@ -22,7 +22,7 @@
2222
- finding 归分诊首触,自分诊见 `triage-duties.md`;可附证据与「Instruments serve the fleet」答案。
2323
- 「Instruments serve the fleet」答不出的 finding ⇒ 建议 p3 或不做,⛔ 不自入队;定级归分诊。
2424
- 内部指令面中文,机器判据恒英文;认领前重读全线程;标签写 read-modify-write 加回读。
25-
- QA 波次由维护者手动触发,⛔ 不自启。
25+
- QA 波次由维护者手动触发,⛔ 不自启;checklist-test §6 两条例行按期由 `qa#1` 跑,不是波次。
2626

2727
## 席内判断
2828

‎.claude/skills/pm-dispatch/references/project-forms.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
- 项目指令只有一行:`pm-dispatch <席位> [<席位>…]`,如 `pm-dispatch 总监`、`pm-dispatch ui skills`。
1414
- 首词恒为技能目录名 `pm-dispatch`;⛔ 不用 `fleet`,它已是 bot 与中继的名字。
1515
- 席位词 = 中文名或车道文件名:总监 = `lanes/director.md`,分诊 = `triage.md`,复核 = `review.md`。
16-
- 其余席位词同 `lanes/<lane>.md` 文件名:ui、skills、devx、server、spec、hotcrm、mobile。
16+
- 其余席位词同 `lanes/<lane>.md` 文件名:ui、skills、devx、server、spec、hotcrm、mobile、qa。
1717
- 一车道多席:协调者取 🟢 行写本账号的那一席;无则取空席;再无则问维护者一次。
1818
- 其余一切住本文:⛔ 不往项目里贴模板;⛔ 不在账号层复制技能。
1919

‎docs/qa/platform-checklist/RUNNER.md‎

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -772,20 +772,14 @@ contradicts it, and correct it here when it does.
772772
no test is not a pass, and here it is not a stale ref either — re-run it under the other
773773
setting before fixing the item.
774774

775-
- **`scripts/qa/qa-rollup.mjs` has two blind spots a runner meets at close-out.** (1) Only a
775+
- **`scripts/qa/qa-rollup.mjs` has a blind spot a runner meets at close-out.** Only a
776776
BARE area name classifies as an area: `classifySelector('platform-core')` answers `area`,
777777
while `area:platform-core` — the selector spelling `checklist-select.mjs` takes — answers
778778
`other`, so a record titled with it rolls up outside its area (16 records of 2026-10-16,
779779
#23926 among them, do). Title an area run with the bare name, `QA run · platform-core (…)`,
780-
as this sweep's records do. (2) Behind this container's egress proxy the roll-up dies on
781-
its second page: it follows GitHub's `Link: rel="next"`, which names
782-
`https://api.github.com/repositories/<id>/issues?…`, the proxy refuses numeric-id paths
783-
**403** ("Numeric-ID repository paths … are not supported through this proxy"), and
784-
`fetchRunRecords` throws on any non-OK page — measured at `bfc1539fe0` with 130 `qa-run`
785-
records, so page 1 (100) is all it reads. The same page asked for by number on the named
786-
path, `repos/objectstack-ai/objectstack/issues?labels=qa-run&state=all&per_page=100&page=2`,
787-
answers 200 (#24579 §6). ⛔ That 403 says nothing about access to the repository: run the
788-
roll-up where the proxy is not in the path, or read the pages by number.
780+
as this sweep's records do. Its second page is read on the named `repos/OWNER/REPO` path
781+
(`onPathOf`), never the numeric `repositories/<id>` path GitHub's next link names, which
782+
this container's egress proxy refuses 403 — a 403 there says nothing about repository access.
789783

790784
- **A map view shows its records as a list under a "Map failed to load" banner here, because
791785
the default base style is unreachable — an environment fact, not a product defect.** The

‎scripts/pm/check-half-states.mjs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1262,7 +1262,7 @@ export const RELEASE_COMMENT_MARKER = /^\s*>?\s*Release\s*:/mi;
12621262
//
12631263
// LANE a seat as the role table spells it: `domain:` or `repo:` plus a lowercase
12641264
// name (`domain:engine-core` carries a hyphen, measured on the label set),
1265-
// `epic:#` plus the parent card, `triage`, `director`, `coordinator`, `security` or `review`
1265+
// `epic:#` plus the parent card, `triage`, `director`, `coordinator`, `security`, `review` or `qa`
12661266
// N the seat number, from 1
12671267
// ACCOUNT the login a claim's `Account:` line carries — GitHub's own login rule
12681268
// SESSION the full id, in the relay envelope's shape (`SESSION_SHAPE`, fleet-write/ops.mjs,
@@ -1286,7 +1286,7 @@ export const RELEASE_COMMENT_MARKER = /^\s*>?\s*Release\s*:/mi;
12861286
// ---------------------------------------------------------------------------
12871287

12881288
/** The four parts' patterns, unanchored, in order — the ONE spelling every reader and writer composes from. */
1289-
const SIGNATURE_LANE_SOURCE = '(?:domain:[a-z][a-z0-9-]*|repo:[a-z][a-z0-9-]*|epic:#[1-9][0-9]*|triage|director|coordinator|security|review)';
1289+
const SIGNATURE_LANE_SOURCE = '(?:domain:[a-z][a-z0-9-]*|repo:[a-z][a-z0-9-]*|epic:#[1-9][0-9]*|triage|director|coordinator|security|review|qa)';
12901290
const SIGNATURE_ACCOUNT_SOURCE = '[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}';
12911291
/** The relay envelope's session shape without its anchors — `SESSION_SHAPE` in `fleet-write/ops.mjs`. */
12921292
export const SEAT_SIGNATURE_SESSION_SOURCE = 'session_[A-Za-z0-9]{6,}';

‎scripts/pm/check-skill-line-ratchet.mjs‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1680,6 +1680,17 @@ export const CEILINGS = new Map([
16801680
// added row and no other row moves; set at the landed line count (headroom
16811681
// 0, same convention as every lane charter above).
16821682
['.claude/skills/pm-dispatch/references/lanes/review.md', 21],
1683+
// QA seat charter (`qa#N`): the Routine seat that owns checklist-test §6's
1684+
// two standing routines (the weekly four-record regression, the post-release
1685+
// npm smoke), each round's close-out, record close and supersede, and the
1686+
// weekly `retest-owed.mjs` read that feeds the next regression; it dispatches
1687+
// no dev and changes no product code. Ruling of record: the maintainer's
1688+
// letter A on the card that ordered this file, 「同意 新设 qa#1」, recorded
1689+
// verbatim in triage's ruling comment 6715420603, whose workload names
1690+
// `references/lanes/qa.md`. A NEW file, so this is an added row and no other
1691+
// row moves; set at the landed line count (headroom 0, same convention as
1692+
// every lane charter above).
1693+
['.claude/skills/pm-dispatch/references/lanes/qa.md', 29],
16831694
// 399 → 405 (#11126): maintainer-ruled (2026-08-23, option B, quoted in that
16841695
// PR) — the +6-line cross-repo dispatch-gates caveat, sized so the queued
16851696
// #11137 (395→399 on main) and this PR's +6 compose to exactly 405.
@@ -1763,7 +1774,14 @@ export const CEILINGS = new Map([
17631774
// hardening re-runs the item itself).
17641775
// 277 → 276 (#29111): the Guardrails security definition and fix dispatch
17651776
// became a pointer at pm-dispatch security-findings.md (lowering is legitimate).
1766-
['.claude/skills/checklist-test/SKILL.md', 276],
1777+
// 276 → 279: ruling comment 6715420603 (maintainer letter A, 「同意 新设 qa#1」)
1778+
// orders the card's three text fixes, two of which land here: §6 names the
1779+
// `qa#1` seat as the routines' one owner (a rewrite in place, 0 lines), and §5
1780+
// gains the close line (close-out complete ⇒ the QA seat closes the record
1781+
// `completed`; residuals stay on their cards, retests owed through
1782+
// `retest-owed.mjs`). Measured delta +3 (two text lines and their paragraph
1783+
// break), ceiling moved by +3; landed count, headroom 0.
1784+
['.claude/skills/checklist-test/SKILL.md', 279],
17671785
['.claude/skills/checklist-author/SKILL.md', 62],
17681786
// 157 → 140 (#25753): lines that named objectui as a sibling repo retired; landed count.
17691787
['.claude/skills/dogfood-verification/SKILL.md', 140],
@@ -2481,6 +2499,7 @@ export const MAX_TABLE_ROW_BYTES = new Map([
24812499
['.claude/skills/pm-dispatch/references/lanes/director.md', 0],
24822500
['.claude/skills/pm-dispatch/references/lanes/triage.md', 0],
24832501
['.claude/skills/pm-dispatch/references/lanes/review.md', 0],
2502+
['.claude/skills/pm-dispatch/references/lanes/qa.md', 0],
24842503
['.claude/agents/os-dev.md', 0],
24852504
['.claude/skills/checklist-test/SKILL.md', 221],
24862505
['.claude/skills/checklist-author/SKILL.md', 0],

‎scripts/pm/mailbox-open.mjs‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@
4242
* a login as a seat any more.
4343
*
4444
* A seat is spelled `LANE#N` — `domain:ui#4`, `repo:cloud#1`, `triage`,
45-
* `epic:#27293#1` — its lane one of the six a seat post's title names
46-
* (`domain:<x>`, `repo:<x>`, `triage`, `director`, `security`, `review`) or the epic lane
45+
* `epic:#27293#1` — its lane one of the seven a seat post's title names
46+
* (`domain:<x>`, `repo:<x>`, `triage`, `director`, `security`, `review`, `qa`) or the epic lane
4747
* `epic:#<parent>`, the seat signature's spelling for the PM delegated a
4848
* `pm:epic` parent card (pm-dispatch `seat-lifecycle.md` 〈Epic 子树车道〉), so a
4949
* login never reads as a seat, and an absent number read as seat 1
@@ -52,7 +52,7 @@
5252
* because a ref cannot carry `:` or `#`: the default `domain:` kind dropped,
5353
* every other `:` (with the epic lane's `#` after it) and the seat's `#` turned
5454
* into `-` (`ui-4`, `repo-cloud-1`, `triage-1`, `security-1`, `review-1`, `epic-27293-1`). No `domain:`
55-
* lane may be named `triage`, `director`, `security`, `review`, `repo-…` or `epic-…` — `seatKey`
55+
* lane may be named `triage`, `director`, `security`, `review`, `qa`, `repo-…` or `epic-…` — `seatKey`
5656
* refuses one — which keeps the slug one-to-one.
5757
*
5858
* ## What it does, in order (open)
@@ -171,17 +171,17 @@ const REPO_SHAPE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
171171
const LOGIN_SHAPE = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
172172
const BRANCH_SHAPE = /^(?!\/|.*\/\/|.*\.\.|.*[\s~^:?*[\\]|.*\.lock$|.*\/$)[A-Za-z0-9._/-]+$/;
173173
const SESSION_SHAPE = /^(?:session|cse)_[A-Za-z0-9]+$/;
174-
// A seat: its lane — `domain:<x>`, `repo:<x>`, `triage`, `director`, `security`, `review`, the six a seat post's title
175-
// names, or the epic lane `epic:#<parent>` — then `#N`; absent = seat 1. Read lower-cased. The bare lanes are a closed
176-
// set of four, so a login is never read as a seat. The lists below are the ONE spelling of those lane words: the shape is
174+
// A seat: its lane — `domain:<x>`, `repo:<x>`, `triage`, `director`, `security`, `review`, `qa`, the seven a seat post's
175+
// title names, or the epic lane `epic:#<parent>` — then `#N`; absent = seat 1. Read lower-cased. The bare lanes are a
176+
// closed set of five, so a login is never read as a seat. The lists below are the ONE spelling of those lane words: the shape is
177177
// built from them, and so is the reserved set a `domain:` lane may not be named after (`seatKey`). The same
178-
// four words close the seat signature's lane alternation (`SIGNATURE_LANE_SOURCE`, check-half-states.mjs), which this
178+
// five words close the seat signature's lane alternation (`SIGNATURE_LANE_SOURCE`, check-half-states.mjs), which this
179179
// list cannot import (the patrol loads this file lazily, so that edge would be a cycle); fleet-roster's self-test,
180180
// which imports both, holds the two equal, `coordinator` (it signs and has no mailbox) aside.
181181
/** The lane kinds that carry a name after the colon: `domain:<x>`, `repo:<x>`. `domain` is the default kind the slug drops. */
182182
export const SEAT_LANE_KINDS = Object.freeze(['domain', 'repo']);
183-
/** The bare lanes, a closed set: `triage`, `director`, `security` (the seat that owns security findings, `lanes/security.md`), `review` (the contract-review seat, `lanes/review.md`). */
184-
export const SEAT_BARE_LANES = Object.freeze(['triage', 'director', 'security', 'review']);
183+
/** The bare lanes, a closed set: `triage`, `director`, `security` (the seat that owns security findings, `lanes/security.md`), `review` (the contract-review seat, `lanes/review.md`), `qa` (the QA routines seat, `lanes/qa.md`). */
184+
export const SEAT_BARE_LANES = Object.freeze(['triage', 'director', 'security', 'review', 'qa']);
185185
/** The epic lane's kind: `epic:#<parent>` names the `pm:epic` parent card's number, never a word (`epic:#27293#1`). */
186186
export const SEAT_EPIC_KIND = 'epic';
187187
const SEAT_SHAPE = new RegExp(`^((?:${SEAT_LANE_KINDS.join('|')}):[a-z0-9._-]+|${SEAT_EPIC_KIND}:#[1-9][0-9]*|${SEAT_BARE_LANES.join('|')})(?:#([1-9][0-9]*))?$`);
@@ -571,9 +571,9 @@ async function selfTest() {
571571
t('⛔ a `domain:` lane named `epic-…` is refused — its slug would be an epic seat\'s — while `domain:epic` and `domain:epics` read', [seatKey('domain:epic-12#1'), seatKey('domain:epic-tools'), seatKey('domain:epic').slug, seatKey('domain:epics').slug], [null, null, 'epic-1', 'epics-1']);
572572
t('⭐ the epic slug collides with no lane slug: one key per slug across every lane kind', (() => { const keys = ['epic:#12#1', 'domain:epic#12', 'domain:epic-12#1', 'repo:epic-12#1', 'domain:12#1', 'triage#12', 'epic:#1#21', 'epic:#12#1'].map(seatKey).filter(Boolean); return new Set(keys.map((k) => k.slug)).size === new Set(keys.map((k) => k.key)).size; })(), true);
573573
t('⛔ a short form is not a seat — `skills#2` names no lane, so it is refused as a login is', ['skills#2', 'ui#4', 'server', 'skills'].map(seatKey), [null, null, null, null]);
574-
t('⛔ a `domain:` lane named after a reserved lane word is refused — its slug would be another seat\'s', ['domain:triage', 'domain:director#2', 'domain:repo-cloud#1', 'Domain:Triage#1', 'domain:security#1', 'domain:review#1'].map(seatKey), [null, null, null, null, null, null]);
574+
t('⛔ a `domain:` lane named after a reserved lane word is refused — its slug would be another seat\'s', ['domain:triage', 'domain:director#2', 'domain:repo-cloud#1', 'Domain:Triage#1', 'domain:security#1', 'domain:review#1', 'domain:qa#1'].map(seatKey), [null, null, null, null, null, null, null]);
575575
t('…while a `domain:` lane that merely CONTAINS a reserved word, and the reserved words as their own lanes, still read', [seatKey('domain:triage-tools#1').slug, seatKey('domain:directory').slug, seatKey('domain:repository').slug, seatKey('triage#2').slug, seatKey('repo:cloud').slug], ['triage-tools-1', 'directory-1', 'repository-1', 'triage-2', 'repo-cloud-1']);
576-
t('the shape and the reserved set are built from the ONE set of lane-word lists', SEAT_LANE_KINDS.join() === 'domain,repo' && SEAT_BARE_LANES.join() === 'triage,director,security,review' && SEAT_EPIC_KIND === 'epic' && SEAT_BARE_LANES.every((w) => seatKey(w)?.key === `${w}#1` && seatKey(`domain:${w}`) === null) && seatKey(`${SEAT_EPIC_KIND}:#7`)?.key === 'epic:#7#1' && seatKey(`domain:${SEAT_EPIC_KIND}-7`) === null, true);
576+
t('the shape and the reserved set are built from the ONE set of lane-word lists', SEAT_LANE_KINDS.join() === 'domain,repo' && SEAT_BARE_LANES.join() === 'triage,director,security,review,qa' && SEAT_EPIC_KIND === 'epic' && SEAT_BARE_LANES.every((w) => seatKey(w)?.key === `${w}#1` && seatKey(`domain:${w}`) === null) && seatKey(`${SEAT_EPIC_KIND}:#7`)?.key === 'epic:#7#1' && seatKey(`domain:${SEAT_EPIC_KIND}-7`) === null, true);
577577
t('the branch is mailbox/<login>/<slug> — no `:` and no `#` in a ref', mailboxBranch('os-bill', seatKey('domain:ui#4')), 'mailbox/os-bill/ui-4');
578578
t('an epic seat\'s branch is mailbox/<login>/epic-N-K', mailboxBranch('sunhaolin', seatKey('epic:#27293#1')), 'mailbox/sunhaolin/epic-27293-1');
579579
t('⭐ two seats under ONE login get two branches', [mailboxBranch('os-sales', seatKey('domain:server#1')), mailboxBranch('os-sales', seatKey('domain:ui#1'))], ['mailbox/os-sales/server-1', 'mailbox/os-sales/ui-1']);

0 commit comments

Comments
 (0)