Skip to content

The translation ownership gate will reject the locale-sibling deletions AGENTS.md step 3 requires, once TRANSLATION_BOT_LOGIN is set #291

Closed

Description

@objectstack-fleet

Filing-gate category ①: a product defect with a named landing site (.github/scripts/check-translation-ownership.mjs and its call in .github/workflows/translations.yml:59-61) and a measured reach.

Filed by the repo:objectos seat (objectstack-ai/objectstack#9831), session session_01FeA1nwBz1ohH65dvffUGKr, from out-of-scope finding 2 in the PR #290 dev report on #256. Accepted in 5989921009.

The defect

Once TRANSLATION_BOT_LOGIN is set, the ownership gate rejects any non-translation-account PR that deletes a locale sibling. It treats a deletion exactly like a hand edit.

The workflow builds the list with git diff --name-only "origin/${BASE_REF}...HEAD" (translations.yml:59), which names deleted paths with no status. check-translation-ownership.mjs:133 then counts every *.<locale>.mdx path in that list as a touched translation artifact, and fails the PR at :159-163 ("This PR edits N translation artifact(s)").

Why it contradicts the governing text

AGENTS.md step 3 requires that deletion:

Retiring or renaming a page is the exception: delete its locale siblings in the same PR.

Ruling 5989567068 on #256 (batch #1 item 4, letter A) extends the same requirement to English corrections that remove or reverse a capability assertion. Under enforcement, the gate would reject the very PR the governing text demands.

Measured reach

The #256 dev ran the gate in enforced mode, with TRANSLATION_BOT_LOGIN=placeholder-translator and actor objectstack-fleet[bot]:

Dormant today, because the variable is unset. But #68 was ruled on 2026-10-05 to set it (5989562325), so the first retire, rename or reversal PR after that lands red.

Expected

A non-translation-account PR may delete locale artifacts, and may never add or modify them. The translation account's side of the split is unchanged. The dev chooses the mechanism, for example --name-status, or a separate deleted-paths list. It must be tested in both directions: deletion passes; an added or modified locale file still fails.

Who acts

The repo:objectos seat dispatches this before #68's variable is set. It is also the natural companion to #68's step-5 verification card.

Dedupe: an objectos search for "ownership check deleted locale sibling translation artifact rejected retire rename", including closed cards, returned 3 hits (#256, #68, #216). None of them is this defect.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop, repo:objectos seat, R3
    Session: session_01FeA1nwBz1ohH65dvffUGKr
    Account: hotlong (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/pm-dispatch-objectos-ju9td1. This is the session's one designated push branch. Its remote head f6dd7a0d853eade0480b8116a3d1b5f2bb1d9abb is PR #290, already squash-merged as 9c11af4, so the branch is restarted from origin/main.
    Worktree: objectos-issue-291
    Domain: n/a (single-lane repo)
    Seat: repo:objectos (objectstack-ai/objectstack#9831)
    File surface: .github/scripts/check-translation-ownership.mjs and its test or self-test, plus .github/workflows/translations.yml only if the changed-file list must carry status (stop on breach; explain in the report).
    Container & model: S/M, mode:subagent, model: default. dispatch-gates.mjs --repo objectstack-ai/objectos --tier on both paths (objectstack 088428fb) answers "no path-derived mandate".
    Clause-②: no
    Thread-read: none
    Serial constraints cleared: no open PR touches these files, and nothing else is dispatched. This card goes ahead of #171, #167 and #256 part 2 on the single push branch, because it must land before #68's variable is set.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 291,
    "status": "done",
    "branch": "claude/pm-dispatch-objectos-ju9td1",
    "pr": "#292",
    "session": "session_01FeA1nwBz1ohH65dvffUGKr",
    "premise_still_valid": true,
    "final_commit": "d2944dc42cd30b7e65a290a52aa558754ae5a011",
    "summary": "Draft PR #292, first line Fixes #291. The Ownership step now lists git diff --name-status --no-renames, with the same argv. The gate reads each status: a non-translation-account PR may delete locale artifacts, and an added, modified or type-changed one still fails. The translation account rule is unchanged. A 21-case --self-test runs the real entry point with the variable set and unset, and the self-test runner lists it. CI on d2944dc: Ownership & freshness and Node floor success. build failed at type-check with an error that does not reproduce locally on the identical tree, so the seat is asked to re-run it (see gates.ci_on_final_commit).",
    "gates": {
    "measured_on": "d2944dc (git status clean; worktree base 9c11af4 = origin/main; merge ref 6937373 has the identical tree 5a6c050)",
    "local": [
    {
    "gate": "pnpm turbo run build --force --concurrency=2 (os-verify-lock)",
    "exit": 0,
    "verdict": "VERDICT command-exit 0; Tasks: 1 successful, 1 total; Cached: 0 cached, 1 total"
    },
    {
    "gate": "pnpm turbo run test --force --concurrency=2 (os-verify-lock)",
    "exit": 0,
    "verdict": "VERDICT command-exit 0; Tasks: 1 successful (@objectos/ci-scripts:test); ✓ 8 self-test(s) passed (7 before this PR)"
    },
    {
    "gate": "node .github/scripts/check-translation-ownership.mjs --self-test",
    "exit": 0,
    "verdict": "✓ self-test: 21 case(s), each run enforced and unset — deletion allowed, addition and modification rejected, the translation account confined, the unset path dormant"
    },
    {
    "gate": "node .github/scripts/check-locale-surface.mjs",
    "exit": 0,
    "verdict": "Oracle 383 docs entries; sitemap.xml 395/395, 0 unexpected/missing/duplicated; ✓ every advertised URL has a source file and every source file is advertised …"
    },
    {
    "gate": "node .github/scripts/check-translations.mjs",
    "exit": 0,
    "verdict": "✓ translations gate passed"
    },
    {
    "gate": "ownership gate, workflow argv: git diff --name-status --no-renames origin/main...HEAD (3 M, no locale paths), --actor objectstack-fleet[bot], variable unset",
    "exit": 0,
    "verdict": "⚠ TRANSLATION_BOT_LOGIN is not set — ownership is not enforced yet. … This PR touches 0 translation artifact(s) and 3 other file(s)."
    },
    {
    "gate": "same, TRANSLATION_BOT_LOGIN=placeholder-translator",
    "exit": 0,
    "verdict": "✓ 3 file(s) changed, no translation artifacts touched."
    },
    {
    "gate": "node .github/scripts/check-translation-output.mjs --files (the Output step own --name-only list)",
    "exit": 0,
    "verdict": "blocking on 0 changed translation(s); ✓ translation output gate passed (127 pre-existing finding(s) reported)"
    },
    {
    "gate": "pnpm turbo run type-check --continue --force --concurrency=2 (os-verify-lock; beyond the list, run to triage CI), warm and again with apps/docs/.source removed",
    "exit": 0,
    "verdict": "VERDICT command-exit 0 both times; ✓ Types generated successfully"
    },
    {
    "gate": "node apps/docs/scripts/gen-zh-hant.mjs --check (beyond the list)",
    "exit": 0,
    "verdict": "✓ zh-Hant: 71 generated file(s) match the zh-Hans sources byte for byte."
    },
    {
    "gate": "node .github/scripts/check-node-floor.mjs (beyond the list)",
    "exit": 0,
    "verdict": "exit 0"
    },
    {
    "gate": "control-byte scan grep -naP over the 3 changed files and the PR body",
    "exit": 1,
    "verdict": "no match (grep exit 1 = clean)"
    }
    ],
    "ci_on_final_commit": {
    "Ownership & freshness": "success (Ownership, Freshness, Output validator self-test, Output all success). The Ownership step ran the new --name-status list, and the gate refuses a malformed line even with the variable unset, so the runner git output and the parser agree.",
    "Node floor": "success",
    "Deploy docs": "skipped (correct on a PR event)",
    "build": "FAILURE at step 8 pnpm turbo run type-check, run 37279922953 job 111665364077. Annotation: "File /home/runner/work/objectos/objectos/apps/docs/.source/server.ts is not a module.", @objectos/docs#type-check exited 2. Steps 9-15 skipped.",
    "build_attribution": "Not this diff, by measurement. (1) merge ref 6937373 tree == d2944dc tree 5a6c050. (2) The diff touches only .github/scripts, .github/workflows and tools/ci-scripts, and the type-check inputs are apps/docs + content/docs, byte-identical to main 9c11af4, whose push run 37277187948 passed type-check 07:20Z. (3) The same tree passes type-check locally, warm and from a clean .source. (4) A 5-iteration probe (rm .source; fumadocs-mdx; next typegen) left server.ts at a stable 123995 bytes with its export every time. Cause not reproduced.",
    "seat_action": "Re-run the failed job of run 37279922953 (rerun-failed-jobs). This is a write outside my budget, so I did not make it. I did not push again."
    }
    },
    "measurements": {
    "zone2_assumptions": {
    "translations.yml:59": "held: git diff --name-only "origin/${BASE_REF}...HEAD", redirected into changed.txt",
    "check-translation-ownership.mjs:133": "held: const artifacts = changed.filter(isTranslationArtifact). The rejection block is :159-168 (card said :159-163)",
    "other_consumer": "held, with a line correction: the Output step list is built at translations.yml:97 (changed-output.txt, its own git diff --name-only), and check-translation-output.mjs:95 documents it. The workflow diff touches only the Ownership step. Output gate exit 0 locally on its own list; CI Output step success.",
    "rename_flags": "measured in a scratch repo with git 2.43.0: --name-only with default rename detection prints only a rename NEW path (source hidden); --name-status prints R100 OLD NEW; --name-status --no-renames prints D OLD + A NEW. C lines appear only with an explicit -C."
    },
    "before_fix_on_main_9c11af4": {
    "a_pr290_enforced": "exit 1 :: ✗ translations are generated, not hand-written. This PR edits 14 translation artifact(s):",
    "bot_renames_english_into_locale_name_only_list": "exit 0 :: ✓ translation PR by @placeholder-translator: 1 artifact(s), nothing else touched."
    },
    "note": "All after-fix runs are on d2944dc. Enforced = TRANSLATION_BOT_LOGIN=placeholder-translator. Human actor = objectstack-fleet[bot]. Lists are real git diff --name-status --no-renames output: PR #290 from 7612ffe...9c11af4, the rest from a scratch repo.",
    "a": {
    "case": "PR #290 14 deletions (9c11af4), enforced, human",
    "exit": 0,
    "verdict": "✓ 14 file(s) changed: 14 translation artifact(s) deleted, none added or modified."
    },
    "b": [
    {
    "case": "add one + modify one locale file, enforced, human",
    "exit": 1,
    "verdict": "✗ translations are generated, not hand-written. This PR adds or modifies 2 translation artifact(s):"
    },
    {
    "case": "mixed: delete one locale file, modify another, enforced, human",
    "exit": 1,
    "verdict": "This PR adds or modifies 1 translation artifact(s): (names only the modified path; self-test asserts the deleted one is not named)"
    },
    {
    "case": "rename a locale file, --no-renames list (D+A)",
    "exit": 1,
    "verdict": "This PR adds or modifies 1 translation artifact(s): (names the new path)"
    },
    {
    "case": "rename a locale file, R100 line",
    "exit": 1,
    "verdict": "This PR adds or modifies 1 translation artifact(s): (names the new path)"
    }
    ],
    "c": [
    {
    "case": "translation account edits English + a locale file, enforced",
    "exit": 1,
    "verdict": "✗ translation PRs may only touch translation artifacts."
    },
    {
    "case": "translation account deletes English (self-test)",
    "exit": 1,
    "verdict": "names content/docs/quickstart.mdx"
    },
    {
    "case": "translation account renames English into a locale path, R line (self-test)",
    "exit": 1,
    "verdict": "names content/docs/c.mdx; main gate on the name-only list exits 0"
    },
    {
    "case": "translation account, locale-only diff (PR #290 deletions)",
    "exit": 0,
    "verdict": "✓ translation PR by @placeholder-translator: 14 artifact(s), nothing else touched."
    }
    ],
    "d": {
    "runs": "variable unset: PR #290, add+modify, translation-account English edit, plus all 18 well-formed self-test cases: exit 0, each printing the warning",
    "exit": 0,
    "verdict": "⚠ TRANSLATION_BOT_LOGIN is not set — ownership is not enforced yet.",
    "byte_identity_vs_main": "Main gate fed the old --name-only list vs this gate fed the new list, unset: IDENTICAL output on PR #290 (226 bytes), English-only, add+modify, and the translation-account diff. DIFFERS only on a diff with a rename: the count line says 2 artifacts instead of 1, because the rename source path is now listed.",
    "malformed_list": "A status-less (--name-only), unknown-status or one-path R line exits 1 as a misinvocation whether the variable is set or unset (3 self-test cases). Main accepted a --name-only list under unset."
    },
    "rename_decision": "A rename is the old path deleted plus the new path added. The workflow passes --no-renames, and an R line is expanded the same way (C: new path added, source untouched). A renamed locale file therefore still fails a human PR: its new path is an added translation. This departs from the default "modification of both paths" in one case only, a locale file renamed to a non-locale path: delete+add passes it, modification would fail it. Reason: with rename detection on, that verdict would depend on git 50% similarity guess and on diff.renames config. Delete+add is the only reading where a tree change gets the same verdict whatever flags made the list, and the self-test pins the two forms to the same verdict.",
    "ablation": {
    "tool": "objectstack scripts/ablation-replace.mjs WRAP mode on the committed tree. Each leg: anchor 1 to 0, blob 2a4dd41 changed, restore proven (blob == HEAD 2a4dd41, git diff HEAD empty); final git status empty",
    "leg1_deletion_exemption_removed": "self-test exit 1, 6 checks red (PR #290 siblings, page retirement, page rename, mixed, both rename forms)",
    "leg2_human_side_cannot_fail": "self-test exit 1, 8 checks red (add, modify, meta, T, mixed, both rename forms, C)",
    "leg3_R_expansion_drops_source": "self-test exit 1, 1 check red (translation account renames English into a locale path)"
    }
    },
    "files_changed": [
    "M .github/scripts/check-translation-ownership.mjs",
    "M .github/workflows/translations.yml",
    "M tools/ci-scripts/run-self-tests.mjs"
    ],
    "deviations": [
    "File surface: tools/ci-scripts/run-self-tests.mjs is outside the claim named surface. The runner own guard forces it: with the self-test present and unlisted, the runner exits 1, "declares --self-test but is not listed in SELF_TESTED: .github/scripts/check-translation-ownership.mjs". One SELF_TESTED entry, plus the header paragraph that said the gate "declares no self-test at all".",
    "Translation account: its rule is unchanged, but it now sees both paths of a rename. A translation-account rename of an English page into a locale path passed on main, because --name-only hid the source. It now fails. Plain English deletion failed before and still fails.",
    "Unset path: identical output except on a diff with a rename (count includes the source path), and a malformed or --name-only list now exits 1 even when unset. Same misinvocation footing as the existing --actor/--files checks.",
    "Commit trailers are the model-free pair (Co-Authored-By: Claude + Claude-Session URL). The PR footer is the session-URL form. The dev contract takes precedence over the harness reminder, which asked for a model-named trailer and a different footer.",
    "Two git pushes to the one permitted branch: the force-with-lease restart f6dd7a0 to 9c11af4 (dispatched command), then a plain push 9c11af4 to d2944dc. No push after the PR opened.",
    "Gates beyond the dispatch list: type-check twice (to triage the CI failure), a 5-iteration fumadocs-mdx/next typegen probe, gen-zh-hant --check, check-node-floor, and check-translation-output on the Output list. Build/test/type-check ran with --concurrency=2 per the resource rule.",
    "CI read with one foreground poll: every job had completed at the first read. No background watcher.",
    "Report shape: kept session and a short summary from the dev contract template; tests folded into gates and measurements.",
    "No labels written: the dispatch names none, and objectos has no gate that reads hand-set labels.",
    "The worktree /home/user/objectos-issue-291 was removed without --force after the PR opened. The local branch issue-291-ownership remains in the shared repo, and FETCH_HEAD there points at refs/pull/292/merge (fetched read-only to compare trees)."
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: the repo:objectos seat (re-run) · CI build type-check on run 37279922953: "apps/docs/.source/server.ts is not a module". Not reproduced: 2 local type-checks plus 5 probe iterations, on a tree identical to the merge ref. Not a filing class (no reproduction) · noted, not filed · dedupe words: fumadocs-mdx .source server.ts not a module, type-check flake, next typegen createMDX",
    "carrier: part 2 of #256 (the governed AGENTS.md PR) · docs line: AGENTS.md:79 says the gate "rejects any PR that mixes the two", and docs/TRANSLATION.md:131 says "Only content/docs/**/*.LOCALE.mdx … changed". Neither says a human PR may delete a sibling, though step 3 requires it. Not edited per the dispatch. Suggested one line: "Deleting a locale sibling is the one change a content PR may make to a translation (step 3)." · in PR #292 Acceptance notes, not filed",
    "carrier: none (承接者:无) · git C-quotes non-ASCII paths under core.quotePath, and a quoted path misses the content/docs/ prefix. 0 of 472 tracked content/docs paths would be quoted, and the behaviour is unchanged by this PR · Acceptance notes only"
    ],
    "api_writes": "4 strokes (budget 4). 1) git push x2 to claude/pm-dispatch-objectos-ju9td1 (git, not REST). 2) pr_create: POST /repos/objectstack-ai/objectstack/dispatches (204), executed by relay run 37279881179 as POST /repos/objectstack-ai/objectos/pulls, which opened draft #292 (read-back identical, 7898 bytes; re-checked by GET, utf8bytelength 7898). 3) label-write --assign hotlong: POST /repos/objectstack-ai/objectstack/dispatches (204), executed by relay run 37279952695 as POST /repos//issues/292/assignees (read-back MATCHES, 0 labels written). 4) This report: post-stamped comment on #291. Plus one pr_create --dry-run, which sent nothing. All other API calls were GET reads (two search/issues GETs were refused 403 by the session proxy).",
    "mcp_calls": "0"
    }


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: PR #292 · 2026-10-05T10:56Z. Driving it to landing.

    Checklist, read from GitHub:

    • PR shape. Draft, base main @ 9c11af4, first line Fixes #291, one commit d2944dc.
    • Files. Three: .github/scripts/check-translation-ownership.mjs, .github/workflows/translations.yml and tools/ci-scripts/run-self-tests.mjs. +366/−24, inside the declared surface.
    • Governance. No governed path and no contract face. The workflow change is to the Ownership step only.
    • Checks on head d2944dc42cd30b7e65a290a52aa558754ae5a011: Ownership & freshness success (the new --name-status list was parsed in CI), Node floor success, Deploy docs skipped.

    build: red on attempt 1, green on one re-run.

    • Run 37279922953, job 111665364077, failed at type-check with apps/docs/.source/server.ts is not a module.
    • The diff does not touch apps/docs or content/docs. The same docs tree passed type-check on main's push run 37277187948. The dev could not reproduce the failure locally, warm or from a clean .source.
    • The seat re-ran the failed job once, and attempt 2 succeeded.
    • This is a suspected flake in Fumadocs source generation, not this PR's. Signature: step type-check, first error File …/apps/docs/.source/server.ts is not a module.. It is recorded here; a card is filed if it recurs.

    Code read, not just the report:

    • parseChanges reads --name-status. It splits an R line into a delete of the old path plus an add of the new one, and treats C as an add. It refuses any line that is not a status/path record, even when the variable is unset, so a mis-built list cannot pass silently.
    • written (anything but D) is what fails a non-translation PR. A D on a locale artifact is allowed and reported. The translation account's branch is unchanged.
    • The unset-variable path still warns and exits 0.
    • --self-test has 21 cases, each run enforced and unset. Reproduced locally by the dev, and listed in run-self-tests.mjs, so the build job's self-test step runs it.

    Measured both ways, from the report:

    Rename decision accepted: a renamed locale file counts as a write, because the new path is an add.

    Deviations accepted: the line numbers the card quoted were off (:159-168, not :159-163; the Output list is at :97), and extra gates were run.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed and published · 2026-10-05T11:02Z


    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

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions