diff --git a/CHANGELOG.md b/CHANGELOG.md index 27c53293..8c5a907d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -180,6 +180,12 @@ All notable changes to AgentDescent are documented here. The format follows ### Fixed +- `agentdescent init selection --kind policy_slot` now finishes successfully and + explains how to provide the `data.problems` ref and its import allowlist. + It previously wrote the starter spec and then crashed looking for `data.path`, + which policy slots do not use. The clean-wheel smoke check now exercises this + installed command as well as the offline demo and MCP server. + - **Onboarding, published-version guidance and CI support coverage.** Lead with the packaged offline demo on current `main`, distinguish it from the v0.5.0 source-example path, name NumPy as a runtime dependency, exercise Python 3.10 diff --git a/agentdescent/cli.py b/agentdescent/cli.py index 5b3b694e..ca754ee0 100644 --- a/agentdescent/cli.py +++ b/agentdescent/cli.py @@ -281,11 +281,17 @@ def cmd_init(a: argparse.Namespace) -> int: with open(out, "w", encoding="utf-8") as fh: json.dump(spec, fh, indent=2) print(f"wrote {out}") - data = spec["data"]["path"] - if not os.path.exists(os.path.expanduser(data)): - print(f"next: create {data} -- one JSON object per line, " - '{"prompt": "...", "gold": "..."}') - print(" (`agentdescent demo` runs a complete example, offline, to copy from)") + if spec["kind"] == "policy_slot": + problems = spec["data"]["problems"] + print(f"next: provide {problems} -- a zero-argument builder " + "returning a mapping of inner problems") + print(" make its module importable and include its package in the spec's allow list") + else: + data = spec["data"]["path"] + if not os.path.exists(os.path.expanduser(data)): + print(f"next: create {data} -- one JSON object per line, " + '{"prompt": "...", "gold": "..."}') + print(" (`agentdescent demo` runs a complete example, offline, to copy from)") print(f"then: agentdescent plan {out}") return 0 @@ -785,7 +791,7 @@ def build_parser() -> argparse.ArgumentParser: s.add_argument("path") s.add_argument("--kind", choices=("text", "skill_dir", "agent_dir", "agent_code", "plugin", "policy_slot")) - s.add_argument("--data", help="cases file to point the spec at") + s.add_argument("--data", help="cases file, or a module:attribute problems ref for policy_slot") s.add_argument("--agent", default="claude_code", help="worker agent short name") s.add_argument("--out", help="where to write the spec") s.set_defaults(fn=cmd_init) diff --git a/docs/meta-evolution.md b/docs/meta-evolution.md index 6c0d9451..f9e193b9 100644 --- a/docs/meta-evolution.md +++ b/docs/meta-evolution.md @@ -170,8 +170,10 @@ nature of the thing rather than an inconsistency: artifact is a decision rule of the optimiser, and there is no file to point at. * **`data` holds refs, not rows.** An inner problem is a callable `(value, seed) -> MetaOutcome`; no row format can express one. `data.problems` - is a `module:attribute` ref to a mapping of them (or a zero-argument builder), - inside the spec's import allowlist. + is a `module:attribute` ref to a zero-argument builder returning a mapping of them, + inside the spec's import allowlist. Pass `--data mypkg.problems:build` to + `init` to choose that ref; make the module importable and include its package + in the generated spec's `allow` list before planning. `score` takes `auc` (the default), `final_reward`, `rollouts_to`, or a ref to any `MetaOutcome -> float`. diff --git a/scripts/smoke-installed-wheel.py b/scripts/smoke-installed-wheel.py index 8fb05e75..af636e8c 100755 --- a/scripts/smoke-installed-wheel.py +++ b/scripts/smoke-installed-wheel.py @@ -75,6 +75,25 @@ def main() -> None: env.pop(name, None) env["AGENTDESCENT_HOME"] = str(workdir / "home") + policy_init = subprocess.run( + ["agentdescent", "init", "selection", "--kind", "policy_slot"], + cwd=workdir, env=env, text=True, capture_output=True, timeout=30, + ) + assert policy_init.returncode == 0, ( + f"installed policy-slot init failed ({policy_init.returncode})\n" + f"{policy_init.stdout}\n{policy_init.stderr}" + ) + policy_spec = json.loads( + (workdir / ".agentdescent" / "selection.evolve.json").read_text(encoding="utf-8") + ) + assert policy_spec["target"] == "selection", policy_spec + assert policy_spec["data"] == {"problems": "mypkg.problems:build", "seeds": [0]}, ( + policy_spec + ) + assert "mypkg.problems:build" in policy_init.stdout, policy_init.stdout + assert "allow list" in policy_init.stdout, policy_init.stdout + assert "JSON object per line" not in policy_init.stdout, policy_init.stdout + demo = subprocess.run( ["agentdescent", "demo"], cwd=workdir, env=env, text=True, capture_output=True, timeout=120, diff --git a/tests/test_cli_init_policy_slot.py b/tests/test_cli_init_policy_slot.py new file mode 100644 index 00000000..94f37bfe --- /dev/null +++ b/tests/test_cli_init_policy_slot.py @@ -0,0 +1,35 @@ +"""Policy-slot onboarding uses problem refs rather than row-data paths.""" + +import json + +import pytest + +from agentdescent import cli +from agentdescent.evolvespec import load_spec + + +@pytest.mark.parametrize("problems", [None, "custom.problems:build"]) +def test_init_policy_slot_writes_spec_and_explains_problem_ref( + tmp_path, monkeypatch, capsys, problems +): + monkeypatch.chdir(tmp_path) + args = ["init", "selection", "--kind", "policy_slot"] + if problems is not None: + args.extend(["--data", problems]) + assert cli.main(args) == 0 + + spec_path = tmp_path / ".agentdescent" / "selection.evolve.json" + payload = json.loads(spec_path.read_text(encoding="utf-8")) + expected = problems or "mypkg.problems:build" + assert payload["kind"] == "policy_slot" + assert payload["target"] == "selection" + assert payload["data"] == {"problems": expected, "seeds": [0]} + assert payload["allow"] == ["mypkg"] # init does not expand import permissions + assert load_spec(str(spec_path)).target == "selection" + + out = capsys.readouterr().out + assert expected in out + assert "zero-argument builder returning a mapping of inner problems" in out + assert "importable" in out and "allow list" in out + assert "then: agentdescent plan .agentdescent/selection.evolve.json" in out + assert "JSON object per line" not in out