You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 115bf92
Browse filesBrowse the repository at this point in the historyBrowse files
pm: the Notify: security doorbell reads a private repository's card as OWNER/REPO#N (#31107)
<!-- claude-projects-attribution: {"github_login":"huangyiirene"} -->
_Requested by **Irene** · [project
thread](https://claude.ai/code/project/chan_01TJXtQ1tVD26ecLDZxFS67x?thread=cmsg_01TJXtQ1tVD26ecLDZxFS67x3ZJ3vUAa9xu4fhtKsK3Vp6)_Fixes#31103
Clause-②: no
Metadata-protocol: none
`dispatch-gates --tier` reads no path-derived model mandate for this
surface; any mandate above this thread's model would be waived by the
maintainer's standing order in this project (2026-11-03). The
dispatching seat runs the contract review.
## 维护者速读
- 改了什么:`record-recognisers.mjs` 的 `Notify: security` 门铃新增一种读法 `Notify:
security · OWNER/REPO#N`(私有仓里那张普通 `security` 卡,读出仓库和卡号),裸 `#n`
仍只表示分诊挂标的公开卡;pm-dispatch 三处参考行把私有仓门铃写成 `OWNER/REPO#N`。
- 为什么改:PR #31102 让私有仓的安全发现建普通 `security` 卡并以裸 `#N` 敲
`security#1`,该号不指明仓库,安全席分不清私有仓卡和误公开卡,也打不开卡(#31102 契约复审 S2)。
- 风险与回滚:只改 pm 工具的读法与三行文字;新读法只带卡引用,不带机制、路径、对象名或发现编号;三个文件行数不变、每行 ≤120
B;回滚即 revert。
- 先看:`scripts/pm/record-recognisers.mjs` 的 `REPO_CARD_REF` 与
`'notify:security'` 读者,及新增两条自测。
- 两分钟验证:`node scripts/pm/record-recognisers.mjs --self-test`,预期退出 0,`368
cases pass`。
- 需要你做的:无。Tier 由 `check-governed-merges` 读出,本分支读为 Tier
S(`.claude/**`),席位契约复审后落地。
## What changes
### Grammar, before and after
| | Before | After |
|---|---|---|
| Template | `Notify: security · GHSA-ID or #n` | `Notify: security ·
GHSA-ID or OWNER/REPO#N or #n` |
| `GHSA-xxxx-xxxx-xxxx` | `{ advisory, numbers: [] }` | unchanged |
| `#n` (triage's labelled public card) | `{ card, numbers: [n] }` |
unchanged |
| `OWNER/REPO#N` (private repository's ordinary `security` issue) |
unreadable | `{ repo, card, numbers: [] }` |
`when` gains one branch: "or the finding is on a PRIVATE repository and
the seat filed an ordinary `security` issue there: that card as
`OWNER/REPO#N` ALONE". `from` and `to` are unchanged.
- **Shape reused, not invented.** `REPO_CARD_REF =
/^([A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._-]*)#([1-9][0-9]*)$/`
is the repository-qualified card reference `check-half-states.mjs`
already reads (`Restart-when: closed <owner/repo>#N` at L3385, `re-check
owner/repo#N` at L7008): two segments, each led by an alphanumeric. It
is mirrored, not imported, because that module is not a recogniser and
the regex is not exported there. The looser `REPO_SHAPE`
(`mailbox-open.mjs`, `issue-create.mjs`) admits a leading `.`, so it was
not used.
- **`numbers` stays empty** for the qualified form, as it does for the
advisory id: `numbers` lists numbers on this board, and a private
repository's `#N` is not one.
- Still one field: a qualified ref beside an advisory id, a public `#n`,
a path or free text is unreadable.
### Self-test cases and counts
Two pins added to the battery `⭐ the mailbox doorbell …`, floor raised
**42 → 44** (by exactly the two pins added, recorded beside the 41 → 42
note). Self-test total 366 → **368** cases, exit 0.
1. ⭐ `Notify: security · objectstack-ai/cloud#412` under `To:
security#1` reads `repo: 'objectstack-ai/cloud'`, `card: 412`, no
`advisory`, `numbers` empty; `my.org-1/a_b.c-d#7` reads its repository;
the bare `Notify: security · #29761` still reads `card: 29761` with no
`repo` and `numbers: [29761]`.
2. ⛔ unreadable (14 lines): no slash (`#412`), three
segments, empty owner, empty repo, owner led by `.`, `#0`, space before
`#`, trailing letters, the template `OWNER/REPO#N`, and the qualified
ref beside a GHSA id (either order), beside `#29761`, beside a path,
beside free text.
The existing pins still hold: the bare `#n` pin (L2869 on main), the
"template sent verbatim is not an event" pin (the new template stays
unreadable), and the both-ways key-name pin against `project-forms.md`
〈信箱 PR〉.
### Line readings (`.claude/**`, in place, net 0 lines)
```diff
--- a/.claude/skills/pm-dispatch/references/security-findings.md (L63, 105 → 115 B)
-- 席位两件事:照第 3 节建私卡;敲 `Notify: security · GHSA-ID`(私有仓 `#N`),只带编号。
+- 席位两件事:照第 3 节建私卡;敲 `Notify: security · GHSA-ID`(私有仓 `OWNER/REPO#N`),只带编号。
--- a/.claude/skills/pm-dispatch/references/project-forms.md (L48, 104 → 119 B)
-- 持发现的席→`security#1`:私卡建成或误公开卡挂标即 `Notify: security`,只带编号。
+- 持发现席→`security#1`:私卡建成或误公开卡挂标即 `Notify: security`,只带编号或 `OWNER/REPO#N`。
--- a/.claude/skills/pm-dispatch/references/lanes/security.md (L16, 117 → 118 B)
-- 接收 ①:发现者的一次 `Notify: security · GHSA-ID`(私有仓 `#N`)门铃,私卡已由其经中继建好。
+- 接收 ①:发现者 `Notify: security · GHSA-ID`(私有仓 `OWNER/REPO#N`)门铃,私卡已由其经中继建好。
```
Bytes are measured without the newline. Line counts are unchanged:
`security-findings.md` 65/65, `project-forms.md` 59/59,
`lanes/security.md` 53/53. No ceiling was raised and no line was
re-wrapped or merged. To fit 120 B, L48 drops `的` from `持发现的席`, and L16
drops `的一次`; the single-ring rule keeps its home in `project-forms.md`
L51 (no re-sends on any key). `lanes/security.md` L49 (`Notify: security
· #N`) is triage's public-card hand-over, so it is left alone.
### Other consumers
No other `scripts/pm` reader uses the parsed fields.
`check-half-states.mjs` H72 reads only each event's `state` (read or
not) through `mailboxEvents`. `check-skill-line-ratchet.mjs` names the
row only in a comment. Nothing else imports `readMailboxLine` or reads
`.card`, `.repo` or `.advisory` from it.
## Why
PR #31102 (#30114) made a security finding on a private repository an
ordinary `security` issue on that repository, and its holder rang
`security#1` with a bare `#N`. The reader only read a bare `#n` as
triage's labelled public card, so the security seat could not tell which
repository the card was on, or tell it from a mis-disclosed public card
except by the sender. This is should-fix S2 of the contract review of PR
#31102 (comment 6704473385).
## Verification
<details><summary>Local gates on 6c698a1: 38 derived, 38 run with exit
0, 0 NOT MEASURED (`dispatch-gates --ran`)</summary>
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 38 commands for the 4 changed paths.
Each was run with its exit code captured before any pipe.
- `pnpm --filter @objectstack/lint run check:doc-formula-expressions`
exited 3 (NOT MEASURED) on the first run because nothing was built.
After `pnpm exec turbo run build --filter=@objectstack/formula
--filter=@objectstack/lint` (exit 0) it exited 0. The other 37 exited 0
on the first run, including `check:pm-record-recognisers`,
`check:pm-skill-ratchet`, `check:pm-dispatch-gates`,
`check:pm-governed-merges`, `check:doc-authoring` and
`check:skill-frame-sync`.
- `--ran` printed: `38 derived famil(ies) accounted for — 38 run, 0
NOT-MEASURED (a DERIVED zero — all 38 recorded an exit code and none of
them is 3)`.
</details>
- `node scripts/pm/record-recognisers.mjs --self-test`: exit 0, `368
cases pass` (366 on `main`).
- `node scripts/pm/check-skill-line-ratchet.mjs`: exit 0.
- `node scripts/pm/check-governed-merges.mjs --branch
claude/project-thread-u4w7zq`: GOVERNED, Tier S (`.claude/**`);
`scripts/pm/record-recognisers.mjs` is not on the register; 32 changed
lines (+25 / −7), under 3000.
- `node scripts/pm/dispatch-gates.mjs --tier`: no path-derived model
mandate.
- `node scripts/check-commit-card-trailers.mjs --range
origin/main..HEAD`: exit 0 before the push.
`skip-changeset`: nothing published.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(No schema touched.)
- [x] 2. Every name I added uses the casing for its kind.
(`REPO_CARD_REF`, beside `ADVISORY_ID_REF`.)
- [x] 3. No formatter ran.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. Nothing publishes (`scripts/pm/**`, `.claude/**`), so no
changeset (`skip-changeset`). Both declaration lines are filled in
above.
- [x] 6. `packages/spec` not touched; nothing to regenerate.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English. (The 维护者速读 section and
the quoted skill lines are Chinese, per the lane template.)
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01J8VVAjdHfu9CsJ7tGW4TiM
<!-- signed: domain:skills#3 · huangyiirene ·
session_01J8VVAj7CvjkGXvgXzd6RZ7 -->
Co-authored-by: Claude <noreply@anthropic.com>
{id: 'notify:seat-online',kind: 'Notify',key: 'seat-online',from: 'the seating session (or a forced takeover, for the walled account)',to: 'the lane\'s sibling seats and triage',when: 'at seating',template: 'Notify: seat-online · LANE#N · LOGIN · mailbox #m · patrol TRIGGER_ID · takes: #a #b …'},
1305
1305
{id: 'notify:seat-offline',kind: 'Notify',key: 'seat-offline',from: 'the signing-off session',to: 'the lane\'s sibling seats and triage',when: 'at sign-off',template: 'Notify: seat-offline · LANE#N · in-flight: … · stay-behind: … · brief ID'},
1306
1306
{id: 'notify:split',kind: 'Notify',key: 'split',from: 'a seat of a multi-seat lane',to: 'its sibling seats',when: 'at round open, only when the list changed',template: 'Notify: split · LANE#N · takes: … · leaves: …'},
1307
-
{id: 'notify:security',kind: 'Notify',key: 'security',from: 'the seat holding the finding (a dev\'s or a QA runner\'s reaches its PM session privately first); triage, for a public card it labelled `security`',to: 'security#1',when: 'the seat\'s relay `advisory_create` draft landed: the opaque advisory id ALONE; or triage labelled a public card `security`: its number ALONE — never a mechanism, a path or an object name',template: 'Notify: security · GHSA-ID or #n'},
1307
+
{id: 'notify:security',kind: 'Notify',key: 'security',from: 'the seat holding the finding (a dev\'s or a QA runner\'s reaches its PM session privately first); triage, for a public card it labelled `security`',to: 'security#1',when: 'the seat\'s relay `advisory_create` draft landed: the opaque advisory id ALONE; or the finding is on a PRIVATE repository and the seat filed an ordinary `security` issue there: that card as `OWNER/REPO#N` ALONE; or triage labelled a public card `security`: its number ALONE — never a mechanism, a path or an object name',template: 'Notify: security · GHSA-ID or OWNER/REPO#N or #n'},
1308
1308
{id: 'request:progress',kind: 'Request',key: 'progress',from: 'triage',to: 'the claiming seat (or the online sibling an off-shift seat\'s items went to)',when: 'a claimed card or an open PR whose last PROGRESS (its head commit, any non-triage comment, a label change by its seat — never `updated_at`) is 3 h old: at most one per item per 60 min; none while an answered ETA stands, or while the item is parked (`Blocked-by:` / `Resume-from:`) or waits on a decision card; two unanswered ⇒ the round record lists it and the seat reads as possibly down',template: 'Request: progress · #n · idle since TIME'},
1309
1309
{id: 'notify:progress',kind: 'Notify',key: 'progress',from: 'the claiming seat',to: 'triage',when: 'answering a progress knock — the record is a comment on #n giving the next act and an ETA, the park (`Blocked-by:` / `Resume-from:`), or the release',template: 'Notify: progress · #n · record ID'},
// keys that stay refused, and the two end-to-end pins through check-half-states H72 (31 → 44 cases).
2265
2279
// Raised 40 → 41 with the `Notify: security · #n` reading (a labelled public card handed to the security seat).
2266
2280
// Raised 41 → 42 with the `Notify: main-green` reading (the knock that resumes a PR held for a main red).
2267
-
'⭐ the mailbox doorbell: `To: <seat>`, then one key of the table read — seat, kind, numbers, sha, login, time — and anything else refused': 42,
2281
+
// Raised 42 → 44 with the `Notify: security · OWNER/REPO#N` reading (a private repository's ordinary `security`
2282
+
// issue) and its unreadable near misses: two pins, two cases.
2283
+
'⭐ the mailbox doorbell: `To: <seat>`, then one key of the table read — seat, kind, numbers, sha, login, time — and anything else refused': 44,
2268
2284
'⭐ the signature line: the reader reads LANE#N · ACCOUNT · SESSION exactly, and one leading signature changes no first-line reading': 22,
2269
2285
'⭐ the `Contract-review-claim:` line — session, FULL head, time; lapses on a moved head or after two hours; the earliest live claim holds': 24,
2270
2286
'⭐ the `Contract-review-hold:` line — session, head, reason, until; a new head clears it; at 24 hours it reads overdue and still holds': 23,
@@ -2867,6 +2883,8 @@ export async function selfTest() {
2867
2883
t('⭐ `Notify: security` under `To: security#1` reads to the security seat (a bare lane `seatKey` spells) with the advisory\'s opaque id and nothing else — it names no card, so `numbers` is empty',(()=>{constr=readMailboxLine(DB.sec);returnr.state==='read'&&r.to==='security#1'&&r.id==='notify:security'&&r.kind==='Notify'&&r.key==='security'&&r.advisory==='GHSA-7q4h-9x2m-5c8v'&&r.numbers.length===0&&readMailboxLine(MSG('Notify: security · GHSA-7q4h-9x2m-5c8v','security')).to==='security#1';})());
2868
2884
t('⛔ `Notify: security` carrying free text, a second field, an upper-case or short id, or no id at all is unreadable — nothing but the opaque id, or a labelled card\'s number alone, rides the doorbell',['Notify: security · see the advisory','Notify: security · GHSA-7q4h-9x2m-5c8v · packages/x/src/door.ts','Notify: security · GHSA-7q4h-9x2m-5c8v · see the advisory','Notify: security · GHSA-7Q4H-9X2M-5C8V','Notify: security · GHSA-7q4h-9x2m','Notify: security'].every((l)=>readMailboxLine(MSG(l)).state==='unreadable'));
2869
2885
t('⭐ `Notify: security · #n` — triage handing a public card it labelled `security` to the seat — reads the card ALONE; the card beside an id, a second field, a PR, `#0` and the bare template stay unreadable',(()=>{constr=readMailboxLine(MSG('Notify: security · #29761','security#1'));returnr.state==='read'&&r.to==='security#1'&&r.id==='notify:security'&&r.card===29761&&r.advisory===undefined&&r.numbers.join()==='29761'&&['Notify: security · #29761 · GHSA-7q4h-9x2m-5c8v','Notify: security · #29761 · see the card','Notify: security · PR #29761','Notify: security · #0','Notify: security · GHSA-ID or #n'].every((l)=>readMailboxLine(MSG(l)).state==='unreadable');})());
2886
+
t('⭐ `Notify: security · OWNER/REPO#N` — a private repository\'s ordinary `security` issue — reads the repository AND the card, `numbers` empty (not a number on this board); the bare `#n` still reads the public card, with no repository',(()=>{constr=readMailboxLine(MSG('Notify: security · objectstack-ai/cloud#412','security#1'));constbare=readMailboxLine(MSG('Notify: security · #29761'));returnr.state==='read'&&r.to==='security#1'&&r.id==='notify:security'&&r.repo==='objectstack-ai/cloud'&&r.card===412&&r.advisory===undefined&&r.numbers.length===0&&readMailboxLine(MSG('Notify: security · my.org-1/a_b.c-d#7')).repo==='my.org-1/a_b.c-d'&&bare.state==='read'&&bare.card===29761&&bare.repo===undefined&&bare.numbers.join()==='29761';})());
2887
+
t('⛔ a malformed qualifier, or the qualified card beside an id, a public card, a path or free text, is unreadable — the private ring carries the card reference ALONE',['Notify: security · objectstack-ai#412','Notify: security · objectstack-ai/cloud/x#412','Notify: security · /cloud#412','Notify: security · objectstack-ai/#412','Notify: security · .x/cloud#412','Notify: security · objectstack-ai/cloud#0','Notify: security · objectstack-ai/cloud #412','Notify: security · objectstack-ai/cloud#412a','Notify: security · OWNER/REPO#N','Notify: security · objectstack-ai/cloud#412 · GHSA-7q4h-9x2m-5c8v','Notify: security · GHSA-7q4h-9x2m-5c8v · objectstack-ai/cloud#412','Notify: security · objectstack-ai/cloud#412 · #29761','Notify: security · objectstack-ai/cloud#412 · packages/x/src/door.ts','Notify: security · objectstack-ai/cloud#412 · see the card'].every((l)=>readMailboxLine(MSG(l)).state==='unreadable'));
2870
2888
t('⭐ `Request: progress` reads the item (a card or a PR, bare `#n`) and the time its last progress was read — with seconds too; the placeholder `TIME`, a duration, a date without a time, a local time and `PR #n` stay unreadable',(()=>{constr=readMailboxLine(DB.rp);constsec=readMailboxLine(DB.rp.replace('T00:06Z','T00:06:30Z'));returnr.state==='read'&&r.id==='request:progress'&&r.kind==='Request'&&r.key==='progress'&&r.to==='domain:skills#1'&&r.item===29420&&r.since==='2026-11-03T00:06Z'&&r.numbers.join()==='29420'&&sec.state==='read'&&sec.since==='2026-11-03T00:06:30Z'&&['Request: progress · #29420 · idle since TIME','Request: progress · #29420 · idle since 3h','Request: progress · #29420 · idle since 2026-11-03','Request: progress · #29420 · idle since 2026-11-03T00:06','Request: progress · #29420 · idle since 2026-11-03T00:06+08:00','Request: progress · PR #29420 · idle since 2026-11-03T00:06Z','Request: progress · #29420','Request: progress · #29420 · idle since 2026-11-03T00:06Z · please answer'].every((l)=>{constu=readMailboxLine(MSG(l));returnu.state==='unreadable'&&/doesnotspell/.test(u.reason);});})());
2871
2889
t('⭐ `Notify: progress` reads the item and the record id (the comment on it with the next act and an ETA, the park, or the release) — `record none`, a sentence and a second field stay unreadable',(()=>{constr=readMailboxLine(DB.np);returnr.state==='read'&&r.id==='notify:progress'&&r.kind==='Notify'&&r.key==='progress'&&r.to==='triage#1'&&r.item===29420&&r.record===6599000001&&r.numbers.join()==='29420'&&readMailboxLine(MSG('Notify: progress · #29420 · record #6599000001','triage')).record===6599000001&&['Notify: progress · #29420 · record none','Notify: progress · #29420 · record ID','Notify: progress · #29420 · ETA 11:00Z','Notify: progress · #29420','Notify: progress · #29420 · record 6599000001 · ETA 11:00Z'].every((l)=>{constu=readMailboxLine(MSG(l,'triage'));returnu.state==='unreadable'&&/doesnotspell/.test(u.reason);});})());
2872
2890
t('⭐ every key of the table is named, as `Kind: key` in a code span, in the protocol text (`references/project-forms.md` 〈信箱 PR〉), and that section names NO other key — the table in both directions — and points at `--mailbox-keys` for the full spellings',(()=>{constforms=readFileSync(newURL('../../.claude/skills/pm-dispatch/references/project-forms.md',import.meta.url),'utf8');constat=forms.indexOf('\n## 信箱 PR\n');constsection=at<0 ? '' : forms.slice(at+1).split(/\n##/)[0];constnamed=newSet([...section.matchAll(/`(Request|Notify):([a-z-]+)`/g)].map((m)=>`${m[1]}: ${m[2]}`));consttable=newSet(MAILBOX_KEYS.map((k)=>`${k.kind}: ${k.key}`));returnsection!==''&&named.size===table.size&&[...table].every((k)=>named.has(k))&§ion.includes('--mailbox-keys');})());
0 commit comments