Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,5 +174,6 @@ than attaching private fixtures, snapshot identifiers, raw output or infrastruct
- [ ] Neither the diff nor this PR body/comments/attachments disclose private state, credentials, raw traces or verifier output, internal links, or local machine paths (including `.loopx/`, `.codex/goals/`, and live `ACTIVE_GOAL_STATE.md`).
- [ ] I did not duplicate maintainer-owned benchmark work unless a maintainer split out a public issue for it.
- [ ] I kept the change scoped to the linked issue/task.
- [ ] Existing English/Chinese RFC pairs (including ledger entries) both have a diff, and the local premerge check `python3 scripts/check_rfc_bilingual.py --base origin/main --head HEAD` passes, or no paired RFC changed.
- [ ] I completed the visual evidence section for UI changes, or marked UI impact `none`.
- [ ] Every commit includes a DCO `Signed-off-by` trailer (`git commit -s`).
20 changes: 20 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,26 @@ for the full DCO policy.
Only skip this worktree/PR gate when the user explicitly says the change is
local-only and must not be proposed for the repository.

### Bilingual RFC Diff Gate

For an existing English/Chinese RFC pair under `docs/architecture/rfcs/`,
including its ledger entries, both files must change in the same PR. Check the
staged files before committing:

```bash
python3 scripts/check_rfc_bilingual.py --base origin/main --staged
```

The staged check includes earlier branch commits. Before merge, run
`python3 scripts/check_rfc_bilingual.py --base origin/main --head HEAD` against
the PR's actual target branch and exact reviewed head. The source-checkout
`uv run --extra test loopx canary premerge --from-git-diff` runs this as a direct
blocking check, including in the quick tier. A failure holds merge even when CI
waiting is disabled or admin bypass is authorized. It checks the committed diff;
an unstaged translation cannot satisfy it. The check uses the merge base, covers
paired deletion/renaming, and leaves legacy single-language files alone. Review
the actual translation; paired diffs do not certify semantic equivalence.

For non-trivial repository changes, especially anything that touches benchmark
adapters, smoke tests, public docs, or commit/push workflows, use the
`git-split-commit-pr` workflow before staging:
Expand Down
14 changes: 14 additions & 0 deletions docs/architecture/rfcs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,20 @@ failure leaves the generated files untouched.
successor reciprocally names that predecessor in `Supersedes / closes`.


## Bilingual changes

Existing English/Chinese RFC pairs and their ledger entries must both have a
diff in the same PR. Before committing, run
`python3 scripts/check_rfc_bilingual.py --base origin/main --staged` (including
earlier branch commits). Before merge, run
`python3 scripts/check_rfc_bilingual.py --base origin/main --head HEAD`, using
the PR's actual target branch and reviewed head. Source-checkout
`uv run --extra test loopx canary premerge --from-git-diff` includes this direct
blocking check in every tier. CI waiting and admin bypass do not waive it.
Only committed changes satisfy the merge check. It compares from the merge base,
includes paired deletion/renaming, and leaves legacy single-language files alone.
Paired diffs do not certify translation accuracy.

## Overall Product and Delivery Roadmap

- [LoopX Overall Roadmap v0](loopx-overall-roadmap-v0.md)
Expand Down
6 changes: 5 additions & 1 deletion examples/canary/premerge-validation-gate-smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,11 @@ def assert_cli_premerge_reports_progress_by_default() -> None:
assert payload["gate"]["status"] == "no_changes", payload
assert payload["validation_summary"]["selected_check_count"] == 0, payload
assert "[loopx canary] premerge start:" in completed.stderr, completed.stderr
assert "[loopx canary] start direct_checks 1/3:" in completed.stderr, completed.stderr
assert "[loopx canary] start direct_checks 1/" in completed.stderr, completed.stderr
assert any(
check["id"] == "rfc_bilingual_diff" and check["status"] == "passed"
for check in payload["direct_checks"]
), payload
assert "[loopx canary] start catalog_canaries" not in completed.stderr, completed.stderr
assert "[loopx canary] premerge done:" in completed.stderr, completed.stderr

Expand Down
11 changes: 11 additions & 0 deletions loopx/canary/premerge.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,17 @@ def _diff_hygiene_checks(
"checks unstaged changes not yet committed",
),
]
rfc_checker = "scripts/check_rfc_bilingual.py"
checker_registered = (repo_root / rfc_checker).is_file() or subprocess.run(
["git", "cat-file", "-e", f"{base}:{rfc_checker}"],
cwd=repo_root, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False,
).returncode == 0
if checker_registered:
specs.append((
"rfc_bilingual_diff",
[sys.executable, rfc_checker, "--base", base, "--head", "HEAD"],
"requires both existing RFC mirrors in the committed PR diff before merge",
))
checks: list[dict[str, Any]] = []
for index, (check_id, argv, reason) in enumerate(specs, start=1):
checks.append(
Expand Down
80 changes: 80 additions & 0 deletions scripts/check_rfc_bilingual.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""Premerge check for both existing RFC language mirrors in the same PR diff."""

from __future__ import annotations

import argparse
import os
import subprocess
import sys

RFC_ROOT = "docs/architecture/rfcs/"


def git_paths(*args: str) -> set[str]:
raw = subprocess.check_output(["git", *args, "-z", "--", RFC_ROOT])
return {os.fsdecode(path) for path in raw.split(b"\0") if path}


def revision(ref: str) -> str:
return subprocess.check_output(
["git", "rev-parse", "--verify", "--end-of-options", f"{ref}^{{commit}}"],
text=True,
).strip()


def missing_mirrors(changed: set[str], existing: set[str]) -> list[tuple[str, str]]:
missing = []
for path in sorted(changed):
if not path.startswith(RFC_ROOT) or not path.endswith(".md"):
continue
mirror = (
path.removesuffix(".zh-CN.md") + ".md"
if path.endswith(".zh-CN.md") else path.removesuffix(".md") + ".zh-CN.md"
)
if mirror in existing and mirror not in changed:
missing.append((path, mirror))
return missing


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--staged", action="store_true", help="check the index; --base includes earlier branch commits")
parser.add_argument("--base", help="target-branch commit; compare from the merge base")
parser.add_argument("--head", help="exact proposed commit, required with --base")
args = parser.parse_args()
if args.staged and args.head:
parser.error("--head cannot be combined with --staged")
if not args.staged and not (args.base and args.head):
parser.error("supply --staged or both --base and --head")
try:
if args.staged:
base = revision("HEAD")
if args.base:
base = subprocess.check_output(
["git", "merge-base", revision(args.base), base], text=True,
).strip()
changed = git_paths("diff", "--cached", "--name-only", "--no-renames", base)
after = git_paths("ls-files", "--cached")
else:
head = revision(args.head)
base = subprocess.check_output(
["git", "merge-base", revision(args.base), head], text=True,
).strip()
changed = git_paths("diff", "--name-only", "--no-renames", base, head)
after = git_paths("ls-tree", "-r", "--name-only", head)
before = git_paths("ls-tree", "-r", "--name-only", base)
except subprocess.CalledProcessError:
print("Cannot read the RFC diff; fetch the exact base/head and retry.", file=sys.stderr)
return 1
missing = missing_mirrors(changed, before | after)
if missing:
print("RFC bilingual diff check failed:", file=sys.stderr)
for path, mirror in missing:
print(f" {path}: include a matching change to {mirror}", file=sys.stderr)
return 1
print("RFC bilingual diff check passed.")
return 0


if __name__ == "__main__":
raise SystemExit(main())
8 changes: 7 additions & 1 deletion skills/loopx-pr-merge/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ authorized: a diff read, green CI, or pull-request metadata is not a substitute.
surface.
2. Apply `completion_gate` literally, and re-read the remote head immediately
before the decision.
3. Immediately before the merge, run `loopx --format json pr-review --goal-id
3. For this repository, run `python3 scripts/check_rfc_bilingual.py --base
origin/main --head HEAD` from the exact reviewed checkout before merge;
substitute the PR's actual target branch and verify local HEAD matches the
remote reviewed head. The source-checkout `loopx canary premerge` includes
the same direct blocking check. A failed check holds merge regardless of CI
waiting or authorized admin bypass. Working-tree changes cannot satisfy it.
4. Immediately before the merge, run `loopx --format json pr-review --goal-id
GOAL --repo OWNER/REPO --check-merge-readiness NUMBER@HEAD_OID`. Merge only
when it returns `ready=true` for that unchanged head; this records the
compact Goal readiness observation consumed by future review queues.
Expand Down
Loading
Loading