diff --git a/examples/repository-hygiene-smoke.py b/examples/repository-hygiene-smoke.py index a2988a33fa..76127ab427 100644 --- a/examples/repository-hygiene-smoke.py +++ b/examples/repository-hygiene-smoke.py @@ -26,6 +26,86 @@ FIRST_PUBLIC_RELEASE = (0, 1, 3) VERSION_TAG_RE = re.compile(r"^v(\d+)\.(\d+)\.(\d+)$") +CANONICAL_REPO = "loopx-project/loopx" +PRE_TRANSFER_REPO_URL = "github.com/huangruiteng/loopx" +OLD_ADDRESS_RE = re.compile( + r'github\.com/huangruiteng/loopx((?:/[^\s"<>)\],]*)?)' +) +# A surface is live by where it is, never by what else its text happens to +# contain: it hands an address to a user, a host or another tool at run time, +# or it is the command someone copies. +LIVE_SURFACE_PREFIXES = ("loopx/", "scripts/", ".github/workflows/", "packages/") +# A built bundle is regenerated, not edited, so its baked-in address is fixed by +# the release that rebuilds it. This is the tracked-build-output cost #4677 names. +GENERATED_ASSET_PREFIXES = ("loopx/web/chat/assets/",) +# Where the pre-transfer address is the reviewed-correct content, by path and by +# use: this project's own disambiguation terms must keep matching the archived +# address, and prose may cite the pull request an event happened under. +REVIEWED_ADDRESS_EXCEPTIONS: dict[str, frozenset[str]] = { + "packages/loopx-community-discussion/src/loopx_community_discussion/normalize.py": + frozenset({"repository", "issue"}), + "packages/loopx-community-discussion/smoke/community_discussion_smoke.py": + frozenset({"issue"}), + "loopx/capabilities/issue_fix/README.md": frozenset({"pull"}), + "loopx/capabilities/issue_fix/README.zh-CN.md": frozenset({"pull"}), + "packages/loopx-codex-provider-routing/RUNBOOK.md": frozenset({"pull"}), +} +DISAMBIGUATION_TERMS_SOURCE = ( + "packages/loopx-community-discussion/src/loopx_community_discussion/normalize.py" +) + + +def _is_live_surface(name: str) -> bool: + if name.startswith(GENERATED_ASSET_PREFIXES): + return False + return name.startswith(LIVE_SURFACE_PREFIXES) + + +def _address_use(raw_path: str) -> str: + """Classify one old-address occurrence by the path that follows it. + + Only the occurrence itself decides the use, so unrelated text in the same + file cannot turn an install command into a citation or the reverse. + """ + + segments = [part for part in raw_path.strip("/").split("/") if part] + if not segments: + return "repository" + lead = segments[0] + if lead == "issues": + return "issue" if len(segments) > 1 and segments[1].isdigit() else "issue_form" + if lead == "releases": + return "release_asset" + if lead == "discussions": + return "discussion" + if lead == "tree": + return "main_pointer" if len(segments) > 1 and segments[1] == "main" else "branch" + if lead.startswith("."): + return "repository" + return {"pull": "pull", "commit": "commit", "blob": "main_pointer"}.get( + lead, lead + ) + + +# A use is either a live pointer this project must own or a dated citation that +# may keep the address the event happened under. +LIVE_ADDRESS_USES = frozenset( + {"repository", "issue_form", "discussion", "release_asset", "main_pointer", "branch"}) + + +def stale_address_uses(name: str, text: str) -> list[str]: + """Return the old-address uses in a live surface that were never reviewed.""" + + tolerated = REVIEWED_ADDRESS_EXCEPTIONS.get(name, frozenset()) + return [ + use + for use in ( + _address_use(match.group(1) or "") + for match in OLD_ADDRESS_RE.finditer(text) + ) + if use not in tolerated + ] + def tracked_files() -> set[str]: completed = subprocess.run( @@ -101,6 +181,84 @@ def release_tags() -> list[str]: return tags + +def validate_canonical_repository_pointer() -> None: + """Fail when a live surface hands out the pre-transfer repository address. + + GitHub's redirect made the migration silent: a first-run link, a projected + documentation pointer, an install command or a provider's own relevance + terms could keep naming the previous owner while everything still resolved. + Exceptions are per path and per use, so a reviewed citation cannot be + reclassified by unrelated text in the same file, and a reviewed file cannot + hide an install command. + """ + offenders: list[str] = [] + for name in sorted(tracked_files()): + if not _is_live_surface(name): + continue + stale = stale_address_uses( + name, (REPO_ROOT / name).read_text(encoding="utf-8", errors="replace") + ) + if stale: + offenders.append(f"{name} ({', '.join(sorted(set(stale)))})") + if offenders: + raise AssertionError( + f"live surfaces must name the canonical {CANONICAL_REPO}; " + f"{PRE_TRANSFER_REPO_URL} still appears in: {offenders}" + ) + terms = (REPO_ROOT / DISAMBIGUATION_TERMS_SOURCE).read_text(encoding="utf-8") + for address in (CANONICAL_REPO, PRE_TRANSFER_REPO_URL.removeprefix("github.com/")): + if f"github.com/{address}" not in terms: + raise AssertionError( + f"project disambiguation terms dropped {address}; current and archived " + "pages must both classify as this project" + ) + _validate_stale_address_classifier() + + +def _validate_stale_address_classifier() -> None: + """Prove the classifier keys on path and use, not on surrounding prose.""" + + install = ( + "curl -L https://github.com/huangruiteng/loopx/releases/download/" + "pkg-v1/pkg.tgz -o pkg.tgz\n" + ) + if stale_address_uses("packages/dsh-loopx-plugin/README.md", install) != [ + "release_asset" + ]: + raise AssertionError( + "an install command under a package README must be named as a live " + "pre-transfer address" + ) + cited = ( + "See the README notes at #12 (https://github.com/huangruiteng/loopx/pull/12)\n" + ) + if stale_address_uses("packages/loopx-codex-provider-routing/RUNBOOK.md", cited): + raise AssertionError( + "a reviewed pull-request citation must stay tolerated even where the " + "same file mentions a README" + ) + if stale_address_uses("loopx/configuration_catalog.py", cited) != ["pull"]: + raise AssertionError( + "a reviewed exception for one path must not tolerate the same shape " + "elsewhere: a pull citation in a product module is still a live address" + ) + pointer = "https://github.com/huangruiteng/loopx/blob/main/docs/x.md\n" + if stale_address_uses("packages/loopx-community-discussion/README.md", pointer) != [ + "main_pointer" + ]: + raise AssertionError("a documentation pointer must be named as a live address") + if _is_live_surface("loopx/web/chat/assets/index-abc123.js"): + raise AssertionError( + "a generated bundle is outside the guard: its address is fixed by the " + "release that rebuilds it, not by hand-editing minified output" + ) + if not _is_live_surface("packages/dsh-loopx-plugin/README.md"): + raise AssertionError( + "an install command under a package README is a live surface" + ) + + def validate_release_timeline() -> None: if not RELEASE_TIMELINE.is_file(): raise AssertionError(f"missing release timeline: {RELEASE_TIMELINE.relative_to(REPO_ROOT)}") @@ -122,6 +280,7 @@ def main() -> int: files = tracked_files() validate_required_tracked_files(files) validate_public_private_boundary() + validate_canonical_repository_pointer() validate_release_timeline() print("repository-hygiene-smoke ok") return 0 diff --git a/loopx/agent_onboarding.py b/loopx/agent_onboarding.py index a861db2fd7..2f90aaf530 100644 --- a/loopx/agent_onboarding.py +++ b/loopx/agent_onboarding.py @@ -242,7 +242,7 @@ def _skill_delivery_contract( else {} ) ), - "source_repository": "https://github.com/huangruiteng/loopx", + "source_repository": "https://github.com/loopx-project/loopx", "source_directories": [ f"skills/{skill_id}" for skill_id in required_skill_ids diff --git a/loopx/capabilities/benchmark_toolkit/README.md b/loopx/capabilities/benchmark_toolkit/README.md index bb0d1e271a..93cf6ac9f7 100644 --- a/loopx/capabilities/benchmark_toolkit/README.md +++ b/loopx/capabilities/benchmark_toolkit/README.md @@ -1360,8 +1360,8 @@ All commands are local and no-upload by default. `benchmark-toolkit` grants no m Docker, runner, upload, submission, publication, or production authority. The active benchmark research program and current public-safe practice live under -[`benchmark/`](https://github.com/huangruiteng/loopx/blob/main/benchmark/README.md). Retired implementations, superseded runners, and dated research -packets are retained under [`deprecate/benchmark-legacy/`](https://github.com/huangruiteng/loopx/blob/main/deprecate/benchmark-legacy/README.md) +[`benchmark/`](https://github.com/loopx-project/loopx/blob/main/benchmark/README.md). Retired implementations, superseded runners, and dated research +packets are retained under [`deprecate/benchmark-legacy/`](https://github.com/loopx-project/loopx/blob/main/deprecate/benchmark-legacy/README.md) for source archaeology only. Immutable experiment snapshots follow the canonical [archive placement rules](../../../benchmark/README.md#archive-placement), diff --git a/loopx/capabilities/manager_runtime/machine_profile.py b/loopx/capabilities/manager_runtime/machine_profile.py index a4c93c5201..a00ad9e9cf 100644 --- a/loopx/capabilities/manager_runtime/machine_profile.py +++ b/loopx/capabilities/manager_runtime/machine_profile.py @@ -54,7 +54,7 @@ def manager_runtime_machine_configuration_namespace() -> MachineConfigurationNam documentation={ "path": "docs/architecture/rfcs/manager-runtime-profile-v0.md", "url": ( - "https://github.com/huangruiteng/loopx/blob/main/" + "https://github.com/loopx-project/loopx/blob/main/" "docs/architecture/rfcs/manager-runtime-profile-v0.md" ), }, diff --git a/loopx/capabilities/steward_executor/machine_defaults.py b/loopx/capabilities/steward_executor/machine_defaults.py index 35482c8a67..be373e8813 100644 --- a/loopx/capabilities/steward_executor/machine_defaults.py +++ b/loopx/capabilities/steward_executor/machine_defaults.py @@ -145,7 +145,7 @@ def steward_executor_machine_configuration_namespace() -> ( documentation={ "path": "docs/architecture/rfcs/harness-selection-dsh-pi-v0.md", "url": ( - "https://github.com/huangruiteng/loopx/blob/main/" + "https://github.com/loopx-project/loopx/blob/main/" "docs/architecture/rfcs/harness-selection-dsh-pi-v0.md" ), }, diff --git a/loopx/capabilities/value_connectors/planner.py b/loopx/capabilities/value_connectors/planner.py index 2b6ab53249..c39ff811ce 100644 --- a/loopx/capabilities/value_connectors/planner.py +++ b/loopx/capabilities/value_connectors/planner.py @@ -271,7 +271,7 @@ def build_value_connector_plan_fixture( channel="GitHub issue", stage="monitor", target_ref="public workflow intake issue", - target_url="https://github.com/huangruiteng/loopx/issues/670", + target_url="https://github.com/loopx-project/loopx/issues/670", access_mode="public_metadata_only", external_reads_allowed=True, value_axis="demand", @@ -286,7 +286,7 @@ def build_value_connector_plan_fixture( channel="GitHub discussion", stage="monitor", target_ref="public workflow discussion", - target_url="https://github.com/huangruiteng/loopx/discussions/673", + target_url="https://github.com/loopx-project/loopx/discussions/673", access_mode="public_metadata_only", external_reads_allowed=True, value_axis="demand", diff --git a/loopx/cli_commands/first_run_report.py b/loopx/cli_commands/first_run_report.py index 4b0f10f5d6..1a4f8d1f66 100644 --- a/loopx/cli_commands/first_run_report.py +++ b/loopx/cli_commands/first_run_report.py @@ -15,7 +15,7 @@ None, ] -FIRST_RUN_ISSUE_URL = "https://github.com/huangruiteng/loopx/issues/new" +FIRST_RUN_ISSUE_URL = "https://github.com/loopx-project/loopx/issues/new" FIRST_RUN_ISSUE_TEMPLATE = "first_run.yml" diff --git a/loopx/configuration_catalog.py b/loopx/configuration_catalog.py index 7448b42093..ee93f988bb 100644 --- a/loopx/configuration_catalog.py +++ b/loopx/configuration_catalog.py @@ -167,7 +167,7 @@ def build_goal_configuration_catalog( "documentation": { "path": "docs/quota-allocation.md#completed-todo-review-cadence", "url": ( - "https://github.com/huangruiteng/loopx/blob/main/" + "https://github.com/loopx-project/loopx/blob/main/" "docs/quota-allocation.md#completed-todo-review-cadence" ), }, @@ -216,7 +216,7 @@ def build_goal_configuration_catalog( "documentation": { "path": "docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md", "url": ( - "https://github.com/huangruiteng/loopx/blob/main/" + "https://github.com/loopx-project/loopx/blob/main/" "docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md" ), }, @@ -280,7 +280,7 @@ def build_goal_configuration_catalog( "documentation": { "path": "docs/integrations/codex-subagent-orchestration.md", "url": ( - "https://github.com/huangruiteng/loopx/blob/main/" + "https://github.com/loopx-project/loopx/blob/main/" "docs/integrations/codex-subagent-orchestration.md" ), }, @@ -341,7 +341,7 @@ def build_goal_configuration_catalog( "documentation": { "path": "docs/integrations/codex-subagent-orchestration.md", "url": ( - "https://github.com/huangruiteng/loopx/blob/main/" + "https://github.com/loopx-project/loopx/blob/main/" "docs/integrations/codex-subagent-orchestration.md" ), }, @@ -390,7 +390,7 @@ def build_goal_configuration_catalog( "documentation": { "path": "loopx/capabilities/explore/README.md", "url": ( - "https://github.com/huangruiteng/loopx/blob/main/" + "https://github.com/loopx-project/loopx/blob/main/" "loopx/capabilities/explore/README.md" ), }, @@ -443,7 +443,7 @@ def build_goal_configuration_catalog( "documentation": { "path": "loopx/capabilities/explore/README.md", "url": ( - "https://github.com/huangruiteng/loopx/blob/main/" + "https://github.com/loopx-project/loopx/blob/main/" "loopx/capabilities/explore/README.md" ), }, @@ -535,7 +535,7 @@ def build_goal_configuration_catalog( "documentation": { "path": "loopx/capabilities/change_quality/README.md", "url": ( - "https://github.com/huangruiteng/loopx/blob/main/" + "https://github.com/loopx-project/loopx/blob/main/" "loopx/capabilities/change_quality/README.md" ), }, @@ -630,7 +630,7 @@ def build_goal_configuration_catalog( "documentation": { "path": "loopx/capabilities/reward_memory/README.md", "url": ( - "https://github.com/huangruiteng/loopx/blob/main/" + "https://github.com/loopx-project/loopx/blob/main/" "loopx/capabilities/reward_memory/README.md" ), }, @@ -701,7 +701,7 @@ def build_goal_configuration_catalog( "documentation": { "path": "loopx/extensions/lark/docs/lark-event-inbox.md", "url": ( - "https://github.com/huangruiteng/loopx/blob/main/" + "https://github.com/loopx-project/loopx/blob/main/" "loopx/extensions/lark/docs/lark-event-inbox.md" ), }, @@ -758,7 +758,7 @@ def build_goal_configuration_catalog( "documentation": { "path": "docs/integrations/lark-kanban-control-plane-adapter.md", "url": ( - "https://github.com/huangruiteng/loopx/blob/main/" + "https://github.com/loopx-project/loopx/blob/main/" "docs/integrations/lark-kanban-control-plane-adapter.md" ), }, diff --git a/packages/dsh-loopx-plugin/README.md b/packages/dsh-loopx-plugin/README.md index 53a53d3b1a..75f9bd02de 100644 --- a/packages/dsh-loopx-plugin/README.md +++ b/packages/dsh-loopx-plugin/README.md @@ -58,7 +58,7 @@ Install the prebuilt release into the web profile: ```bash dsh plugin --profile web add \ - "https://github.com/huangruiteng/loopx/releases/download/dsh-loopx-plugin-v0.1.1-beta.5/dsh-loopx-plugin-0.1.1-beta.5.tgz" + "https://github.com/loopx-project/loopx/releases/download/dsh-loopx-plugin-v0.1.1-beta.5/dsh-loopx-plugin-0.1.1-beta.5.tgz" ``` The prebuilt release above retains its original DSH compatibility. This source diff --git a/packages/dsh-loopx-plugin/package.json b/packages/dsh-loopx-plugin/package.json index 64767d92ff..f70b9e9426 100644 --- a/packages/dsh-loopx-plugin/package.json +++ b/packages/dsh-loopx-plugin/package.json @@ -113,7 +113,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/huangruiteng/loopx.git", + "url": "git+https://github.com/loopx-project/loopx.git", "directory": "packages/dsh-loopx-plugin" }, "license": "Apache-2.0", diff --git a/packages/loopx-community-discussion/README.md b/packages/loopx-community-discussion/README.md index a28b5b2725..2bc90a832a 100644 --- a/packages/loopx-community-discussion/README.md +++ b/packages/loopx-community-discussion/README.md @@ -60,8 +60,8 @@ Direct CLI: ```bash loopx-community-discussion --doctor -loopx-community-discussion scan --owner huangruiteng --repo loopx --days 14 --format json -loopx-community-discussion scan --owner huangruiteng --repo loopx --days 14 --format md +loopx-community-discussion scan --owner loopx-project --repo loopx --days 14 --format json +loopx-community-discussion scan --owner loopx-project --repo loopx --days 14 --format md ``` `schemas/fact.schema.json`, `schemas/scan.schema.json`, and the request/response diff --git a/packages/loopx-community-discussion/examples/request.json b/packages/loopx-community-discussion/examples/request.json index 925c614f15..0fc240e206 100644 --- a/packages/loopx-community-discussion/examples/request.json +++ b/packages/loopx-community-discussion/examples/request.json @@ -1,6 +1,6 @@ { "schema_version": "loopx_community_discussion_request_v0", - "owner": "huangruiteng", + "owner": "loopx-project", "repo": "loopx", "days": 14 } diff --git a/packages/loopx-community-discussion/smoke/community_discussion_smoke.py b/packages/loopx-community-discussion/smoke/community_discussion_smoke.py index ab73a37528..dee12da70d 100644 --- a/packages/loopx-community-discussion/smoke/community_discussion_smoke.py +++ b/packages/loopx-community-discussion/smoke/community_discussion_smoke.py @@ -24,7 +24,7 @@ def _fixture() -> dict: external = make_fact( fact_type="external_discussion", source="github", - source_url="https://github.com/huangruiteng/loopx/issues/1", + source_url="https://github.com/loopx-project/loopx/issues/1", title="Question: how do I set a stop condition?", author="external-user", published_at="2026-08-17T00:00:00Z", @@ -32,7 +32,7 @@ def _fixture() -> dict: maintainer = make_fact( fact_type="maintainer_signal", source="github", - source_url="https://github.com/huangruiteng/loopx/issues/2", + source_url="https://github.com/loopx-project/loopx/issues/2", title="RFC: typed handoff packets", author="huangruiteng", published_at="2026-08-16T00:00:00Z", @@ -58,7 +58,7 @@ def _fixture() -> dict: duplicate = make_fact( fact_type="external_discussion", source="github", - source_url="https://github.com/huangruiteng/loopx/issues/1", + source_url="https://github.com/loopx-project/loopx/issues/1", title="Question: how do I set a stop condition?", author="external-user", published_at="2026-08-17T00:00:00Z", @@ -68,7 +68,7 @@ def _fixture() -> dict: "schema_version": SCAN_SCHEMA_VERSION, "scan_at": "2026-08-17T00:00:00+00:00", "window_days": 14, - "repo": {"owner": "huangruiteng", "name": "loopx"}, + "repo": {"owner": "loopx-project", "name": "loopx"}, "stats": { "raw_facts": 5, "deduped_facts": len(facts), @@ -80,7 +80,63 @@ def _fixture() -> dict: } +def _pre_and_post_transfer_addresses_are_both_strong() -> int: + """The move must not downgrade the project's own canonical pages.""" + + current = make_fact( + fact_type="external_discussion", + source="github", + source_url="https://github.com/loopx-project/loopx/issues/7", + title="Question from a new user", + author="external-user", + published_at="2026-09-19T00:00:00Z", + ) + archived = make_fact( + fact_type="external_discussion", + source="github", + source_url="https://github.com/huangruiteng/loopx/issues/8", + title="Archived question that still cites the old address", + author="external-user", + published_at="2026-09-18T00:00:00Z", + ) + generic = make_fact( + fact_type="external_discussion", + source="github", + source_url="https://github.com/example/other-tool/issues/3", + title="How do we schedule background jobs", + author="someone", + published_at="2026-09-16T00:00:00Z", + text="a question with no project identity in it", + ) + noise = make_fact( + fact_type="ecosystem_signal", + source="hacker_news", + source_url="https://news.ycombinator.com/item?id=9", + title="We loopear the backlog every sprint", + author="someone", + published_at="2026-09-17T00:00:00Z", + ) + for label, fact in (("current", current), ("archived", archived)): + if fact is None or fact["relevance"] != "strong": + print(f"FAIL: {label} project address should classify strong, got {fact}") + return 1 + if noise is not None: + print(f"FAIL: noise term must stay dropped, got {noise}") + return 1 + if generic is None or generic["relevance"] != "weak": + print( + "FAIL: a fact with no project identity must stay weak rather than be " + f"promoted or dropped, got {generic}" + ) + return 1 + return 0 + + def _run_offline() -> int: + status = _pre_and_post_transfer_addresses_are_both_strong() + if status: + return status + fixture = _fixture() if len(fixture["facts"]) != 4: print(f"FAIL: dedupe should collapse duplicate facts, got {len(fixture['facts'])}", file=sys.stderr) @@ -131,7 +187,7 @@ def _run_offline() -> int: return 1 md = render_markdown(fixture) - if "## External discussions" not in md or "https://github.com/huangruiteng/loopx/issues/1" not in md: + if "## External discussions" not in md or "https://github.com/loopx-project/loopx/issues/1" not in md: print("FAIL: markdown digest missing external discussion section", file=sys.stderr) return 1 if "## Public adoption & recommendations" not in md or "we-standardize-on-loopx" not in md: diff --git a/packages/loopx-community-discussion/src/loopx_community_discussion/normalize.py b/packages/loopx-community-discussion/src/loopx_community_discussion/normalize.py index b98a8d8e62..79c63f1f36 100644 --- a/packages/loopx-community-discussion/src/loopx_community_discussion/normalize.py +++ b/packages/loopx-community-discussion/src/loopx_community_discussion/normalize.py @@ -17,12 +17,16 @@ "loopear", ) +# Both repository addresses are strong: the project moved to `loopx-project` and +# archived pages, release notes and citations still name the pre-transfer one. STRONG_TERMS = ( "huangruiteng", "loop engineering", "agent control plane", "local-first agent", "github.com/huangruiteng/loopx", + "loopx-project", + "github.com/loopx-project/loopx", "loopx project", "loopx repo", ) diff --git a/packages/loopx-repo-health/examples/request.json b/packages/loopx-repo-health/examples/request.json index 77cef29374..a6b5095ecd 100644 --- a/packages/loopx-repo-health/examples/request.json +++ b/packages/loopx-repo-health/examples/request.json @@ -1,5 +1,5 @@ { - "owner": "huangruiteng", + "owner": "loopx-project", "repo": "loopx", "schema_version": "loopx_repo_health_request_v0" }