ouroboros/scripts/cleanup_test_pollution.py
Ouroboros dcd7a5ed9e v5.15.0-rc.8: quality dx closed-loop skills wave on top of rc.7
Layered on top of rc.5..rc.7 catch-up reduction line. Preserves the
rc.7 prompt-injection fix in renderSkillRepairPrompt (web/modules/utils.js)
and the rc.6 docs/accounting cleanup. Adds the Quality, DX & Closed-loop
Skills wave from the user-approved finish plan:

- runtime_mode=light reframed as a minimal compatibility/self-modification
  guard. Path-aware shell filter (_light_shell_repo_mutation): blocks
  simple writer commands (cp, mv, rm, sed, sort -o, uniq, ...) only when
  their target resolves inside the Ouroboros checkout, blocks shell-
  wrapped writes via 'sh -c' / 'bash -c', blocks mutative direct git
  through run_shell, and otherwise lets ordinary python/node/bash
  diagnostics run. Removed the heavy Python AST scanner + script-content
  scan that was over-blocking legitimate work.
- Chat scroll fixes: ResizeObserver re-attach, near-bottom threshold,
  Playwright smoke now asserts scrollTop ~ scrollHeight after send.
- Recent-chat dedup: read_jsonl_tail_after_offset honours
  dialogue_meta.last_consolidated_offset; provenance-aware via a chat
  log generation signature (first_line_sha256 + size) so log rotation
  cannot silently drop entries.
- Stable WORLD.md injected into context.build_memory_sections.
- plan_task and the commit triad accept duplicate model IDs as valid
  reviewer slots (single-provider stochastic sampling); _get_review_models
  no longer pads 2-slot configs to 3.
- Settings: OUROBOROS_AUTO_GRANT_REVIEWED_SKILLS owner-confirmed via the
  desktop launcher bridge (request_auto_grant_reviewed_skills_change),
  hot-read from settings.json so toggle changes take effect without
  restart, /api/settings POST drops the key. Consistent truthy parsing
  on JS + launcher sides.
- skill_review.py runs an optional fail-open Claude Code advisory over
  the skill payload only (include_repo_diff=False), and injects its
  output as inert evidence BEFORE the authoritative output contract.
- skill_exec emits skill_exec_finished / skill_exec_failed events;
  worker enqueues them on ctx.event_queue, supervisor.events dispatches
  them to logs/events.jsonl + the live log + the in-process event bus
  for skill.lifecycle subscriptions. host_service_api allows manifest-
  declared skill.lifecycle subscriptions without an extra grant.
- run_shell auto-rewrites grep "A\|B" argv-mode to grep -E "A|B" with
  SHELL_REGEX_AUTO_CORRECTED prefix; explicit -E/-G/-P/-F still pass
  through. SAFE_SHELL_COMMANDS no longer includes sort/uniq.
- Extension loader hardening: load_extension(drive_root=...) is
  mandatory (no silent ~/Ouroboros/data fallback), TestClient lifespan
  + settings hot-reload pin to app.state.drive_root, _sweep_stale_-
  extension_imports preserves the live import root via a keep-list,
  fixture cleanup uses unload_extension. Adds clean_extension_runtime_-
  state superset helper and tests for cleanup, drive_root requirement,
  and live-root preservation.
- Test pollution: scripts/cleanup_test_pollution.py (dry-run-first
  utility), tests/_shared._make_safe_mock_ctx for advisory-workflow
  ctxs, _make_safe_mock_ctx adoption in test_advisory_workflow*.
- PluginAPI v1.2: PLUGIN_API_VERSION bumped, skill_job_dir added to
  the frozen Protocol + the contract test.
- docs/ARCHITECTURE.md, docs/CHECKLISTS.md, docs/CREATING_SKILLS.md,
  prompts/SYSTEM.md updated for new behavior (light-mode wording,
  cleanup script, skill.lifecycle topic, advisory pre-review path,
  duplicate reviewer slots, auto-grant settings + permissions wording).

VERSION 5.15.0-rc.8 / pyproject.toml 5.15.0rc8 / README badge + Version
History row + ARCHITECTURE.md header all in sync.
2026-05-11 03:32:41 +03:00

94 lines
3.4 KiB
Python

#!/usr/bin/env python3
"""Remove known test-pollution artifacts from the local Ouroboros data dir.
Dry-run by default. Use --apply after inspecting the planned removals.
"""
from __future__ import annotations
import argparse
import pathlib
import shutil
TEST_STATE_NAMES = {
"badui", "cleanup_ext", "crashy", "d1", "delayed_ghost_ext", "env_async_neighbor",
"env_async_owner", "env_import", "env_namespace", "env_overlap_neighbor",
"env_overlap_owner", "env_owner", "env_pth", "env_symlink", "env_untracked",
"ext1", "ext2", "ghost_ext", "hello", "pollui", "settings_ext", "settings_unload_race",
"staleish", "tree_ext", "uiwait", "ws1",
}
def _default_data_dir() -> pathlib.Path:
return pathlib.Path.home() / "Ouroboros" / "data"
def collect_targets(
data_dir: pathlib.Path,
*,
all_extension_imports: bool = False,
repo_dir: pathlib.Path | None = None,
) -> list[pathlib.Path]:
state_skills = data_dir / "state" / "skills"
targets: list[pathlib.Path] = []
if state_skills.exists():
for skill_dir in state_skills.iterdir():
if not skill_dir.is_dir():
continue
imports = skill_dir / "__extension_imports"
if imports.exists() and imports.is_dir() and (all_extension_imports or skill_dir.name in TEST_STATE_NAMES):
for child in imports.iterdir():
if child.is_dir():
targets.append(child)
if skill_dir.name in TEST_STATE_NAMES:
remaining = [p.name for p in skill_dir.iterdir() if p.name != "__extension_imports"]
safe_only = all(name in {"enabled.json", "review.json", "grants.json", "deps.json"} for name in remaining)
if safe_only:
targets.append(skill_dir)
if repo_dir and repo_dir.exists():
targets.extend(
path for path in repo_dir.iterdir()
if path.is_file() and "MagicMock" in path.name
)
return sorted(set(targets), key=lambda p: str(p))
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--data-dir", type=pathlib.Path, default=_default_data_dir())
parser.add_argument("--repo-dir", type=pathlib.Path, default=pathlib.Path.cwd())
parser.add_argument(
"--all-extension-imports",
action="store_true",
help=(
"also remove stale __extension_imports for non-test skill names; "
"use only while Ouroboros is stopped"
),
)
parser.add_argument("--dry-run", action="store_true", help="show planned removals without deleting")
parser.add_argument("--apply", action="store_true", help="actually delete planned targets")
args = parser.parse_args()
if args.dry_run and args.apply:
parser.error("--dry-run and --apply are mutually exclusive")
targets = collect_targets(
args.data_dir.expanduser(),
all_extension_imports=bool(args.all_extension_imports),
repo_dir=args.repo_dir.expanduser(),
)
action = "DELETE" if args.apply else "DRY-RUN"
if not targets:
print(f"{action}: no known test-pollution targets found")
return 0
for path in targets:
print(f"{action}: {path}")
if args.apply:
if path.is_dir():
shutil.rmtree(path, ignore_errors=True)
else:
path.unlink(missing_ok=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())