Skip to content

[finding] Adding a NEW hand-written page under content/docs/ makes a PR human-merge-only — always, and invisibly until the gate runs #13591

Closed

Description

@claude

[finding] Adding a genuinely NEW hand-written page under content/docs/ makes a PR human-merge-only — always, and invisibly until the gate runs

Filed by the domain:devx PM seat (post #6023, session session_01Pk26oZ12t5N1hwGW1m1MgC), R33, from evidence produced on #13548 / PR #13584. ⛔ Ungraded, unassigned.

The property

check:docs-audit-scope enforces a two-way equality between the set of hand-written pages under content/docs/** and the ALL_HANDWRITTEN list that lives inside .claude/workflows/docs-accuracy-audit.js.

.claude/** is governed surface (register printed today: docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md). Prime Directive #14 judges a PR on its file list, and "a mixed diff is not a proportion question — one path hit is enough."

⇒ Adding a new page forces an edit to a governed file, so the whole PR becomes human-merge-only: ⛔ never armed, ⛔ never queued. Modifying an existing page does not, because it is already listed.

Measured, not inferred

On PR #13584 (a 7-file, +1928 diff whose only governed line is a single ALL_HANDWRITTEN entry):

run result
non-governed six files alone exit 1 — + 1 hand-written doc(s) exist but are not listed
the one governed line alone exit 1 — - 1 listed path(s) do not exist
both together exit 0 — scope is in sync with content/docs/: 190 hand-written doc(s)

⇒ the AGENTS.md remedy for a mixed diff — "split the governed files into their own PR" — cannot be applied: it produces two PRs each red on a required gate, the governed one red until the other merges. An ordering deadlock, not a sequencing inconvenience.

Control, confirming this is about ADDING rather than about docs work generally: 2be4a6729 — the isSystem census commit, the pattern #13548 was told to mirror — touched 5 files, none under .claude/, because system-context.mdx was already listed in its parent (grep count 1).

Why the existing escape hatches do not apply

Why this is worth a card

⚠️ It is invisible at dispatch time. A PM reading a card that says "write a new documentation page" sees no governed path in any file list; the governed hit only materialises when the gate runs, after the work is done. ⇒ no dispatch fence can be written for it, and every such card silently costs a human merge that nobody budgeted.

⚠️ And the direction is wrong for the fleet: the governed-surface rule exists so that agent-instruction and policy files get human eyes. A new customer documentation page is not that — it acquires the ceremony incidentally, through a bookkeeping list that happens to live in .claude/.

Options, ⛔ no decision claimed

  1. Accept and document it — write the property into the gate's header and the dispatch playbook so it is at least predictable and budgeted. Cheapest; changes nothing.
  2. Move ALL_HANDWRITTEN out of .claude/ into a non-governed artifact the node:vm body can be handed at build time. ⚠️ Non-trivial: the vm sandbox is the reason it is inline, and docs-accuracy-audit.js 的 ALL_HANDWRITTEN 清单 11 条全部指向改名前的 protocol/objectos/* —— 内部文档审计静默漏审整个 protocol/kernel 目录 #4851's "one subject, two hand-kept lists" bill has to stay paid.
  3. Carve .claude/workflows/** out of the governed register. ⛔ Narrowing the governed surface is a governance change and squarely a maintainer decision — noted for completeness, ⛔ not recommended by this seat.

Refs: #13548 / PR #13584 (the evidence) · #4851, #4920 (why the list is inline and un-excludable) · Prime Directive #14 · 2be4a6729 (the ADD-vs-MODIFY control)


Generated by Claude Code

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 31, 2026
  2. added
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    domain:skillsDomain lane — seat card indexed by label:pm:seat
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 31, 2026
  3. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    分诊定级 → domain:skills · needs-user-decision · p2 · tooling。摘 finding。

    锚定。 三条选项全部落在治理面或其治理规则上:选项 2 改 .claude/workflows/docs-accuracy-audit.js(governed),选项 3 改 governed 登记本身,选项 1 改门禁头注释 + 派单教材。⇒ domain:skills。

    载重事实已复核,卡片属实:

    ALL_HANDWRITTEN 出现在:  .claude/workflows/docs-accuracy-audit.js   ← governed
                            scripts/docs-audit/check-audit-scope.mjs   ← 门禁
                            scripts/pm/check-governed-merges.mjs
    governed 登记(check-governed-merges.mjs:86,维护者批准的统一清单):
                            「docs/adr/** + .claude/** …」
    

    ⇒ 新增一个手写文档页必须同时改一个 governed 文件,Prime Directive #14 按文件清单判定且「一处命中即可」⇒ 整个 PR 变成人工合并、⛔ 永不武装、⛔ 永不入队。

    为什么交你裁而不是我判

    ⛔ 缩小 governed 面是治理变更,恒交维护者 —— 选项 3 直接是这件事。选项 2 也不干净(vm 沙箱正是清单内联的原因,且 #4851「一个主题两份手工清单」的账必须继续付)。选项 1 什么都不改。三条里没有一条是分诊能替你拿的。

    四维分析

    ① 实际业务需求. 拉动是实的且已量化:PR #13584 上三次运行给出了排序死锁的证据 —— 非治理六文件单独跑 exit 1(+1 hand-written doc(s) exist but are not listed),那一行治理改动单独跑也 exit 1(-1 listed path(s) do not exist),两者合并才 exit 0。⇒ AGENTS.md 对混合 diff 给的补救(「把治理文件拆成单独 PR」)在这里物理上不可执行:拆完是两个都红的 PR,治理那个红到另一个合并为止。这不是排序不便,是死锁。

    ② 项目长远合理性. ⭐ 卡片的核心论证在这里,而且我认为它成立:governed 面规则的存在,是为了让agent 指令与政策文件得到人眼审查。一个新的客户文档页不是那种东西 —— 它是通过一份恰好住在 .claude/ 里的记账清单,附带地获得了这份礼节。⇒ 现状是一次范畴错误,而不是一个严格性选择。

    ③ 防 AI 写代码犯错. ⚠️ 本案最重的一轴,而且方向出乎意料。 这条税在派单时不可见:PM 读一张「写一篇新文档页」的卡,任何文件清单里都看不到 governed 路径 —— 治理命中只在工作做完、门禁跑起来之后才具体化。⇒ 没有任何派单围栏能为它写出来,每一张这样的卡都在静默地花掉一次没人预算过的人工合并。一条无法被事前看见的规则,agent 无法遵守它,只能撞上它。

    ④ 创业阶段不扩散需求. 成本差极大:选项 1 是两处文本(门禁头 + 派单教材),零机制;选项 2 要把清单搬出 vm 沙箱,而沙箱的 globals 不含 require/import/任何文件系统访问 —— 非平凡,且要重新安排 #4851 那笔账;选项 3 是一行登记改动,但代价是治理面的语义。⇒ 若只按扩散度选,选项 1 胜;但它把 ③ 轴那条不可见性原样留着。

    建议:选项 1 + 一处 ③ 轴的补丁。 即 —— 接受这条属性并写进门禁头与派单教材(最省、零风险),同时让它在派单时可见:check:docs-audit-scope 或派单闸门在检测到卡片会新增 content/docs/** 页时,提前声明「本卡将命中 governed 面,需人工合并」。这样 ③ 轴的伤害(不可预算)被消掉,而 ② 轴的范畴错误被如实记录下来,留待有人真的想付选项 2 或 3 的代价时再动。

    ⚠️ 我不推荐选项 3,与立卡席位一致:为了一份记账清单去缩小 governed 面,是用治理语义换便利。

    ⚠️ 需要你连带答一句:若你倾向选项 1,那么「一张会新增文档页的卡默认带人工合并成本」应当成为派单时的既定事实(写进 pm-dispatch 教材),还是每次由 PM 现场判断?前者可预算,后者更灵活但会反复重新发现。

    定级 p2:反复计费、且在派单时不可见;但无正确性伤害、无安全后果,现状可运转(只是每次要一次人工合并)。

    Refs:#13548 / PR #13584(证据)· #4851、#4920(清单为何内联且不可豁免)· Prime Directive #14 · 2be4a6729(ADD 与 MODIFY 的对照)。


    Generated by Claude Code

  4. claude commented on Sep 1, 2026

    @claude
    ContributorAuthor

    os-dev-report

    {
      "issue": 13591,
      "status": "done",
      "branch": "claude/issue-13591-handwritten-list-degovern",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/14085",
      "premise_still_valid": true,
      "summary": "Option 2 implemented as ruled; the pre-authorized option-1 fallback was NOT taken, because runner-side injection measured FEASIBLE. Measurement: the runner is the Claude Code Workflow tool (not in this repo, not modifiable here); its documented invocation surface offers exactly one channel into the node:vm sandbox — the `args` global, delivered 'verbatim' from the invocation, beside log/phase/console/budget/timers and agent/parallel/pipeline/workflow, with codeGeneration disabled and no filesystem. The earlier reading ('the list has to be inline') missed that the BODY does not have to do the reading: the caller reads the file outside the sandbox and hands the list in as data, so the sandbox stays exactly as FS-blind as before. Single source chosen: scripts/docs-audit/handwritten-docs.json — beside its generator (check-audit-scope.mjs --write) and its derivation (affected-docs.mjs), verified against the governed register (docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md) as outside every governed glob, and unable to be mistaken for docs content or swept into the docs-site build. Its readme block states it is THE single source, names both consumers, and records that it is deliberately non-governed by ruling. Assumption A holds: ZERO copies remain — the inline array and its generated: markers are gone from the vm body (190 path literals removed), the gate reads the artifact, and a self-test pin asserts no inline copy can return. Assumption B: check-governed-merges.mjs referenced ALL_HANDWRITTEN through the #9866/#10277 provenance row (GENERATED_SURFACE_EXCEPTIONS[0]) that lifted the governed fork for .claude/workflows/docs-accuracy-audit.js on a byte-exact recompute; that row is RETIRED, because the 2026-09-01 ruling removes its subject — there is no generated artifact at that path any more, and a row still naming it could only ever be asked to certify hand-authored instruction prose. Retiring it is a NARROWING (nothing governed became clear) and GOVERNED_SURFACES is character-for-character unchanged — option 3 was not touched. One shape considered and rejected: sourcing the scope from an AGENT (agents do have a filesystem) would make the scope a self-report with nothing to reconcile it against, re-creating #4851 unverifiably. There is deliberately no 'omit args = audit everything' invocation any more and there cannot be; the body refuses by name. NOTE FOR THE MAINTAINER, not a premise failure: the card's 'always human-merge-only' was true of Prime Directive #14's file-list judgment but NOT of the automated predicate — the #9866 row had lifted --test and the queue guard for a pure regeneration since 2026-08-20/22. The load-bearing premise (the list lives inline in a governed file; every page-adding PR must edit it) was verified intact on today's origin/main before any edit.",
      "tests": "All at HEAD 8a82109c4 (union re-derived AFTER the last commit). Exit codes captured by redirect-then-read, never through a pipe. || PREMISE (before edits, at origin/main c56592bc2): all three sites present; `node scripts/docs-audit/check-audit-scope.mjs` EXIT=0 '190 hand-written doc(s)'; `--self-test` EXIT=0 24 cases; `check-governed-merges.mjs --self-test` EXIT=0 253 assertions. || DERIVED GATE UNION: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` at 8a82109c4 -> 8 changed paths, 39 commands, run under scripts/pm/os-verify-lock.sh (queued 1s, held 421s). 35 exit 0; 4 NOT MEASURED, 0 findings: check-test-completeness.mjs exit 3 PREREQUISITE NOT MET (grades a saved turbo test log, none named); check-governed-queue-guard.mjs exit 1 'could not read GITHUB_EVENT_PATH' (reads a workflow event payload); check:doc-formula-expressions exit 3 PREREQUISITE NOT MET (@objectstack/formula not built); check:type-check-debt exit 3 PREREQUISITE NOT MET (--re-measure needs 56 built dependency closures; its --self-test and the coverage half both passed). || MANDATED EXTRAS: `pnpm check:docs-audit-scope` EXIT=0 (3 lines: scope in sync 190 docs · 9 release-owned pages read-only · scope injection is live); `check-audit-scope.mjs --self-test` EXIT=0 32 cases (was 24); `check-governed-merges.mjs --self-test` EXIT=0 241 assertions; `check-governed-queue-guard.mjs --self-test` EXIT=0 129 cases; `check:pm-governed-prose` EXIT=0 (2 instruction surfaces name all 5 registered surfaces, claim no others); `check-nul-bytes` EXIT=0 (7700 files, no raw control bytes) plus a manual grep -naP scan of the PR body. || BOTH-DIRECTION MUTATION TEST of check:docs-audit-scope, run from a COMMITTED state, restore legs blob-hash-proven (artifact HEAD blob dc81dcb245a6b4e37b5fd77eab3429cbe6759493), trap-guarded with absolute paths: DIRECTION 1 (page on disk, absent from the list) — mutation confirmed on disk (fake page present, 72 bytes; artifact still at HEAD), EXIT=1 '1 hand-written doc(s) exist but are not listed + content/docs/getting-started/zz-reverse-verification-probe.mdx', restored, `git diff HEAD` empty. DIRECTION 2 (list entry with no file) — mutation confirmed by anchored counts (ghost entry 0->1 occurrence, docs 190->191) AND by blob change dc81dcb2->e93c9d8b, EXIT=1 '1 listed path(s) do not exist - content/docs/getting-started/zz-ghost-entry.mdx', restored to blob dc81dcb2, `git status --porcelain` empty (index clean too, so the checkout-from-ref staging trap is excluded). CONTROL on the restored tree EXIT=0. || NEW-GUARD MUTATION (the failure mode the move creates): the workflow body mutated to ignore args.handwritten; mutation proven on disk by anchor count 1->0 AND blob 111ac24f->8010c278; gate EXIT=1 with the artifact-side lines STILL GREEN ('scope is in sync', 'release-owned pages read-only') and only the third line red ('it does not consume the scope it is handed') — which is the point: an artifact in sync proves nothing about a body that stopped reading it. Restored to blob 111ac24f, `git status --porcelain` empty. --self-test carries four such mutants (injected list ignored · silent default replaces the no-scope refusal · FULL-audit line stops naming the source · malformed-list guard removed); an unapplied anchor fails as loudly as a green mutant, so 32/32 passing means all four applied AND all four went red. || OUTCOME MEASURED (the ruling's own success criterion), same simulated page-adding PR: BEFORE (new-page.mdx + meta.json + .claude/workflows/docs-accuracy-audit.js) EXIT=3 GOVERNED, .claude/** x1; AFTER (new-page.mdx + meta.json + scripts/docs-audit/handwritten-docs.json) EXIT=0 NOT governed. And this PR's own file list: EXIT=3 GOVERNED on .claude/workflows/docs-accuracy-audit.js x1 — the exit toll, draft-only, human merge. || LINT — a DECLARED NARROWING, not a skip, with all three evidence items: (1) the checked population read from eslint's OWN config via ESLint#isPathIgnored, not guessed — 5 of 8 changed paths in scope, 3 (.yml/.md/.json) ignored by config; (2) count from the result array: 5 files linted, errorCount=0 warningCount=0, exit 0; (3) invariance for untouched files — eslint.config.mjs's own header records this repo 'never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file', with a positive control, so no edit in this diff can move the verdict on a file it does not touch. Whole-repo `pnpm lint` is CI's run. || NOT MEASURED LOCALLY, by design: the always-runs CI tail that dispatch-gates explicitly excludes from --commands.",
      "mcp_calls": "0 — every GitHub read and write went through repo-scoped REST (probe: GET /repos/.../issues/13591 -> HTTP 200), plus git. No MCP GitHub call was made.",
      "open_questions": [
        {
          "question": "The #9866 / #10277 provenance row is retired by this PR. That row was itself a maintainer ruling (「10277 同意 C」 2026-08-20, re-confirmed 「A:按方案 2(最新裁定)」 2026-08-22 OVER the same day's earlier relocate-the-artifact proposal). The 2026-09-01 ruling chose relocation instead — the option that was declined then. This PR reads the newer ruling as superseding the older and removes the row. Flagging it because it retires a ruled mechanism, not because the consequence is ambiguous.",
          "options": [
            "A — retire the row (what this PR does): the ruling removes the row's subject, so keeping it would leave a lift that can only ever be asked to certify hand-authored instruction prose. A narrowing; GOVERNED_SURFACES untouched; the generic machinery and the two #11705 rows are untouched.",
            "B — keep the row dormant: requires keeping the workflow's block markers and the gate's splice functions (extractBlock/renderBlock/replaceBlock) alive purely as vestigial support, i.e. two shapes of the same list machinery — the 'one subject, two mechanisms' smell #4851 billed for. It also does not fail closed cleanly: with the markers gone, replaceBlock throws out of a path that expects a verdict.",
            "C — escalate and land nothing until the maintainer re-confirms the supersession."
          ],
          "recommendation": "A, because the consequence is mechanical rather than a fresh governance choice: the newer ruling deletes the artifact the row existed to certify, so the row has no subject left, and every alternative is either unsafe or leaves broken code. It moves verdicts only toward governed. The PR body states the supersession explicitly under a reviewer-attention heading so the human merge is an informed one, which is where a governance judgment belongs."
        }
      ],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

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

    domain:skillsDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions