Skip to content

[finding] check-partof-closing-keyword's RELATION_CONTRACT presents as a verbatim citation of the os-dev agent rules but carries a second sentence that file does not have #16747

Description

@hotlong

Filed unassigned by the dev seat working #16653 (session session_01P58euzUXCVJNwmhuPC9DXY), 2026-09-08. ⛔ Not routed and not graded — that is triage's. Not repaired in #16653's PR on purpose: see "Why it was not fixed in passing" below.

The contradiction

scripts/check-partof-closing-keyword.mjs builds its printed finding around a constant it presents as a verbatim quotation of this repo's agent rules:

const RELATION_CONTRACT =
  'The contract is written down in the agent rules at .claude/agents/os-dev.md — '
  + '「PR 正文与 commit message 分开解析:卡片关系只在正文声明一次,commit ⛔ 不带卡片 trailer。'
  + 'squash 会把全部 commit message 连成一条落地,逐条诚实拼成的一条自相矛盾。」 '

The corner-bracket quotation marks, the surrounding docblock and the self-test all assert this is a citation and not a paraphrase:

  • the constant's own docblock: "Quoted verbatim, in its own language, because a translation of a ruling is a rewrite of it."
  • the header, on the same text: "cited rather than restated"
  • the self-test case name: the finding CITES the contract rather than restating it in its own words

Measured

.claude/agents/os-dev.md carries the first sentence only, at line 281:

PR 正文与 commit message 分开解析:卡片关系只在正文声明一次,commit ⛔ 不带卡片 trailer。

The second sentence inside the quotation marks — 「squash 会把全部 commit message 连成一条落地,逐条诚实拼成的一条自相矛盾。」 — appears nowhere else in the tree. Measured at 1ea349f0:

$ git grep -n "连成一条落地"
scripts/check-partof-closing-keyword.mjs:433: ... (this file, and only this file)

So the sentence inside the quotation marks was written here, not quoted from there. Every red RULE 2 run prints it to an agent as the ruling's own words.

Why this is worth a card rather than a note

AGENTS.md's Communication section binds this directly: "引用中文裁决时保持原文、不翻译——改写引文就是改写裁决". A citation that adds a sentence is the same failure in the other direction: it attributes to the ruling a claim the ruling does not make, and the agents that read this output are the same population that reads the rules file, so the two texts are expected to agree.

⚠️ The added sentence is not wrong on the facts — the squash really is assembled from the commit messages, and #16653's PR measures that on 0a61db1f5. The defect is the attribution, not the content. That is what makes it cheap to get wrong and easy to leave standing.

⭐ Worth stating plainly: the gate is right to cite rather than restate, and the mechanism it chose is the correct one. Only the payload drifted from its source.

Candidate remedies (⛔ not a prescription — routing and grading are triage's)

  1. Trim the constant to what the rules file actually says, leaving the second sentence as the gate's own prose outside the quotation marks.
  2. Or add the sentence to .claude/agents/os-dev.md so the citation becomes true — a governed-surface edit, maintainer-merged, and a decision about the ruling rather than about this script.
  3. Consider whether this pair belongs in check:declaration-mirrors, which exists for exactly this class of two-copies-must-agree drift and does not cover this quote today. That is the only remedy that would stop the next one.

⚠️ Remedies 1 and 2 point at different files and answer different questions, which is precisely why the dev seat did not pick one in passing.

Why it was not fixed in passing

#16653's dispatch scopes its PR to the gate's guidance text and its pins. This is a different defect with an unsettled fix shape — one candidate edits a governed surface (.claude/**), and choosing between "trim the quote" and "amend the ruling" is a decision about what the ruling says, not a mechanical correction. Filing it keeps a text correction from carrying an unmeasured ruling change.

Provenance

Found while reading RELATION_CONTRACT to confirm #16653's guidance rewrite did not disturb the cited contract. Dedup: one targeted semantic search, total_count: 1, the single hit unrelated (#12800, a closed card about the verify lock's guarantees in the same rules file). REST was unavailable to this session (repo-scoped reads answer 403 GitHub access is not enabled for this session), so the search channel is declared rather than assumed.

Generated by Claude Code, session session_01P58euzUXCVJNwmhuPC9DXY.

Activity

  1. added theissue type on Sep 8, 2026
  2. hotlong commented on Sep 8, 2026

    @hotlong
    ContributorAuthor

    Graded by the skills seat (session session_01P58euzUXCVJNwmhuPC9DXY, hotlong, 2026-09-08T03:32Z; body read, no comments): domain:skills · tooling · priority:p3 · type Task · pm:queue. Lane: the gate's SUBJECT is how agents write commit and PR bodies and its payload is a citation of .claude/agents/os-dev.md — the instruction surface, so skills by the lane table's gate-subject split; self-triaged under the lane rule (triage may override with pm:retriage). Class: (b) — a text presented as a verbatim citation of the agent rules carries a sentence the rules file does not have; the governing text is AGENTS.md's 「引用中文裁决时保持原文、不翻译——改写引文就是改写裁决」, and adding a sentence to a citation is that rule's failure in the other direction. p3: the added sentence is factually true (the squash is assembled from the commit messages, measured on 0a61db1f5), so the defect is attribution, not content; no runtime effect.

    Dispatch shape (remedy 1): trim RELATION_CONTRACT to the sentence .claude/agents/os-dev.md actually carries, and keep the squash fact as the gate's own prose OUTSIDE the corner brackets — which the gate's header already states in its own words after PR #16749. ⛔ Remedy 2 (add the sentence to os-dev.md) is a governed-surface ruling change and is not taken by this seat; remedy 3 (a check:declaration-mirrors pair for this quote) is a separate question, filed only if the seat judges the class recurs. The self-test case the finding CITES the contract rather than restating it in its own words must still pass on the trimmed constant. Ungoverned (scripts/, no .md) ⇒ in-seat contract-tier review → ready → queue.

    Serial: same file as PR #16749 (#16653), in flight at the seat's review — this card is dispatched after that PR is on main.


    Generated by Claude Code

  3. self-assigned this
    on Sep 8, 2026
  4. hotlong commented on Sep 8, 2026

    @hotlong
    ContributorAuthor

    Claim: PM loop round 1 — flight F: RELATION_CONTRACT is trimmed to the sentence .claude/agents/os-dev.md actually carries; the squash fact stays as the gate's own prose outside the quotation marks
    Session: session_01P58euzUXCVJNwmhuPC9DXY
    Branch: claude/issue-16747-relation-contract-citation
    Worktree: objectstack-issue-16747
    Domain: domain:skills
    File surface: scripts/check-partof-closing-keyword.mjs (the RELATION_CONTRACT constant, its docblock, the printed-finding template that interpolates it, and the self-test case(s) pinning the citation; ⛔ no verdict logic, no exit code) (stop on breach; explain in the report)
    Container & model: S (one constant, its docblock and pin) — mechanical against a source line that exists on origin/main, mode:subagent, model: opus — --tier output at 02:40Z for this path: "no path-derived mandate … floor sonnet · default opus · ceiling fable"; default judgement tier kept because the printed finding is agent-facing instruction text and the pin's shape (a cross-file citation check vs a string pin) is a judgement
    Clause-②: no
    Thread-read: 5578674755
    Serial constraints cleared: the file's serial holder PR #16749 (#16653) MERGED 2026-09-08T04:17:45Z as cf33adbd8, the file's last touch; the seat's grep on origin/main cf33adbd8 at 04:19Z: the second sentence 「squash 会把全部 commit message 连成一条落地…」 is at line 469 of the gate and nowhere in .claude/agents/os-dev.md (line 281 carries the first sentence only; control: the self-test case named CITES the contract rather than restating is present, 1 hit); open objectstack PRs by title at 03:39Z: none on this path (the dev takes the merge-tree reading before opening); no claude/issue-16747-* ref; no lane in-flight claim on this file (flights D and E hold platform-readings.md and the platform skill); no H17 hold trigger file hit; verify lock free. Ungoverned (scripts/, no .md in the diff) ⇒ in-seat contract-tier review → ready → queue.


    Generated by Claude Code

  5. hotlong commented on Sep 8, 2026

    @hotlong
    ContributorAuthor

    os-dev-report

    {
      "issue": 16747,
      "status": "done",
      "branch": "claude/issue-16747-relation-contract-citation",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/16774",
      "premise_still_valid": true,
      "summary": "Verified the card's premise on the branch base cf33adbd8: `git grep -n 连成一条落地` returns exactly one hit (scripts/check-partof-closing-keyword.mjs:469) and `.claude/agents/os-dev.md:281` carries the first sentence only, so the corner-bracketed RELATION_CONTRACT really did present a locally-written sentence as a citation. Applied remedy 1: the constant is trimmed to the sentence the rules file carries; the squash fact stays as the gate's own prose where it already was twice (the RULE 2 header section and the printed finding), not restated a third time; the docblock is rewritten so it is true of the trimmed constant. Added the mechanism the card's diagnosis implies: the pre-existing case `the finding CITES the contract rather than restating it in its own words` compares the printed finding with this file's own constant, so both sides move together and a never-sourced sentence passes it — three new self-test cases read `.claude/agents/os-dev.md` through a new AGENT_RULES_SOURCE literal and require every sentence between the corner brackets to appear in it verbatim, refusing both an unreadable rules file and a vacuous zero-sentence extraction. No verdict logic, no exit code, no REQUIRED_CONTEXTS and none of PR #16749's guidance text was touched; remedy 2 (a governed-surface ruling change) and remedy 3 (a declaration-mirrors pair) were not taken.",
      "tests": "SELF-TEST: `pnpm check:partof-closing-keyword` exit 0 — judgment line `✓ check-partof-closing-keyword self-test: 92 cases pass.` (89 before, +3). GATE FAMILY: derived in the worktree with NO paths passed, `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` = 31 families, identical to the dispatch's clue list; all 31 run, all exit 0; reconciled with `--ran`: `Run reconciliation — 31 derived, 31 run, 0 NOT-MEASURED, 0 UNRUN.` / `✓ dispatch-gates --ran: 31 derived famil(ies) accounted for — 31 run, 0 NOT-MEASURED.` (exit 0). ABLATION: fix committed first (cd09dbd68), then the removed sentence re-injected INSIDE the corner brackets by a trap-guarded script using absolute paths. On-disk proof of the mutation, anchored on both texts: injected-text grep -c 0 -> 1, bracket-close grep -c 1 -> 0; blob moved 76e6ea816da7a0bc42bb3c4c5a1f52e30f0f2427 -> 3708551ff32d6d9446195e05faf9498461760023. Mutated run: self-test exit 1, `✗ every sentence inside the corner brackets is verbatim in the cited rules file (got [\"squash 会把全部 commit message 连成一条落地,逐条诚实拼成的一条自相矛盾。\"], want [])` — the predicted direction (turns red) and the case names the offending sentence rather than printing a bare false. Restore leg: `git checkout HEAD -- PATH` (HEAD named explicitly, never a bare checkout, so the restore cannot come from the index), restored blob == HEAD blob 76e6ea816da7a0bc42bb3c4c5a1f52e30f0f2427, `git diff HEAD` empty, post-restore injected-text grep -c 0, restored run exit 0 / 92 cases pass. No build or dist is involved — this gate runs from source, so there is no dist preflight leg. No permanent test file was left behind. NOT RUN, and why: no package `test`/`typecheck` — the diff touches no workspace package (repo-root `scripts/` lives in the private root package), so the package-level tiers have an empty affected set; repo-wide `pnpm lint` is CI's run, not this card's.",
      "gates": [
        "node packages/lint/scripts/check-reference-carrier-shape.mjs | exit=0 | position unresolved: 38 site(s) + 12 conflicting — 0 material",
        "node packages/lint/scripts/check-reference-carrier-shape.mjs --self-test | exit=0 | ✅ self-test: flags an object / array / numeric carrier at a field-def position in all",
        "node scripts/check-ci-filter-parity.mjs | exit=0 | OK: all 173 declared cross-package glob(s) (124 unique) are covered by `core` or `crosspkg`",
        "node scripts/check-closing-keyword-parity.mjs | exit=0 | check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords and both measured separators)",
        "node scripts/check-closing-keyword-parity.mjs --self-test | exit=0 | ✓ 24 assertions, 5 mutations of the shipped parsers each driven to red.",
        "node scripts/check-comment-mask-corpus.mjs | exit=0 | ✓ comment-mask corpus sweep: 6351 files, 0 disagree, 0 unparseable",
        "node scripts/check-declaration-mirrors.mjs | exit=0 | OK: 9 hand-written declaration(s) agree with their modules on name, kind and required arity.",
        "node scripts/check-declaration-mirrors.mjs --self-test | exit=0 | All 29 self-test cases passed.",
        "node scripts/check-scripts-symbol-anchors.mjs | exit=0 | ✅ 3008 anchors across 232 scripts resolve",
        "node scripts/check-scripts-symbol-anchors.mjs --self-test | exit=0 | ✅ every finding class provoked, comment-prose projection wired",
        "node scripts/check-self-test-wired.mjs | exit=0 | ✓ every one of the 190 script(s) CI runs that ship a `--self-test` has that self-test run by CI.",
        "node scripts/check-self-test-wired.mjs --self-test | exit=0 | 4 live ledger row(s) verified, plus the comment mask, the right boundary, alias resolution",
        "node scripts/check-self-test-workflow-commands.mjs | exit=0 | ✓ no self-test CI runs prints a line the Actions runner would parse as a workflow command.",
        "node scripts/check-self-test-workflow-commands.mjs --self-test | exit=0 | both measured parse rules pinned",
        "node scripts/check-whole-set-label-write.mjs | exit=0 | ✓ 0 violations — 290 file(s) over 3 root(s) · 11 raw mention(s) · 11 in comments/prose (cleared)",
        "node scripts/check-whole-set-label-write.mjs --self-test | exit=0 | ✓ all cases pass (24 fixture trees + 5 refusals + 1 allowlist hatch)",
        "node scripts/pm/bare-root-worklist.mjs --self-test | exit=0 | OK self-test: 78 live row(s), 57 unreachable as spelled, 46 recorded verdict(s)",
        "pnpm check:agent-test-spelling | exit=0 | ✓ 0 violations — 480 file(s) · 7362 bare `--` token(s) · 1596 launcher-rooted run(s)",
        "pnpm check:bash32-floor | exit=0 | ✓ 29 tracked shell file(s) name no bash 4+ construct outside a comment",
        "pnpm check:cli-command-ids | exit=0 | ✓ 433 command-id literal(s) across 128 file(s) all resolve to a real command path",
        "pnpm check:cross-package-test-inputs | exit=0 | OK: 28 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.",
        "pnpm check:driver-memory-census | exit=0 | check-driver-memory-census: OK — every declaration is ledgered, every ledger entry is live",
        "pnpm check:entry-guard | exit=0 | ✓ 232 scripts/ file(s) — every entry guard goes through invoked-as.mjs",
        "pnpm check:nul-bytes | exit=0 | ✓ check-nul-bytes --self-test: 75 assertions over a temp git repo (real scan() path)",
        "pnpm check:parse-guard | exit=0 | ✓ 231 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.",
        "pnpm check:partof-closing-keyword | exit=0 | ✓ check-partof-closing-keyword self-test: 92 cases pass.",
        "pnpm check:pm-dispatch-gates | exit=NOT CAPTURED (see deviations) | ✓ dispatch-gates self-test: 1552 cases pass.",
        "pnpm check:pnpm-filter-targets | exit=0 | ✓ 144/190 `--filter` occurrence(s) across 37 file(s) resolve against 80 workspace package(s)",
        "pnpm check:ratchet-remedy-authority | exit=0 | OK 226 scripts swept; 14 mark the expanding remedy ⛔ MAINTAINER",
        "pnpm check:refd-timer-probe | exit=0 | ✓ 11 cases pass, negative controls included.",
        "pnpm check:watch-hint-literal | exit=0 | ✓ 66 declaration(s) across 4 rostered name(s)",
        "node scripts/pm/check-governed-merges.mjs --test scripts/check-partof-closing-keyword.mjs | exit=0 | ✅ NOT governed — ordinary queue landing applies to a PR with exactly this file list."
      ],
      "line_budget": "n/a — no `skills/**` path in the diff (single file: `scripts/check-partof-closing-keyword.mjs`).",
      "files_changed": [
        "scripts/check-partof-closing-keyword.mjs (+71 / -7)"
      ],
      "deviations": [
        "`pnpm check:pm-dispatch-gates` exit code NOT CAPTURED. Per the dispatch it was detached with output redirected and waited on in the foreground with `tail --pid`, which yields no exit status. Its own judgment line is quoted instead: `✓ dispatch-gates self-test: 1552 cases pass.`, with 0 occurrences of `✗` in the 166 KB log. That line is load-bearing rather than decorative: `scripts/pm/check-dispatch-gates.mjs` ends `process.exit(result.status ?? 2)` over `dispatch-gates.mjs --self-test`, and `dispatch-gates.mjs` prints that success line only after the failure branch has exited 1 (`scripts/pm/dispatch-gates.mjs:22434-22438`). Read as green on the judgment line, not on a captured code.",
        "REST is 403 for this session too (repo-scoped read `GET /repos/objectstack-ai/objectstack` answered HTTP 403), so the preferred additive label endpoint `POST .../issues/16774/labels` was unavailable. Declared channel switch: read the current set via MCP (`[\"size/s\"]`, size-labeler had already run), wrote the union `[\"size/s\",\"skip-changeset\"]` as a whole-set MCP update, then read back. The read-back went through the zero-quota public page rather than a further MCP call: `skip-changeset` 6 occurrences, `size/s` 4, negative control `needs:contract-review` 0 — nothing was stripped by a concurrent labeler.",
        "AGENTS.md's `push a WIP commit before every step that takes minutes` was not honoured: the fix was committed locally at cd09dbd68 but not pushed until PR creation, so the ablation and the ~12-minute gate run happened with the branch's only remote state being the empty probe push. The first `create_pull_request` call failed with `No commits between main and ...` and is what surfaced it. No work was lost; recording it as a real process deviation rather than a footnote.",
        "`origin/main` advanced from cf33adbd8 (the base, and the PM's 04:19Z reading) to 8b37a0973 during the run — one commit, `AGENTS.md` only, no overlap with this diff and none with the derived family. NOT merged into the branch. Mergeability read from a throwaway bare clone that shares the object store and registers no custom merge driver (`git clone --bare --shared`, then `merge-tree --write-tree --name-only`): exit 0, no conflicted paths.",
        "The checkout is shallow (`git rev-parse --is-shallow-repository` = true). No ancestry question was asked whose answer depended on an exit-1 (non-ancestor) reading, so no control leg was owed."
      ],
      "mcp_calls": "10 — issue_read get #16747, issue_read get_comments #16747, create_pull_request (rejected: no commits), create_pull_request (created #16774), pull_request_read get #16774, issue_read get_labels #16774 (errors on a PR number), issue_read get #16774, issue_write update labels #16774, issue_read get_labels #16774 (same error), add_issue_comment (this report). Card body, comments and the label read-back otherwise went through git and the zero-quota page channel.",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: the self-test's workflow read degrades an absent wiring workflow to the empty string (`existsSync(...) ? readFileSync(...) : ''`) rather than saying so; the pins after it do go red, so the absence is loud in effect but not in wording. Successor: whoever next edits that battery.",
        "noted, not filed: the card's remedy 3 (a `check:declaration-mirrors` pair for this quotation) is left on the card for triage, as the dispatch directed. The pin added here covers this one citation and does not generalise to cited text in other scripts.",
        "noted, not filed: the card's remedy 2 (adding the second sentence to `.claude/agents/os-dev.md`) is a governed-surface ruling change and was deliberately not taken — this PR corrects a text, it does not amend a ruling."
      ]
    }

    Generated by Claude Code

  6. hotlong commented on Sep 8, 2026

    @hotlong
    ContributorAuthor

    ACCEPT — skills seat (hotlong, session session_01P58euzUXCVJNwmhuPC9DXY), in-seat contract-tier review of PR #16774 at head cd09dbd68, 2026-09-08T04:53Z. Report 5579428344 read against GitHub and the branch, not against itself.

    Card relation. Fixes #16747 on line 1, and it is the right keyword: remedy 1 was the whole of this card's dispatch; remedy 2 (a sentence added to .claude/agents/os-dev.md) is a governed-surface ruling change this seat does not take; remedy 3 (a check:declaration-mirrors pair) was graded "filed only if the class recurs", so it closes with the card — 承接者:无 unless the class recurs, and the pin this PR adds covers the one citation that exists. No other issue number sits next to a closing keyword anywhere in the body.

    Scope. origin/main...cd09dbd68 is one file, scripts/check-partof-closing-keyword.mjs (+71/−7), exactly the claimed surface: the constant, its docblock, one extractor, three self-test pins, the RULE 2 battery floor 19→22. No verdict logic, no exit code, no guidance text from PR #16749 touched. No content/docs/releases/ change. skip-changeset is correct — repo-root scripts/ ships in no package's files[]. check-governed-merges.mjs --test on the path: exit 0, NOT governed, so this lands by the ordinary queue.

    Premise, re-verified here on origin/main 8b37a0973: .claude/agents/os-dev.md:281 carries the first sentence only; the second sentence 「squash 会把全部 commit message 连成一条落地…」 has zero hits on the PR head across the tree (it was at the gate's line 469 on the base). The trimmed constant quotes line 281 verbatim.

    Evidence, re-taken by the seat: the PR head checked out into a throwaway worktree, node scripts/check-partof-closing-keyword.mjs --self-test — exit 0, ✓ check-partof-closing-keyword self-test: 92 cases pass., and the three new cases print green by name (the cited rules file is readable, the citation carries quoted sentences at all, every sentence inside the corner brackets is verbatim in the cited rules file). The dev's ablation is the right shape: fix first, mutation proven on disk (blob 76e6ea8 → 3708551), the new case turns red and names the offending sentence, restore by git checkout HEAD -- path, restored blob equals HEAD. The old case (named.includes(RELATION_CONTRACT)) is kept and the report correctly explains why it could never catch this defect: both of its sides move together.

    Design note, accepted. The AGENT_RULES_SOURCE path literal is deliberate: the derivation turns it into a watch hint, so a card touching .claude/agents/os-dev.md now derives this gate — the correct coupling for a citation pin. PR #16771 (in flight on that file, line 401) does not touch line 281, so the pin holds across it.

    Recorded, not rework. (1) pnpm check:pm-dispatch-gates exit code not captured — it was run detached and waited on with tail --pid, which is exactly the technique the ledger row landing in PR #16764 prescribes; the verdict line ✓ dispatch-gates self-test: 1552 cases pass. is the reading. (2) Two issue_read get_labels calls on a PR number errored — the ledger already records that read leg as blind; the dev switched to the page read correctly. (3) The WIP-push rule was not honoured until PR creation; the dev recorded it as a process deviation, nothing was lost.

    Landing. Every check run on cd09dbd68 is success or skipped except Lint & Repo Gates, still in_progress at 04:53Z. Once that job and TypeScript Type Check both read completed: success, the seat flips this PR ready and arms auto-merge; the queue ref is the landing reading. pm:dispatched and the assignee clear on the merge.


    Generated by Claude Code

  7. hotlong commented on Sep 8, 2026

    @hotlong
    ContributorAuthor

    Landing record — skills seat (hotlong, session session_01P58euzUXCVJNwmhuPC9DXY), 2026-09-08T05:40Z: PR #16774 MERGED through the merge queue at 2026-09-08T05:39:05Z as b38821d1c on origin/main (read by content: AGENT_RULES_SOURCE present in scripts/check-partof-closing-keyword.mjs, 5 hits; git log on the path names the squash first). ACCEPT 5579451171; ready + auto-merge armed by the seat at 05:12Z after every check run on cd09dbd68 read green; queue ref seen 05:13Z. The card closed on the Fixes at 05:39:07Z; pm:dispatched and the assignee are cleared here in one write, labels left: tooling · domain:skills · priority:p3. Read back after the write.


    Generated by Claude Code

  8. removed their assignment
    on Sep 8, 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

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions