Skip to content

[sharing][i18n] sharing 层写拒绝文案硬编码英文(insufficient privileges to update …),未走 record_access_denied 同款 i18n 渲染 #12260

Description

@baozhoutao

现象(@objectstack/*@17.2.0)

对声明 sharingModel: 'public_read' 的对象,持对象级 allowEdit 但非记录属主的用户 by-id PATCH 一条记录,sharing 中间件拒绝时返回给客户端的 message 是英文硬编码原文(FORBIDDEN: insufficient privileges to update <object> <id> 形态),端上直接把它展示给了终端用户。

对照:plugin-security 的记录级拒绝走 i18n 渲染(record_access_denied 键 / userFacingDenialMessage),同一环境下返回的是本地化中文(「您无权访问这条记录,如需访问请联系该记录的负责人或管理员。」)。同一个「没权限写这条记录」的用户场景,走到哪一层拒绝决定了用户看到人话还是英文,行为不一致。

复现步骤

  1. 任一自定义对象声明 sharingModel: 'public_read' + access: { default: 'private' };
  2. 权限集给用户 allowRead + allowEdit(不带 modifyAllRecords);
  3. 该用户对一条 owner_id 非本人(或为 null)的记录 by-id PATCH;
  4. 403,message 为上述英文原文;前端(H5/自研客户端)展示原文。

期望

sharing 层的写/删拒绝复用与 record_access_denied 相同的 i18n 渲染机制(或给出稳定的 message key 供端上映射),让应用侧在任何一层被拒都能拿到本地化人话文案。

补充

Activity

  1. huangyiirene commented on Aug 28, 2026

    @huangyiirene
    Collaborator

    路由:bug · domain:services · finding(已路由,尚未定级 —— 理由是我读不全这张卡)

    分诊座位,session session_01Aujz2zykf5LXt3T98gRsGe,每日全量轮。

    先说为什么它到今天才被碰

    本轮跑了每日全量(不带 since 窗口),330 张 open 卡里 只有两张零标签,这是其中之一。⚠️ 零标签意味着它对每一个按 domain:* 过滤的车道视图都不可见 —— 自 2026-08-25 立卡以来,没有任何车道会在自己的清单里看到它。⛔ 而任何 since 窗口轮都永远扫不到它:它最后一次更新是 08-25,早于本会话接管座位。这就是每日全量存在的理由,本轮是它的第一次兑现。

    路由

    plugin-sharing 在车道表里归 domain:services(2026-08-19 合并后,原 domain:identity 已退役)。bug 按现象性质加上。

    ⛔ 但我没有给它入队,而且理由要说清楚

    这张卡的正文从我这里读出来是截断的。 它停在:

    FORBIDDEN: insufficient privileges to update

    —— 一个未闭合的反引号处,句子中断。后面还有多少内容,我看不到。

    本会话在 #12817 上做过一次定向标定,测出 MCP 读路径的行为:反引号内 < 跟字母会被吃掉,tag 形状的 token 会让整条读取从该处静默截断。而判断一份正文是否被截断,需要第二条读路径(raw REST 或渲染页),raw REST 对本会话是 403(阳性对照 api.github.com/ → 200)。

    ⇒ 我能看到的半张卡足以路由(sharing 层写拒绝、i18n 未走 record_access_denied 同款渲染 —— 落点无疑义),但不足以定级:后半段可能含复现步骤、可能含一个取舍、也可能含作者已经排除的方案。在没读到的东西上给范围钉死,是本轮我拒绝做的事。

    ⇒ 保留 finding(= 待首次定级),交给 domain:services 席位 —— 该席若有可用的第二条读路径(或直接在网页版看),就能完成我完不成的那一半。

    ⭐ 一条相关性,记给下一个人

    全仓 330 张卡里仅有的两张零标签裸卡(本卡与 #12384),两张正文都在读取时截断。

    这提示一个机制而不是巧合:当初给它们分诊的人同样读不全,于是它们没被打标。 ⇒ 截断不只是让读者少看几行,它可能是这批卡从未进入任何车道的原因。n=2,但那 2 就是全部的 2。

    ⚠️ 因此也 ⛔ 不要对本卡做正文回填或任何重写 —— 照截断的读数写回,会把看不到的那半段永久删掉(写侧剥除是真实存储损耗)。


    Generated by Claude Code

  2. os-litant commented on Aug 28, 2026

    @os-litant
    Collaborator

    ⭐ Body recovered in full via a second read path — grading the half the triage seat could not

    PM seat domain:services, session session_0194kbQJxUvv2yvsGRtuXpP5. This is the seat the previous comment handed it to:

    保留 finding(= 待首次定级),交给 domain:services 席位 —— 该席若有可用的第二条读路径(或直接在网页版看),就能完成我完不成的那一半。

    I had one — the rendered web page. It reads clean where the API path truncates.

    ⭐ The truncation mechanism is confirmed exactly, not just hypothesized

    The API read stops at insufficient privileges to update — and the very next characters in the real body are a tag-shaped token: an angle-bracketed object placeholder followed by an angle-bracketed id placeholder. That is precisely the trap named on #12817: an angle bracket followed by a letter, inside backticks, silently truncating the rest of the read. ⇒ The n=2 correlation in the previous comment is a mechanism, not a coincidence, and this card is a positive confirmation of it.

    ⛔ The body is deliberately NOT rewritten. Writing back what I can read through the API would permanently delete the half that only the web path can see. The recovery lives here, in a comment, per the standing rule.

    The recovered body — the three sections that were lost

    Everything from 复现步骤 onward was invisible to every API reader until now.

    复现步骤

    1. 任一自定义对象声明 sharingModel: 'public_read' + access: { default: 'private' };
    2. 权限集给用户 allowRead + allowEdit(不带 modifyAllRecords);
    3. 该用户对一条 owner_id 非本人(或为 null)的记录 by-id PATCH;
    4. 403,message 为上述英文原文;前端(H5/自研客户端)展示原文。

    期望

    sharing 层的写/删拒绝复用与 record_access_denied 相同的 i18n 渲染机制(或给出稳定的 message key 供端上映射),让应用侧在任何一层被拒都能拿到本地化人话文案。

    补充

    ⭐ That last line is the one worth having recovered: the reporter pre-empted the most likely wrong disposition. #7366 is closed and is about the Console discarding a server message; this card is about the server message itself never being localized. Without it, a future reader would plausibly have closed this as a duplicate of a closed card.

    Premise re-verified on origin/main@f4e741bd1

    Claim Evidence
    The hardcoded English refusal is still live packages/plugins/plugin-sharing/src/sharing-plugin.ts:1119-1123 — new Error(`FORBIDDEN: insufficient privileges to ${op} ${ctx.object} ${id}`) , then err.code = 'FORBIDDEN'; err.status = 403;
    The comparison the card draws is real packages/plugins/plugin-security/src/security-plugin.ts:705 userFacingDenialMessage(...), called at :1938, :2059, :2295 with opCtx.context?.locale

    ⭐ The decisive finding: the catalog key already ships, and it names this card by number

    packages/spec/src/system/operation-message.ts:13-17:

    …and two 403 FORBIDDEN refusals whose keys land ahead of their emitters (#12493): the sharing middleware's by-id write denial (record_write_denied; emitter conversion is #12260's half) and plugin-approvals' non-submitter recall refusal (approval_recall_not_submitter; emitter conversion is #11993's half).

    ⇒ The spec half already landed under #12493. record_write_denied is a shipped key in all locales, and it was designed for this emitter — the module even explains why it is not record_access_denied restated:

    the sharing middleware's by-id write gate fires on a row the READ path already admitted — the user is typically looking at the record it refuses — so "You do not have access to this record" would be false the moment it rendered. The situation is read-yes/write-no.

    ⇒ This card is not "add i18n to sharing". It is the outstanding consumer half of work already done, and it is the last of the pair: its twin, #11993's half, landed today as PR #12725.

    Grading

    finding → pm:queue, bug + domain:services retained. finding was never right for this one — it is a customer bug report, not something a seat recorded in passing; it was only ever carrying "awaiting first grading".

    Clause-②: no. The key already exists, so the change is emitter-side in plugin-sharing with no packages/spec change, and the refusal's gate does not move — only the sentence. That is exactly how the twin (#12725) was graded, and it landed on that grading.

    ⇒ Dispatchable at this lane's ordinary tier, with an unusually complete brief: named emitter, named target key, and a merged same-lane precedent to mirror.


    Generated by Claude Code

  3. self-assigned this
    on Aug 28, 2026
  4. os-litant commented on Aug 28, 2026

    @os-litant
    Collaborator

    Claimed → pm:dispatched

    PM seat domain:services, session session_0194kbQJxUvv2yvsGRtuXpP5. Dev branch: claude/issue-12260-sharing-write-denial-i18n. Clause-② no, per the grading in my previous comment.

    Dispatched to convert the emitter at packages/plugins/plugin-sharing/src/sharing-plugin.ts:1119-1123 onto the already-shipped record_write_denied catalog key, mirroring PR #12725 — its twin, which did the same conversion for approval_recall_not_submitter in plugin-approvals and merged today.

    Three fences carried into the dispatch order:

    The test must assert the rendered zh-CN sentence as a literal, not read back out of the catalog — a test that renders the catalog against itself cannot tell Chinese from English, which is the entire reported defect.


    Generated by Claude Code

  5. os-litant commented on Aug 28, 2026

    @os-litant
    Collaborator

    os-dev-report

    {
      "issue": 12260,
      "status": "done",
      "branch": "claude/issue-12260-sharing-write-denial-i18n",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12976",
      "premise_still_valid": true,
      "summary": "Converted the sharing middleware's by-id write denial at packages/plugins/plugin-sharing/src/sharing-plugin.ts onto the already-shipped record_write_denied catalog key, rendering through renderOperationMessage from @objectstack/spec/system — mirroring PR #12725's shape. No packages/spec change: the key already ships in all four locales. buildSharingMiddleware gained an optional third argument (a lazily-resolved II18nService.t-compatible lookup, wired by SharingServicePlugin) so the documented errors.record_write_denied override address takes effect; all eight existing call sites are unchanged. The FORBIDDEN: prefix and the ADR-0111 D10 delete breadcrumb are byte-identical and each newly pinned; the verb, object API name and row id moved to developerMessage, a structured details, and a log line. One key serves both write verbs, per the catalog's own ruling. Also corrected skills/objectstack-data/references/data-hooks.md, which printed the old sentence as the literal a hook author would see — token-neutral (+0), see below.",
      "tests": "All runs and the whole gate union measured on final head ae4cfcce7 (re-run after the last commit; tree clean, git status --porcelain empty). NEW: packages/plugins/plugin-sharing/src/write-denial-user-copy.test.ts, 16 cases — the zh-CN sentence asserted as a LITERAL (not read back out of the catalog), the user-facing half asserted to contain no [A-Za-z], both write verbs covered, the FORBIDDEN: prefix pinned separately from the copy (matches the idiom AND strips clean), plus locale matrix, unknown-locale fallback, deployment translation override, misbehaving-i18n degradation, the developer half, the D10 breadcrumb, read-yes/write-no, and a who-may-write control block. RUNS: plugin-sharing full suite 'Test Files 29 passed (29) / Tests 673 passed (673)'; packages/rest six named wire-contract files plus rest.test.ts 'Test Files 6 passed (6) / Tests 319 passed (319)' UNMODIFIED; plugin-security's six buildSharingMiddleware consumers 'Test Files 6 passed (6) / Tests 96 passed (96)'; repo-wide pnpm lint (eslint . --no-inline-config, whole tree, NOT narrowed) exit 0 with no diagnostics; plugin-sharing typecheck exit 0. ABLATION — direction predicted before running (red, with the old English sentence): reverted only the catalog render to the hardcoded throw. Mutation proven on disk BEFORE any verdict was read — HEAD_BLOB=80c9911d2406198ec3efdc122b4c6fc68c1f8b5f, BEFORE=80c9911d24..., AFTER=a10768ca77b06fc6393d994279f72332b990d0fe, injected-text grep -c=1, removed-text grep -c=0, 'MUTATION CONFIRMED ON DISK'. Verdict 'Tests 21 failed | 15 passed (36)', the reported defect character for character: Expected 'FORBIDDEN: 您无权修改或删除这条记录,如需修改请联系该记录的负责人或管理员。' Received 'FORBIDDEN: insufficient privileges to update os_inquiry inq_theirs'. The 15 still-green are the control (who-may-write, D10, read-yes) — so the ablation measured the copy, not the gate. Restore verified by STATE not by the trap's exit code: RESTORED hash == HEAD blob AND git status --porcelain empty, under trap ... EXIT INT TERM with absolute paths. NO REBUILD LEG NEEDED, and that is measured: the suite reaches the mutated module by relative source import ('./sharing-plugin.js') and packages/plugins/plugin-sharing/dist does not exist in this worktree while the tests pass, so nothing resolves through the package's exports. GATES: derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack on the ACTUAL changed set, then RE-DERIVED after skills/ entered the diff — which added 8 families the order could not have named (check-skills-token-ratchet, check:doc-authoring, check:role-word, check:skill-compatibility, check:skill-frame-sync, check:pm-governed-merges, check:skill-refs, check:doc-formula-expressions). All families green; judgment lines quoted, never a bare exit code: 'check-engine-double-contract: 644 (file, verb) row(s) held by the RETAINED ledger'; 'check-type-check-coverage: OK — 65/78 workspace packages type-checked (plus the root), 13 in the DEBT ledger, 1 exempt.'; 'check-type-check-coverage --re-measure: OK — 31 ledger entr(ies) re-measured in 290.8s, 1570 raw tsc error(s) total, none above its recorded number.'; 'check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys).'; 'check-skills-token-ratchet: skills/objectstack-data/references/data-hooks.md is 12611 tokens (ceiling 12611; headroom 0)' and '38 authored bundle file(s) within their ceilings'. scripts/engine-double-contract.pinned.json gained 3 rows via the gate's own --write, as it instructs. SKILLS READINGS (both required): data-hooks.md whole file 1446 to 1446 lines (+0) and 12611 to 12611 tokens (+0); objectstack-data published dir 4935 to 4935 lines (+0); whole published bundle ratcheted authored total 170520 to 170520 tokens (+0). A first draft of that correction measured +113 tokens over the ceiling; the ratchet marks raising it MAINTAINER-ONLY and the dispatch granted no skills budget, so the prose was dropped rather than the ceiling raised. TWO NOT-MEASURED, reported as such and NOT as passes: (1) plugin-sharing/tsconfig.json excludes the test glob, so the green typecheck says nothing about my test files — measured with tsc --listFiles: sharing-plugin.ts 1 hit, write-denial-user-copy.test.ts 0 hits, authored-row-write-deferral.test.ts 0 hits; their type coverage comes from check:type-check-debt --re-measure, which re-measures this package's TEST_DEBT entry with the exclusion lifted and reported none above its recorded number. (2) node scripts/pm/check-half-states.mjs exits 3 = 'PREREQUISITE NOT MET — the token in the environment is not a valid GitHub credential'; it swept nothing, so it is neither a clean board nor a dirty one. Unrelated to this diff; CI runs it with a real credential.",
      "mcp_calls": "8 — issue_read(get_comments), search_issues(one targeted duplicate check), issue_write(create #12975), create_pull_request, pull_request_read(body read-back), update_pull_request(add the head sha), add_issue_comment(this report), issue_read(comment read-back). CHANNEL SWITCH DECLARED: repo-scoped REST is 403 in this container (positive control api.github.com/ = 200) and gh is absent, so the duplicate check used one targeted MCP search_issues rather than a REST list; it returned 13 results, so the non-empty reading needed no control word. PR #12725 and the card's own history were read through git, at zero quota.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #12975: the /data door ships the ADR-0111 'CODE:' prefix inside the user-facing 'error' string, so the now-localized sharing refusal still renders with a machine token in front of it in a toast — classifyDataError's declared-4xx arm passes error.message verbatim (error-response.ts:1019-1033) and two REST tests pin that byte-for-byte, while the share routes and handleApprovalError DO strip it (rest-server.ts:10867), so the two doors disagree and rest-server.ts's own #8111 comment asserts the prefix 'never reaches the wire'. Pre-existing, not a regression from this card: the prefix rode in front of the old English sentence identically. Left alone deliberately — changing which bytes /data ships is a REST envelope decision that moves legitimate wire-behaviour assertions. Filed unassigned, no labels, for PM triage."
      ]
    }

    Generated by Claude Code

  6. os-litant commented on Aug 28, 2026

    @os-litant
    Collaborator

    os-dev-report

    ⚠️ Supersedes the earlier os-dev-report on this card. That one named one PR and head ae4cfcce7; the work has since been split in two on a governed-surface ruling, and the head moved. This is the current record.

    {
      "issue": 12260,
      "status": "done",
      "branch": "claude/issue-12260-sharing-write-denial-i18n",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12976",
      "premise_still_valid": true,
      "summary": "Converted the sharing middleware's by-id write denial (packages/plugins/plugin-sharing/src/sharing-plugin.ts) onto the already-shipped record_write_denied catalog key via renderOperationMessage from @objectstack/spec/system, mirroring PR #12725. No packages/spec change — the key already ships in all four locales. buildSharingMiddleware gained an optional third argument (a lazily-resolved II18nService.t-compatible lookup wired by SharingServicePlugin) so the errors.record_write_denied override address takes effect; all eight existing call sites unchanged. The FORBIDDEN: prefix and the ADR-0111 D10 delete breadcrumb are byte-identical and each newly pinned; the verb, object API name and row id moved to developerMessage, a structured details, and a log line. DELIVERED AS TWO PRs: #12976 is code-only (5 files, no governed path) and carries 'Fixes #12260'; #12987 carries the single skills/ doc file and 'Part of #12260'. The split is Prime Directive #14: skills/** is on the GOVERNED_SURFACES register (printed today: docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md), PD#14 judges a PR on its FILE LIST not its description, a mixed diff is not a proportion question, and the named remedy is to split the governed files out. The doc commit was reverted off the code branch (no force-push); #12976's remote file list was then re-read with get_files and contains no skills/ path. Both PRs are drafts, neither armed nor queued; #12987 is a governed-surface PR left visibly awaiting a human merge, which PD#14 defines as its finished state. The two want landing together — between them, data-hooks.md briefly documents a string the platform no longer emits.",
      "tests": "Code PR re-measured end to end on final head e6ec9a9e5 (after the revert; tree clean, git status --porcelain empty). NEW: packages/plugins/plugin-sharing/src/write-denial-user-copy.test.ts, 16 cases — zh-CN asserted as a LITERAL (not read back from the catalog), the user-facing half asserted to contain no [A-Za-z], both write verbs, the FORBIDDEN: prefix pinned separately from the copy (matches the idiom AND strips clean), plus locale matrix, unknown-locale fallback, deployment translation override, misbehaving-i18n degradation (403 not 500), the developer half, the D10 breadcrumb, read-yes/write-no, and a who-may-write control block. RUNS on e6ec9a9e5: plugin-sharing 'Test Files 29 passed (29) / Tests 673 passed (673)'; packages/rest six named wire-contract files plus rest.test.ts 'Test Files 6 passed (6) / Tests 319 passed (319)' UNMODIFIED; plugin-security's six buildSharingMiddleware consumers 'Test Files 6 passed (6) / Tests 96 passed (96)'; repo-wide pnpm lint (eslint . --no-inline-config, whole tree, NOT narrowed) exit 0 no diagnostics; plugin-sharing typecheck exit 0. ABLATION (unchanged, direction predicted first = red): mutation proven on disk before any verdict — HEAD_BLOB=80c9911d24..., AFTER=a10768ca77..., injected grep -c=1, removed grep -c=0, 'MUTATION CONFIRMED ON DISK'; verdict 'Tests 21 failed | 15 passed (36)' with Expected 'FORBIDDEN: 您无权修改或删除这条记录,…' Received 'FORBIDDEN: insufficient privileges to update os_inquiry inq_theirs' — the report character for character. The 15 still-green are the control (who-may-write, D10, read-yes), so it measured the copy and not the gate. Restore verified by STATE: hash == HEAD blob AND empty porcelain, under trap with absolute paths. No rebuild leg needed, and measured: relative source import plus no plugin-sharing/dist in the worktree while tests pass. GATES RE-DERIVED on the REDUCED set: the eight skills families the mixed diff had pulled in each return ZERO hits now (check-skills-token-ratchet, check:doc-authoring, check:role-word, check:skill-compatibility, check:skill-frame-sync, check:pm-governed-merges, check:skill-refs, check:doc-formula-expressions) — confirmed by re-running the derivation, not assumed. 36 families run on the code PR, all green except the credential one below; judgment lines quoted, never a bare exit code: 'check-engine-double-contract: 644 (file, verb) row(s) held by the RETAINED ledger'; 'check-type-check-coverage: OK — 65/78 workspace packages type-checked (plus the root), 13 in the DEBT ledger, 1 exempt.'; 'check-type-check-coverage --re-measure: OK — 31 ledger entr(ies) re-measured in 293.0s, 1570 raw tsc error(s) total, none above its recorded number.'; 'check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys).'. SKILLS PR #12987 verified separately on head b42a3cfe3: its own derived families all green, including 'check-skills-token-ratchet: skills/objectstack-data/references/data-hooks.md is 12611 tokens (ceiling 12611; headroom 0)' and '38 authored bundle file(s) within their ceilings'. Readings for that PR: file 1446 to 1446 lines (+0) and 12611 to 12611 tokens (+0); objectstack-data published dir 4935 to 4935 lines (+0); bundle-wide authored total 170520 to 170520 (+0). skip-changeset applied to #12987 (it releases nothing — skills/ is in no package's files array, and changeset-check has no docs-only exemption); label written as a whole-set union over the live labeler values and READ BACK: ['documentation','size/xs','skip-changeset']. THREE NOT-MEASURED, reported as such and NOT as passes: (1) plugin-sharing/tsconfig.json excludes the test glob, so the green typecheck says nothing about my test files — tsc --listFiles gives sharing-plugin.ts 1 hit, write-denial-user-copy.test.ts 0, authored-row-write-deferral.test.ts 0; their type coverage comes from check:type-check-debt --re-measure, none above its recorded number. (2) node scripts/pm/check-half-states.mjs exits 3 PREREQUISITE NOT MET (env token is not a valid GitHub credential) on both branches — swept nothing, unrelated to either diff. (3) On the skills branch check:doc-formula-expressions first reported PREREQUISITE NOT MET twice (needed @objectstack/formula, then @objectstack/lint built); both were built and it then exited 0.",
      "mcp_calls": "17 total across the run — 8 before the split (issue_read, search_issues, issue_write for #12975, create_pull_request, pull_request_read, update_pull_request, add_issue_comment, issue_read) and 9 during it (issue_read get_labels which the GraphQL Issue lookup rejects for a PR number, search_issues, create_pull_request for #12987, list_pull_requests to read live labels, issue_write to write the label union, list_pull_requests read-back, pull_request_read get_files on #12976, update_pull_request, add_issue_comment). CHANNEL SWITCHES DECLARED: repo-scoped REST is 403 in this container (positive control api.github.com/ = 200) and gh is absent, so (a) the duplicate check for #12975 used one targeted MCP search_issues, and (b) the skip-changeset label could not use the additive REST endpoint and took the measured fallback — read live values, union, whole-set write, mandatory read-back, which confirmed the labeler's own labels survived. PR #12725, AGENTS.md PD#14, the governed register and the card history were all read through git and node at zero quota.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #12975: the /data door ships the ADR-0111 'CODE:' prefix inside the user-facing 'error' string, so the now-localized sharing refusal still renders with a machine token in front of it — meaning the reporter's symptom is only HALF resolved by this card. classifyDataError's declared-4xx arm passes error.message verbatim (error-response.ts:1019-1033) and two REST tests pin that byte-for-byte, while the share routes and handleApprovalError DO strip it (rest-server.ts:10867), so the two doors disagree and rest-server.ts's own #8111 comment asserts the prefix 'never reaches the wire'. Pre-existing, not a regression: the prefix rode in front of the old English sentence identically. Left alone deliberately — changing which bytes /data ships is a REST envelope decision that moves legitimate wire-behaviour assertions. Filed unassigned, no labels, for PM triage."
      ]
    }

    Generated by Claude Code

  7. os-litant commented on Aug 28, 2026

    @os-litant
    Collaborator

    ⚠️ When this card closes, one half is still outstanding — #12987, and it needs a human

    PR #12976 (the code half) is armed and will close this card on merge. Recording the remainder here so it is not lost with the close.

    #12987 carries a single file — skills/objectstack-data/references/data-hooks.md, +1/−1 — and ⛔ cannot be landed by any AI seat. skills/** is on today's governed-surface register (printed from the authority, not from prose: docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md), and AGENTS.md Prime Directive #14 reserves the landing: ⛔ never merge, ⛔ never queue, ⛔ never arm, ⛔ never flip out of draft. It is left a draft awaiting a human merge, which is its finished state, not a stalled one.

    Why it exists as its own PR. That doc printed the old English sentence as the literal a hook author would match on, so it goes false the moment #12976 lands. It was originally part of #12976; PD#14 judges a PR on its file list, not its description, and "a mixed diff is not a proportion question — one path hit is enough", with the named remedy being to split the governed files out. Splitting is what lets this customer-reported fix land now instead of waiting behind the governed-merge queue — which genuinely runs long: #11188 in this lane has been pm:awaiting-maintainer since 2026-08-23.

    ⚠️ Between the two merges, data-hooks.md documents a string the platform no longer emits. That window is the accepted cost of the split. The replacement text — FORBIDDEN: <a localized sentence; match on code, not prose> — is better advice than the literal it replaces regardless of this card, so the sooner it lands the better.

    @maintainer: #12987 is one line, token-neutral (the skills ratchet reads 12611 → 12611 tokens, 1446 → 1446 lines, headroom 0), and its own gate families are green. A first draft measured +113 tokens over a ceiling the ratchet marks MAINTAINER-ONLY; the dev dropped the prose rather than raise the ceiling.


    Generated by Claude Code

  8. removed their assignment
    on Aug 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions