OpenClaw migration: source directory archiving and Hermes rebranding
Cluster 8188. Data as of
Nothing open on GitHub. Fix merged in #8210 on 12 Apr 2026. Check it covers your case.
The short answer
- Known?
- Yes. 3 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #8210. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #8210. That is separate from the fix event. - Still open
- Nothing open on GitHub.
Two of the three members concern the migration process not auto-archiving the OpenClaw source directory, with one also asking for coexistence guidance. The third proposes rebranding OpenClaw references to Hermes during migration, which is related to migration but a separate change.
Open issues0
Open PRs0
Fixes on record
-
#8210 feat: rebrand OpenClaw references to Hermes during migration
files:
docs/migration/openclaw.md,hermes_cli/claw.py,optional-skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py,tests/hermes_cli/test_claw.py,tests/skills/test_openclaw_migration.py,website/docs/guides/migrate-from-openclaw.md,website/docs/reference/cli-commands.mdShow the diff
-
docs/migration/openclaw.md@@ -118,7 +118,7 @@ For executed migrations, the full report is saved to `~/.hermes/migration/opencl ## Troubleshooting ### "OpenClaw directory not found" -The migration looks for `~/.openclaw` by default, then tries `~/.clawdbot` and `~/.moldbot`. If your OpenClaw is installed elsewhere, use `--source`: +The migration looks for `~/.openclaw` by default, then tries `~/.clawdbot` and `~/.moltbot`. If your OpenClaw is installed elsewhere, use `--source`: ```bash hermes claw migrate --source /path/to/.openclaw ``` -
hermes_cli/claw.py@@ -50,7 +50,7 @@ ) # Known OpenClaw directory names (current + legacy) -_OPENCLAW_DIR_NAMES = (".openclaw", ".clawdbot", ".moldbot") +_OPENCLAW_DIR_NAMES = (".openclaw", ".clawdbot", ".moltbot") def _warn_if_gateway_running(auto_yes: bool) -> None: """Check if a Hermes gateway is running with connected platforms. @@ -87,8 +87,8 @@ def _warn_if_gateway_running(auto_yes: bool) -> None: print_info("Migration cancelled. Stop the gateway and try again.") sys.exit(0) -# State files commonly found in OpenClaw workspace directories that cause -# confusion after migration (the agent discovers them and writes to them) +# State files commonly found in OpenClaw workspace directories — listed +# during cleanup to help the user decide whether to archive _WORKSPACE_STATE_GLOBS = ( "*/todo.json", "*/sessions/*", @@ -133,7 +133,7 @@ def _find_openclaw_dirs() -> list[Path]: def _scan_workspace_state(source_dir: Path) -> list[tuple[Path, str]]: - """Scan an OpenClaw directory for workspace state files that cause confusion. + """Scan an OpenClaw directory for workspace state files. Returns a list of (path, description) tuples. """ @@ -216,7 +216,7 @@ def _cmd_migrate(args): source_dir = Path.home() / ".openclaw" if not source_dir.is_dir(): # Try legacy directory names - for legacy in (".clawdbot", ".moldbot"): + for legacy in (".clawdbot", ".moltbot"): candidate = Path.home() / legacy if candidate.is_dir(): source_dir = candidate @@ -384,65 +384,16 @@ def _cmd_migrate(args): # Print results _print_migration_report(report, dry_run=False) - # After successful migration, offer to archive the source directory - if report.get("summary", {}).get("migrated", 0) > 0: - _offer_source_archival(source_dir, auto_yes) - - -def _offer_source_archival(source_dir: Path, auto_yes: bool = False): - """After migration, offer to rename the source directory to prevent state fragmentation. - - OpenClaw workspace directories contain state files (todo.json, sessions, etc.) - that the agent may discover and write to, causing confusion. Renaming the - directory prevents this. - """ - if not source_dir.is_dir(): - return - - # Scan for state files that could cause problems - state_files = _scan_workspace_state(source_dir) - - print() - print_header("Post-Migration Cleanup") - print_info("The OpenClaw directory still exists and contains workspace state files") - print_info("that can confuse the agent (todo lists, sessions, logs).") - if state_files: - print() - print(color(" Found state files:", Colors.YELLOW)) - # Show up to 10 most relevant findings - for path, desc in state_files[:10]: - print(f" {desc}") - if len(state_files) > 10: - print(f" ... and {len(state_files) - 10} more") - print() - print_info(f"Recommend: rename {source_dir.name}/ to {source_dir.name}.pre-migration/") - print_info("This prevents the agent from discovering old workspace directories.") - print_info("You can always rename it back if needed.") - print() - - if not auto_yes and not sys.stdin.isatty(): - print_info("Non-interactive session — skipping archival.") - print_info("Run later with: hermes claw cleanup") - return - - if auto_yes or prompt_yes_no(f"Archive {source_dir} now?", default=True): - try: - archive_path = _archive_directory(source_dir) - print_success(f"Archived: {source_dir} → {archive_path}") - print_info("The original directory has been renamed, not deleted.") - print_info(f"To undo: mv {archive_path} {source_dir}") - except OSError as e: - print_error(f"Could not archive: {e}") - print_info(f"You can do it manually: mv {source_dir} {source_dir}.pre-migration") - else: - print_info("Skipped. You can archive later with: hermes claw cleanup") + # Source directory is left untouched — archiving is not the migration + # tool's responsibility. Users who want to clean up can run + # 'hermes claw cleanup' separately. def _cmd_cleanup(args): """Archive leftover OpenClaw directories after migration. Scans for OpenClaw directories that still exist after migration and offers - to rename them to .pre-migration to prevent state fragmentation. + to rename them to .pre-migration to free disk space. """ dry_run = getattr(args, "dry_run", False) auto_yes = getattr(args, "yes", False) @@ -517,7 +468,7 @@ def _cmd_cleanup(args): if state_files: print() - print(color(f" {len(state_files)} state file(s) that could cause confusion:", Colors.YELLOW)) + print(color(f" {len(state_files)} state file(s) found:", Colors.YELLOW)) for path, desc in state_files[:8]: print(f" {desc}") if len(state_files) > 8: -
optional-skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py@@ -376,6 +376,24 @@ def backup_existing(path: Path, backup_root: Path) -> Optional[Path]: return dest +# ── Brand rewriting ───────────────────────────────────────── +# Replace OpenClaw brand names with Hermes in migrated text so that +# memory entries, user profiles, SOUL.md, and workspace instructions +# read as self-referential to the new agent identity. +_REBRAND_PATTERNS: List[Tuple[re.Pattern, str]] = [ + (re.compile(r'\bOpen[\s-]?Claw\b', re.IGNORECASE), 'Hermes'), + (re.compile(r'\bClawdBot\b', re.IGNORECASE), 'Hermes'), + (re.compile(r'\bMoltBot\b', re.IGNORECASE), 'Hermes'), +] + + +def rebrand_text(text: str) -> str: + """Replace OpenClaw / ClawdBot / MoltBot brand names with Hermes.""" + for pattern, replacement in _REBRAND_PATTERNS: + text = pattern.sub(replacement, text) + return text + + def parse_existing_memory_entries(path: Path) -> List[str]: if not path.exists(): return [] @@ -782,12 +800,13 @@ def write_overflow_entries(self, kind: str, entries: Sequence[str]) -> Optional[ path.write_text("\n".join(entries) + "\n", encoding="utf-8") return path - def copy_file(self, source: Path, destination: Path, kind: str) -> None: + def copy_file(self, source: Path, destination: Path, kind: str, + transform: Optional[Any] = None) -> None: if not source or not source.exists(): return if destination.exists(): - if sha256_file(source) == sha256_file(destination): + if not transform and sha256_file(source) == sha256_file(destination): self.record(kind, source, destination, "skipped", "Target already matches source") return if not self.overwrite: @@ -797,7 +816,13 @@ def copy_file(self, source: Path, destination: Path, kind: str) -> None: if self.execute: backup_path = self.maybe_backup(destination) ensure_parent(destination) - shutil.copy2(source, destination) + if transform: + content = read_text(source) + content = transform(content) + destination.write_text(content, encoding="utf-8") + shutil.copystat(source, destination) + else: + shutil.copy2(source, destination) self.record(kind, source, destination, "migrated", backup=str(backup_path) if backup_path else None) else: self.record(kind, source, destination, "migrated", "Would copy") @@ -807,7 +832,7 @@ def migrate_soul(self) -> None: if not source: self.record("soul", None, self.target_root / "SOUL.md", "skipped", "No OpenClaw SOUL.md found") return - self.copy_file(source, self.target_root / "SOUL.md", kind="soul") + self.copy_file(source, self.target_root / "SOUL.md", kind="soul", transform=rebrand_text) def migrate_workspace_agents(self) -> None: source = self.source_candidate( @@ -821,7 +846,7 @@ def migrate_workspace_agents(self) -> None: self.record("workspace-agents", source, None, "skipped", "No workspace target was provided") return destination = self.workspace_target / WORKSPACE_INSTRUCTIONS_FILENAME - self.copy_file(source, destination, kind="workspace-agents") + self.copy_file(source, destination, kind="workspace-agents", transform=rebrand_text) def migrate_memory(self, source: Optional[Path], destination: Path, limit: int, kind: str) -> None: if not source or not source.exists(): @@ -832,6 +857,7 @@ def migrate_memory(self, source: Optional[Path], destination: Path, limit: int, if not incoming: self.record(kind, source, destination, "skipped", "No importable entries found") return + incoming = [rebrand_text(entry) for entry in incoming] existing = parse_existing_memory_entries(destination) merged, stats, overflowed = merge_entries(existing, incoming, limit) @@ -927,7 +953,7 @@ def migrate_command_allowlist(self) -> None: def load_openclaw_config(self) -> Dict[str, Any]: # Check current name and legacy config filenames - for name in ("openclaw.json", "clawdbot.json", "moldbot.json"): + for name in ("openclaw.json", "clawdbot.json", "moltbot.json"): config_path = self.source_root / name if config_path.exists(): try: @@ -997,7 +1023,17 @@ def migrate_messaging_settings(self, config: Optional[Dict[str, Any]] = None) -> .get("workspace") ) if isinstance(workspace, str) and workspace.strip(): - additions["MESSAGING_CWD"] = workspace.strip() + ws_path = workspace.strip() + # Skip if the workspace points inside the OpenClaw source directory — + # that path will be stale after migration and would cause the Hermes + # gateway to use the old OpenClaw workspace as its cwd, picking up + # OpenClaw's AGENTS.md, MEMORY.md, etc. + try: + inside_source = Path(ws_path).resolve().is_relative_to(self.source_root.resolve()) + except (ValueError, OSError): + inside_source = False + if not inside_source: + additions["MESSAGING_CWD"] = ws_path allowlist_path = self.source_root / "credentials" / "telegram-default-allowFrom.json" if allowlist_path.exists(): @@ -1543,6 +1579,7 @@ def migrate_daily_memory(self) -> None: if not all_incoming: self.record("daily-memory", source_dir, destination, "skipped", "No importable entries found in daily memory files") return + all_incoming = [rebrand_text(entry) for entry in all_incoming] existing = parse_existing_memory_entries(destination) merged, stats, overflowed = merge_entries(existing, all_incoming, self.memory_limit) -
tests/hermes_cli/test_claw.py@@ -58,13 +58,13 @@ def test_finds_openclaw_dir(self, tmp_path): def test_finds_legacy_dirs(self, tmp_path): clawdbot = tmp_path / ".clawdbot" clawdbot.mkdir() - moldbot = tmp_path / ".moldbot" - moldbot.mkdir() + moltbot = tmp_path / ".moltbot" + moltbot.mkdir() with patch("pathlib.Path.home", return_value=tmp_path): found = claw_mod._find_openclaw_dirs() assert len(found) == 2 assert clawdbot in found - assert moldbot in found + assert moltbot in found def test_returns_empty_when_none_exist(self, tmp_path): with patch("pathlib.Path.home", return_value=tmp_path): @@ -297,7 +297,6 @@ def test_execute_with_confirmation(self, tmp_path, capsys): patch.object(claw_mod, "_load_migration_module", return_value=fake_mod), patch.object(claw_mod, "get_config_path", return_value=config_path), patch.object(claw_mod, "prompt_yes_no", return_value=True), - patch.object(claw_mod, "_offer_source_archival"), patch("sys.stdin", mock_stdin), ): claw_mod._cmd_migrate(args) @@ -306,43 +305,8 @@ def test_execute_with_confirmation(self, tmp_path, capsys): assert "Migration Results" in captured.out assert "Migration complete!" in captured.out - def test_execute_offers_archival_on_success(self, tmp_path, capsys): - """After successful migration, _offer_source_archival should be called.""" - openclaw_dir = tmp_path / ".openclaw" - openclaw_dir.mkdir() - - fake_mod = ModuleType("openclaw_to_hermes") - fake_mod.resolve_selected_options = MagicMock(return_value={"soul"}) - fake_migrator = MagicMock() - fake_migrator.migrate.return_value = { - "summary": {"migrated": 3, "skipped": 0, "conflict": 0, "error": 0}, - "items": [ - {"kind": "soul", "status": "migrated", "destination": str(tmp_path / "SOUL.md")}, - ], - } - fake_mod.Migrator = MagicMock(return_value=fake_migrator) - - args = Namespace( - source=str(openclaw_dir), - dry_run=False, preset="full", overwrite=False, - migrate_secrets=[redacted:secret-assignment] workspace_target=None, - skill_conflict="skip", yes=True, - ) - - with ( - patch.object(claw_mod, "_find_migration_script", return_value=tmp_path / "s.py"), - patch.object(claw_mod, "_load_migration_module", return_value=fake_mod), - patch.object(claw_mod, "get_config_path", return_value=tmp_path / "config.yaml"), - patch.object(claw_mod, "save_config"), - patch.object(claw_mod, "load_config", return_value={}), - patch.object(claw_mod, "_offer_source_archival") as mock_archival, - ): - claw_mod._cmd_migrate(args) - - mock_archival.assert_called_once_with(openclaw_dir, True) - - def test_dry_run_skips_archival(self, tmp_path, capsys): - """Dry run should not offer archival.""" + def test_dry_run_does_not_touch_source(self, tmp_path, capsys): + """Dry run should not modify the source directory.""" openclaw_dir = tmp_path / ".openclaw" openclaw_dir.mkdir() @@ -369,11 +333,10 @@ def test_dry_run_skips_archival(self, tmp_path, capsys): patch.object(claw_mod, "get_config_path", return_value=tmp_path / "config.yaml"), patch.object(claw_mod, "save_config"), patch.object(claw_mod, "load_config", return_value={}), - patch.object(claw_mod, "_offer_source_archival") as mock_archival, ): claw_mod._cmd_migrate(args) - mock_archival.assert_not_called() + assert openclaw_dir.is_dir() # Source untouched def test_execute_cancelled_by_user(self, tmp_path, capsys): openclaw_dir = tmp_path / ".openclaw" @@ -506,73 +469,6 @@ def test_full_preset_enables_secrets(self, tmp_path, capsys): assert call_kwargs["migrate_secrets"] is True -# --------------------------------------------------------------------------- -# _offer_source_archival -# --------------------------------------------------------------------------- - - -class TestOfferSourceArchival: - """Test the post-migration archival offer.""" - - def test_archives_with_auto_yes(self, tmp_path, capsys): - source = tmp_path / ".openclaw" - source.mkdir() - (source / "workspace").mkdir() - (source / "workspace" / "todo.json").write_text("{}") - - claw_mod._offer_source_archival(source, auto_yes=True) - - captured = capsys.readouterr() - assert "Archived" in captured.out - assert not source.exists() - assert (tmp_path / ".openclaw.pre-migration").is_dir() - - def test_skips_when_user_declines(self, tmp_path, capsys): - source = tmp_path / ".openclaw" - source.mkdir() - - mock_stdin = MagicMock() - mock_stdin.isatty.return_value = True - - with ( - patch.object(claw_mod, "prompt_yes_no", return_value=False), - patch("sys.stdin", mock_stdin), - ): - claw_mod._offer_source_archival(source, auto_yes=False) - - captured = capsys.readouterr() - assert "Skipped" in captured.out - assert source.is_dir() # Still exists - - def test_noop_when_source_missing(self, tmp_path, capsys): - claw_mod._offer_source_archival(tmp_path / "nonexistent", auto_yes=True) - captured = capsys.readouterr() - assert captured.out == "" # No output - - def test_shows_state_files(self, tmp_path, capsys): - source = tmp_path / ".openclaw" - source.mkdir() - ws = source / "workspace" - ws.mkdir() - (ws / "todo.json").write_text("{}") - - with patch.object(claw_mod, "prompt_yes_no", return_value=False): - claw_mod._offer_source_archival(source, auto_yes=False) - - captured = capsys.readouterr() - assert "todo.json" in captured.out - - def test_handles_archive_error(self, tmp_path, capsys): - source = tmp_path / ".openclaw" - source.mkdir() - - with patch.object(claw_mod, "_archive_directory", side_effect=OSError("permission denied")): - claw_mod._offer_source_archival(source, auto_yes=True) - - captured = capsys.readouterr() - assert "Could not archive" in captured.out - - # --------------------------------------------------------------------------- # _cmd_cleanup # --------------------------------------------------------------------------- -
tests/skills/test_openclaw_migration.py@@ -185,6 +185,38 @@ def test_migrator_optionally_imports_supported_secrets_and_messaging_settings(tm assert "TELEGRAM_BOT_TOKEN=[redacted:secret-assignment]" in env_text +def test_messaging_cwd_skipped_when_inside_source(tmp_path: Path): + """MESSAGING_CWD pointing inside the OpenClaw source dir should be skipped.""" + mod = load_module() + source = tmp_path / ".openclaw" + target = tmp_path / ".hermes" + target.mkdir() + + # Workspace path is inside the source directory + ws_path = str(source / "workspace") + (source / "credentials").mkdir(parents=True) + (source / "openclaw.json").write_text( + json.dumps({"agents": {"defaults": {"workspace": ws_path}}}), + encoding="utf-8", + ) + + migrator = mod.Migrator( + source_root=source, + target_root=target, + execute=True, + workspace_target=None, + overwrite=False, + migrate_secrets=True, + output_dir=target / "migration-report", + selected_options={"messaging-settings"}, + ) + migrator.migrate() + + env_path = target / ".env" + if env_path.exists(): + assert "MESSAGING_CWD" not in env_path.read_text(encoding="utf-8") + + def test_migrator_can_execute_only_selected_categories(tmp_path: Path): mod = load_module() source = tmp_path / ".openclaw" @@ -722,3 +754,98 @@ def test_skill_installs_cleanly_under_skills_guard(): KNOWN_FALSE_POSITIVES = {"agent_config_mod", "python_os_environ", "hermes_config_mod"} for f in result.findings: assert f.pattern_id in KNOWN_FALSE_POSITIVES, f"Unexpected finding: {f}" + + +# ── rebrand_text tests ──────────────────────────────────────── + + +def test_rebrand_text_replaces_openclaw_variants(): + mod = load_module() + assert mod.rebrand_text("OpenClaw prefers Python 3.11") == "Hermes prefers Python 3.11" + assert mod.rebrand_text("I told Open Claw to use dark mode") == "I told Hermes to use dark mode" + assert mod.rebrand_text("Open-Claw config is great") == "Hermes config is great" + assert mod.rebrand_text("openclaw should always respond concisely") == "Hermes should always respond concisely" + assert mod.rebrand_text("OPENCLAW uses tools well") == "Hermes uses tools well" + + +def test_rebrand_text_replaces_legacy_bot_names(): + mod = load_module() + assert mod.rebrand_text("ClawdBot remembers my timezone") == "Hermes remembers my timezone" + assert mod.rebrand_text("clawdbot prefers tabs") == "Hermes prefers tabs" + assert mod.rebrand_text("MoltBot was configured for Spanish") == "Hermes was configured for Spanish" + assert mod.rebrand_text("moltbot uses Python") == "Hermes uses Python" + + +def test_rebrand_text_preserves_unrelated_content(): + mod = load_module() + text = "User prefers dark mode and lives in Las Vegas" + assert mod.rebrand_text(text) == text + + +def test_rebrand_text_handles_multiple_replacements(): + mod = load_module() + text = "OpenClaw said to ask ClawdBot about MoltBot settings" + assert mod.rebrand_text(text) == "Hermes said to ask Hermes about Hermes settings" + + +def test_migrate_memory_rebrands_entries(tmp_path): + mod = load_module() + source_root = tmp_path / "openclaw" + source_root.mkdir() + workspace = source_root / "workspace" + workspace.mkdir() + memory_md = workspace / "MEMORY.md" + memory_md.write_text( + "# Memory\n\n- OpenClaw should use Python 3.11\n- ClawdBot prefers dark mode\n", + encoding="utf-8", + ) + + target_root = tmp_path / "hermes" + target_root.mkdir() + (target_root / "memories").mkdir() + + migrator = mod.Migrator( + source_root=source_root, + target_root=target_root, + execute=True, + workspace_target=None, + overwrite=False, + migrate_secrets=[redacted:secret-assignment] + output_dir=tmp_path / "report", + selected_options={"memory"}, + ) + migrator.migrate() + + result = (target_root / "memories" / "MEMORY.md").read_text(encoding="utf-8") + assert "OpenClaw" not in result + assert "ClawdBot" not in result + assert "Hermes" in result + + +def test_migrate_soul_rebrands_content(tmp_path): + mod = load_module() + source_root = tmp_path / "openclaw" + source_root.mkdir() + workspace = source_root / "workspace" + workspace.mkdir() + soul_md = workspace / "SOUL.md" + soul_md.write_text("You are OpenClaw, an AI assistant made by SparkLab.", encoding="utf-8") + + target_root = tmp_path / "hermes" + target_root.mkdir() + + migrator = mod.Migrator( + source_root=source_root, + target_root=target_root, + execute=True, + workspace_target=None, + overwrite=False, + migrate_secrets=[redacted:secret-assignment] + output_dir=tmp_path / "report", + selected_options={"soul"}, + ) + migrator.migrate() + + result = (target_root / "SOUL.md").read_text(encoding="utf-8") + assert "OpenClaw" not in result + assert "You are Hermes" in result -
website/docs/guides/migrate-from-openclaw.md@@ -23,7 +23,7 @@ hermes claw migrate --preset full --yes The migration always shows a full preview of what will be imported before making any changes. Review the list, then confirm to proceed. -Reads from `~/.openclaw/` by default. Legacy `~/.clawdbot/` or `~/.moldbot/` directories are detected automatically. Same for legacy config filenames (`clawdbot.json`, `moldbot.json`). +Reads from `~/.openclaw/` by default. Legacy `~/.clawdbot/` or `~/.moltbot/` directories are detected automatically. Same for legacy config filenames (`clawdbot.json`, `moltbot.json`). ## Options @@ -234,7 +234,7 @@ The migration resolves all three formats. For env templates and SecretRef object ### "OpenClaw directory not found" -The migration checks `~/.openclaw/`, then `~/.clawdbot/`, then `~/.moldbot/`. If your installation is elsewhere, use `--source /path/to/your/openclaw`. +The migration checks `~/.openclaw/`, then `~/.clawdbot/`, then `~/.moltbot/`. If your installation is elsewhere, use `--source /path/to/your/openclaw`. ### "No provider API keys found" -
website/docs/reference/cli-commands.md@@ -660,7 +660,7 @@ hermes insights [--days N] [--source platform] hermes claw migrate [options] ``` -Migrate your OpenClaw setup to Hermes. Reads from `~/.openclaw` (or a custom path) and writes to `~/.hermes`. Automatically detects legacy directory names (`~/.clawdbot`, `~/.moldbot`) and config filenames (`clawdbot.json`, `moldbot.json`). +Migrate your OpenClaw setup to Hermes. Reads from `~/.openclaw` (or a custom path) and writes to `~/.hermes`. Automatically detects legacy directory names (`~/.clawdbot`, `~/.moltbot`) and config filenames (`clawdbot.json`, `moltbot.json`). | Option | Description | |--------|-------------|
-
1 PR closed without merging
From the close comments: merged via another change 1
-
#8192 fix(migration): don't auto-archive OpenClaw source directory · merged via another change →
#8210
Closed issues · 1
- Closed#8188Migration should not auto-archive source directory; needs coexistence guidance
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Merged PRs
- #8210
- Closed issues
- 1
- Attempts
- 1 PR closed without merging
- Fix link checked
- 12 Apr 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.