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 docs/reference/protocols/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Current contracts:
- [todo_detail_cold_path_v0](todo-detail-cold-path-v0.md)
- [long_horizon_agent_state_protocol_v0](long-horizon-agent-state-protocol-v0.md)
- [global_manager_command_v0](global-manager-command-v0.md)
- [pr_review_command_v0](pr-review-command-v0.md)
- [event_sourced_state_contract_v0](event-sourced-state-contract-v0.md)
- [rollback_packet_v0](rollback-packet-v0.md)
- [content_ops_surface_v0](content-ops-surface-v0.md)
Expand Down
6 changes: 6 additions & 0 deletions docs/reference/protocols/global-manager-command-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Recommended first commands:
| `/loopx-global-gates` | Show open user/controller gates and what each blocks. | current state |
| `/loopx-global-todos` | Show top runnable, blocked, deferred-ready, and review todos. | current state |
| `/loopx-global-risks` | Show stale runs, public/private boundary warnings, failing checks, and rollback candidates. | 24 hours |
| `/loopx-pr-review` | Walk the current project's or explicit repository's unmerged GitHub PRs one by one with motivation, scope, checks, risks, and review prompts. | current open PRs |
| `/loop-goal-summary <goal id>` | Drill into one goal without scanning unrelated projects. | 24 hours |

Commands are read-only by default. They can propose follow-up actions, but
Expand All @@ -46,6 +47,11 @@ Related project-local command: `/loopx <goal text>` is covered by
command: it starts one project goal, plans ranked todos, writes them in order,
and then enters the quota-gated automation flow.

Related repo-review command: `/loopx-pr-review` is covered by
[`pr_review_command_v0`](pr-review-command-v0.md). It is read-only and helps a
human review open PRs in the caller's current project or an explicit
`--repo owner/repo` target; it does not approve, comment, merge, or spend quota.

## Request Shape

```json
Expand Down
115 changes: 115 additions & 0 deletions docs/reference/protocols/pr-review-command-v0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# pr_review_command_v0

`pr_review_command_v0` defines the `/loopx-pr-review` command. It helps a
user review unmerged pull requests one by one by turning public GitHub PR
metadata into a guided review queue.

The reviewed repository is the caller's current GitHub project by default, as
resolved by `gh`, or the explicit `--repo owner/repo` target. LoopX's own
repository may be used for dogfood and public fixtures, but the command is not
LoopX-repo-specific.

The command is read-only. It does not approve reviews, post PR comments, merge,
push, spend LoopX quota, or mark LoopX todos complete.

## Command

| Command | CLI reference | Intent |
| --- | --- | --- |
| `/loopx-pr-review` | `loopx pr-review [--repo owner/repo]` | List open PRs for the current project or explicit repository and build a guided review packet with motivation, change scope, checks, risks, and review prompts. |

## Source Reads

Implementations may read compact public PR surfaces:

- pull request title, number, URL, branch, author, state, and review decision;
- PR body summary;
- changed-file list and diff scale;
- commit headlines;
- status-check rollup;
- merge-state metadata.

They must not include raw logs, private connector payloads, credentials, local
absolute paths, private source bodies, or hidden CI artifacts.

## Response Shape

`loopx_pr_review_command_response_v0`:

```json
{
"schema_version": "loopx_pr_review_command_response_v0",
"request": {
"schema_version": "loopx_pr_review_command_request_v0",
"command": "/loopx-pr-review",
"cli_command": "loopx pr-review [--repo owner/repo]",
"repository": "owner/repo",
"limit": 10,
"source": "github_cli",
"privacy_mode": "public_safe_github_metadata",
"dry_run": true
},
"summary": {
"headline": "5 open PR(s) found; 5 need review attention.",
"open_pr_count": 5,
"review_attention_count": 5,
"draft_count": 0,
"recommended_first_pr": {
"rank": 1,
"number": 773,
"review_depth": "docs_and_smoke_review"
}
},
"review_sequence": [
{
"rank": 1,
"number": 773,
"title": "docs: add newcomer command path",
"url": "https://github.com/owner/repo/pull/773",
"review_depth": "docs_and_smoke_review",
"why_now": "Open and awaiting reviewer decision."
}
],
"pull_requests": [
{
"number": 773,
"motivation": "Adds a newcomer command path...",
"scale": {"changed_files": 3, "additions": 90, "deletions": 4},
"areas": {"public_docs": 3},
"checks": {"summary": "2 successful check(s)."},
"risk_notes": [],
"review_prompts": [
"What user or maintainer value does this PR unlock now?"
]
}
],
"boundary": {
"raw_logs_recorded": false,
"credential_values_recorded": false,
"absolute_paths_recorded": false
}
}
```

## Review Flow

The packet should let a reviewer move through PRs in order:

1. Read the motivation and decide whether the PR should exist.
2. Compare the touched areas and key files with that scope.
3. Inspect validation and risk notes before approval or merge handoff.
4. Decide `approve`, `request changes`, `defer`, or `merge after checks`.

## Acceptance Checks

A first implementation is acceptable when:

- `loopx slash-commands` exposes `/loopx-pr-review`;
- `loopx pr-review` returns `loopx_pr_review_command_response_v0`;
- default live reads use the caller's current `gh` repository, while
`--repo owner/repo` can review another GitHub project;
- the response includes review sequence, motivation, changed-file scope,
status checks, risk notes, and review prompts;
- live GitHub reads and fixture-based smokes share the same schema;
- no raw logs, private payloads, credentials, local paths, or private source
bodies are recorded.
88 changes: 88 additions & 0 deletions examples/fixtures/pr-review.public.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
{
"repository": "huangruiteng/loopx",
"pull_requests": [
{
"number": 773,
"title": "docs: add newcomer command path",
"url": "https://github.com/huangruiteng/loopx/pull/773",
"author": {"login": "huangruiteng"},
"baseRefName": "main",
"headRefName": "codex/side-bypass-command-surface-20260627",
"updatedAt": "2026-06-27T12:21:43Z",
"isDraft": false,
"reviewDecision": "REVIEW_REQUIRED",
"mergeStateStatus": "CLEAN",
"body": "Adds a newcomer command path so first-time users see /loopx and one quickstart before the full command catalog. It also adds manager-view commands as second-layer discovery.",
"changedFiles": 3,
"additions": 90,
"deletions": 4,
"files": [
{"path": "docs/guides/newcomer-command-path.md", "additions": 75, "deletions": 0},
{"path": "docs/README.md", "additions": 8, "deletions": 2},
{"path": "docs/guides/getting-started.md", "additions": 7, "deletions": 2}
],
"commits": [
{"messageHeadline": "docs: add newcomer command path"},
{"messageHeadline": "docs: mention global manager slash commands"}
],
"statusCheckRollup": [
{"name": "build", "status": "COMPLETED", "conclusion": "SUCCESS"},
{"name": "docs-smoke", "status": "COMPLETED", "conclusion": "SUCCESS"}
]
},
{
"number": 771,
"title": "docs: add public showcase pages and experimental value path",
"url": "https://github.com/huangruiteng/loopx/pull/771",
"author": {"login": "huangruiteng"},
"baseRefName": "main",
"headRefName": "codex/hardware-canonical-showcases",
"updatedAt": "2026-06-27T12:18:59Z",
"isDraft": false,
"reviewDecision": "REVIEW_REQUIRED",
"mergeStateStatus": "CLEAN",
"body": "Adds public showcase pages, preserves the hardware workflow page as the quality bar, and places the Today Value Path as an experimental section below the first screen.",
"changedFiles": 9,
"additions": 740,
"deletions": 42,
"files": [
{"path": "README.md", "additions": 20, "deletions": 5},
{"path": "README.zh-CN.md", "additions": 20, "deletions": 5},
{"path": "docs/showcases/index.html", "additions": 150, "deletions": 20},
{"path": "docs/showcases/showcase-catalog.json", "additions": 190, "deletions": 5},
{"path": "apps/dashboard/src/views/frontstage-page.tsx", "additions": 80, "deletions": 4}
],
"commits": [
{"messageHeadline": "docs: require first-screen owner review"},
{"messageHeadline": "docs: add experimental today value path"}
],
"statusCheckRollup": [
{"name": "build", "status": "COMPLETED", "conclusion": "SUCCESS"},
{"name": "showcase-smoke", "status": "COMPLETED", "conclusion": "SUCCESS"}
]
},
{
"number": 775,
"title": "draft: explore PR review dashboard cards",
"url": "https://github.com/huangruiteng/loopx/pull/775",
"author": {"login": "huangruiteng"},
"baseRefName": "main",
"headRefName": "codex/pr-review-dashboard-cards",
"updatedAt": "2026-06-27T12:30:00Z",
"isDraft": true,
"reviewDecision": "REVIEW_REQUIRED",
"mergeStateStatus": "UNKNOWN",
"body": "Explores a dashboard card view for PR review packets.",
"changedFiles": 1,
"additions": 40,
"deletions": 0,
"files": [
{"path": "apps/dashboard/src/views/pr-review-card.tsx", "additions": 40, "deletions": 0}
],
"commits": [
{"messageHeadline": "draft: explore PR review cards"}
],
"statusCheckRollup": []
}
]
}
79 changes: 79 additions & 0 deletions examples/pr-review-command-smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Smoke-test the public-safe `loopx pr-review` command."""

from __future__ import annotations

import json
import re
import subprocess
import sys
from pathlib import Path


REPO_ROOT = Path(__file__).resolve().parents[1]
FIXTURE = REPO_ROOT / "examples" / "fixtures" / "pr-review.public.json"
PRIVATE_PATTERNS = [
re.compile(r"/" + r"Users/[A-Za-z0-9._-]+/"),
re.compile(r"/" + r"private/"),
re.compile(r"/tmp/"),
re.compile(r"/var/"),
re.compile(r"[A-Za-z]:\\\\Users\\\\"),
]


def run_cli(*args: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, "-m", "loopx.cli", *args],
cwd=REPO_ROOT,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=True,
)


def assert_public_safe(payload: dict[str, object]) -> None:
text = json.dumps(payload, ensure_ascii=False)
for pattern in PRIVATE_PATTERNS:
if pattern.search(text):
raise AssertionError(f"pr-review payload leaked private pattern {pattern.pattern!r}")


def main() -> int:
payload = json.loads(
run_cli("--format", "json", "pr-review", "--fixture", str(FIXTURE), "--limit", "5").stdout
)
assert payload["schema_version"] == "loopx_pr_review_command_response_v0", payload
request = payload["request"]
assert request["command"] == "/loopx-pr-review", request
assert request["cli_command"] == "loopx pr-review [--repo owner/repo]", request
assert request["privacy_mode"] == "public_safe_github_metadata", request
assert request["dry_run"] is True, request
assert request["repository"] == "huangruiteng/loopx", request
assert payload["summary"]["open_pr_count"] == 3, payload["summary"]
assert payload["summary"]["review_attention_count"] == 3, payload["summary"]
assert payload["summary"]["draft_count"] == 1, payload["summary"]
sequence = payload["review_sequence"]
assert sequence[0]["number"] == 773, sequence
assert sequence[-1]["number"] == 775, sequence
first = payload["pull_requests"][0]
assert first["number"] == 773, first
assert "newcomer command path" in first["motivation"], first
assert first["checks"]["counts"]["success"] == 2, first["checks"]
assert "public_docs" in first["areas"], first["areas"]
assert payload["boundary"]["absolute_paths_recorded"] is False, payload["boundary"]
assert_public_safe(payload)

markdown = run_cli("pr-review", "--fixture", str(FIXTURE), "--limit", "1").stdout
assert "# Project PR Review Queue" in markdown, markdown
assert "current gh repository" not in markdown, markdown
assert "## Review Sequence" in markdown, markdown
assert "PR #773" in markdown, markdown
assert "review prompts" in markdown, markdown

print("pr-review-command-smoke ok")
return 0


if __name__ == "__main__":
raise SystemExit(main())
3 changes: 3 additions & 0 deletions examples/slash-command-catalog-smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def main() -> int:
"/loopx-global-gates",
"/loopx-global-todos",
"/loopx-global-risks",
"/loopx-pr-review",
]:
assert command in commands, commands
assert "/loop-global-summary" in commands["/loopx-global-summary"]["legacy_aliases"]
Expand All @@ -51,6 +52,8 @@ def main() -> int:
assert "# LoopX Slash Commands" in markdown, markdown
assert "`/loopx-global-summary`" in markdown, markdown
assert "`loopx global-summary`" in markdown, markdown
assert "`/loopx-pr-review`" in markdown, markdown
assert "`loopx pr-review [--repo owner/repo]`" in markdown, markdown
assert "`/loopx-summary-all`" not in markdown, markdown

top_help = run_cli("--help").stdout
Expand Down
11 changes: 11 additions & 0 deletions loopx/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
handle_lark_kanban_command,
handle_ml_experiment_command,
handle_project_lifecycle_command,
handle_pr_review_command,
handle_quota_command,
handle_registry_admin_command,
handle_review_packet_command,
Expand All @@ -49,6 +50,7 @@
register_lark_kanban_commands,
register_ml_experiment_commands,
register_project_lifecycle_commands,
register_pr_review_command,
register_quota_command,
register_registry_admin_commands,
register_slash_commands_command,
Expand Down Expand Up @@ -150,6 +152,7 @@ def main(argv: list[str] | None = None) -> int:

register_status_commands(sub, add_subcommand_format)
register_summary_all_command(sub, add_subcommand_format)
register_pr_review_command(sub, add_subcommand_format)
register_slash_commands_command(sub, add_subcommand_format)
register_dreaming_commands(sub, add_subcommand_format)
register_todo_command(sub)
Expand Down Expand Up @@ -344,6 +347,14 @@ def main(argv: list[str] | None = None) -> int:
if summary_all_result is not None:
return summary_all_result

pr_review_result = handle_pr_review_command(
args,
output_format=output_format,
print_payload=print_payload,
)
if pr_review_result is not None:
return pr_review_result

slash_commands_result = handle_slash_commands_command(
args,
output_format=output_format,
Expand Down
3 changes: 3 additions & 0 deletions loopx/cli_commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
handle_project_lifecycle_command,
register_project_lifecycle_commands,
)
from .pr_review import handle_pr_review_command, register_pr_review_command
from .quota import handle_quota_command, register_quota_command
from .registry_admin import (
handle_registry_admin_command,
Expand Down Expand Up @@ -204,6 +205,7 @@
"handle_ml_experiment_command",
"handle_new_project_prompt_command",
"handle_project_lifecycle_command",
"handle_pr_review_command",
"handle_quota_command",
"handle_registry_admin_command",
"handle_review_packet_command",
Expand Down Expand Up @@ -245,6 +247,7 @@
"register_lark_kanban_commands",
"register_ml_experiment_commands",
"register_project_lifecycle_commands",
"register_pr_review_command",
"register_quota_command",
"register_registry_admin_commands",
"register_slash_commands_command",
Expand Down
Loading