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
10 changes: 10 additions & 0 deletions src/hyrule_engineering_loop/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,10 @@ def daemon_command(args: argparse.Namespace) -> int:
memory_dir=args.memory_dir,
max_runs_per_day=args.max_runs_per_day,
max_cost_usd_per_day=args.max_cost_usd_per_day,
allowed_paths_by_repo={
repo: tuple(prefixes)
for repo, prefixes in _parse_repo_paths(args.allow, option="--allow").items()
},
)
report = daemon_once(config, client=GhCli())
print(json.dumps(report.as_dict(), indent=2, sort_keys=True))
Expand Down Expand Up @@ -828,6 +832,12 @@ def build_parser() -> argparse.ArgumentParser:
daemon_parser.add_argument(
"--max-cost-usd-per-day", type=float, default=DaemonConfig.max_cost_usd_per_day
)
daemon_parser.add_argument(
"--allow",
action="append",
metavar="REPO=PATH_PREFIX",
help="widen allowed write paths for a repo (default: docs only). Repeatable.",
)
daemon_parser.set_defaults(func=daemon_command)

intake_parser = subparsers.add_parser("intake", help="signal mining and triage inbox")
Expand Down
10 changes: 8 additions & 2 deletions src/hyrule_engineering_loop/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ class DaemonConfig:
state_dir: Path = Path(".engineering-loop-state/daemon")
memory_dir: str | None = None
allowed_paths: tuple[str, ...] = ("docs",)
# Per-repo override of allowed_paths, keyed by sibling checkout name
# (see repo_name_for_issue). Falls back to allowed_paths (docs-only) for any
# repo not listed, so the daemon stays docs-only unless explicitly widened.
allowed_paths_by_repo: dict[str, tuple[str, ...]] = field(default_factory=dict)
remote: str = "origin"
max_runs_per_day: int = 2
max_cost_usd_per_day: float = 10.0
Expand Down Expand Up @@ -399,14 +403,16 @@ def daemon_once(
)

runner = feature_runner or run_feature_intake
repo_name = repo_name_for_issue(item)
effective_allowed_paths = list(config.allowed_paths_by_repo.get(repo_name, config.allowed_paths))
result = runner(
change_id=change_id,
change_class=change_class,
workspace_root=config.workspace_root,
output_root=output_root,
repo_name=repo_name_for_issue(item),
repo_name=repo_name,
request_path=request_path,
allowed_paths=list(config.allowed_paths),
allowed_paths=effective_allowed_paths,
source_files=["README.md"],
memory_dir=config.memory_dir,
backend_budget={
Expand Down
37 changes: 37 additions & 0 deletions tests/test_phase24_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,43 @@ def test_daemon_defaults_to_core_repos_and_low_and_slow_budget() -> None:
assert config.max_runs_per_day == 2
assert config.max_cost_usd_per_day == 10.0
assert config.allowed_paths == ("docs",)
assert config.allowed_paths_by_repo == {}


def _capture_allowed_paths(tmp_path: Path, config_kwargs: dict[str, Any], repo: str = "AS215932/hyrule-cloud") -> dict[str, Any]:
captured: dict[str, Any] = {}

def runner(**kwargs: Any) -> dict[str, Any]:
captured.update(kwargs)
return {"final_state": {}, "state_path": str(tmp_path / "state.json")}

config = DaemonConfig(repos=(repo,), state_dir=tmp_path / "state", output_root=tmp_path / "runs", **config_kwargs)
gh = FakeGh({"issue list": _approved_issue_json(1, repo=repo, labels=["loop:approved"]), "issue view": json.dumps({"body": "x"})})
daemon_once(config, client=gh, feature_runner=runner)
return captured


def test_daemon_allowed_paths_default_is_docs_only(tmp_path: Path) -> None:
captured = _capture_allowed_paths(tmp_path, {})
assert captured["repo_name"] == "hyrule-cloud"
assert captured["allowed_paths"] == ["docs"]


def test_daemon_allowed_paths_per_repo_override(tmp_path: Path) -> None:
captured = _capture_allowed_paths(
tmp_path, {"allowed_paths_by_repo": {"hyrule-cloud": ("hyrule_cloud", "tests", "docs")}}
)
assert captured["allowed_paths"] == ["hyrule_cloud", "tests", "docs"]


def test_daemon_allowed_paths_unlisted_repo_falls_back_to_docs(tmp_path: Path) -> None:
# An override for one repo must not widen a different repo.
captured = _capture_allowed_paths(
tmp_path,
{"allowed_paths_by_repo": {"hyrule-web": ("hyrule_web",)}},
repo="AS215932/hyrule-cloud",
)
assert captured["allowed_paths"] == ["docs"]


def test_repo_name_for_issue_maps_core_repo_checkout_names() -> None:
Expand Down