diff --git a/harness_manager/upgrade.py b/harness_manager/upgrade.py index e9d7ad7..f187ecf 100644 --- a/harness_manager/upgrade.py +++ b/harness_manager/upgrade.py @@ -108,7 +108,7 @@ def _new_loop_assets(src_agent: Path, dst_agent: Path) -> list[tuple[Path, Path] if not skill_dir.is_dir() or (dst_skills / skill_dir.name).exists(): continue for src in sorted(p for p in skill_dir.rglob("*") if p.is_file()): - actions.append((src, dst_skills / src.relative_to(src_agent))) + actions.append((src, dst_agent / src.relative_to(src_agent))) return actions diff --git a/tests/test_loop_integrations.py b/tests/test_loop_integrations.py index 01d4b1b..b277a98 100644 --- a/tests/test_loop_integrations.py +++ b/tests/test_loop_integrations.py @@ -42,6 +42,21 @@ def test_upgrade_adds_missing_loop_assets_but_preserves_authored_contract(tmp_pa assert (target / ".agent" / "runtime" / ".gitignore").exists() +def test_upgrade_copies_missing_loop_skills_to_the_correct_path(tmp_path: Path): + """A genuinely-missing loop-* skill must land at .agent/skills/loop-x/, + not .agent/skills/skills/loop-x/ (a doubled path segment regression: + the existing coverage above only ever pre-seeds loop-triage, so it + exercises the "already exists, skip" branch and never the fresh-copy + branch that builds the destination path). + """ + target = make_installed_project(tmp_path) + assert upgrade(target, ROOT, yes=True) == 0 + for name in ("loop-constraints", "loop-guard", "loop-triage", "loop-verifier"): + dst = target / ".agent" / "skills" / name / "SKILL.md" + assert dst.is_file(), f"expected {dst}, not doubled under skills/skills/" + assert not (target / ".agent" / "skills" / "skills").exists() + + def test_upgrade_does_not_copy_runtime_children_or_overwrite_loop_skills(tmp_path: Path): target = make_installed_project(tmp_path) skill = target / ".agent" / "skills" / "loop-triage"