M4.2: the toolchain step
The preflight becomes the toolchain step (D3, as #135 describes it): a bounded run of the item's driver model, in a transcript of its own, that makes the build VM and the project's files agree. - Started by the harness when the VM isn't set up for the project (M4.1's checks): it sets the VM up from the repository and edits nothing there (no write_file; anything a command changed is reported, left for the work). - Requested by the driver with toolchain_step(reason) once work is agreed: it installs, then brings pins, setup docs and CI configuration in step; its edits land in the current milestone. The driver gets the step's report as the tool's result, not its turns. - D1 made structural: until the go-ahead the fast path refuses what it knows changes something (and anything dangerous), with a pointer to the restatement; unknown commands still go to the classifier, so a bug can be reproduced before it's restated. - A `toolchain` actor and view; the walk asks for steps now and then. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
This commit is contained in:
1 parent
97ea451629
commit
d9f2ebd5aa
10 files changed
+283
-62
No files matched your search
@@ -145,6 +145,12 @@ class PolicyModel:
|
||||
return [call("ask_human", question=f"talk first {self.n}?")] # discuss
|
||||
if "finish_milestone" not in offered:
|
||||
return [call("restate", text=f"restatement {self.n}", changed=f"c{self.n}")]
|
||||
if "toolchain_step" in offered and self.rng.random() < 0.06:
|
||||
# M4.2: the work asks for a toolchain change.
|
||||
return [
|
||||
call("start_milestone", name=f"m{self.n}"),
|
||||
call("toolchain_step", reason=f"needs a tool {self.n}"),
|
||||
]
|
||||
if "delegate" in offered and self.rng.random() < 0.1:
|
||||
return [call("delegate", task=f"help {self.n}")]
|
||||
allowed = [t for t in facts[-3:] if "allowed this once" in t]
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""Toolchain preflight in the loop (D3, M4.1): before the driver plans, the
|
||||
build VM is checked (baseline, clone, marker); without a marker the item's
|
||||
driver gets the toolchain ready and the harness checks what it reports; later
|
||||
runs check the versions again; a rebuilt VM is re-cloned and set up again,
|
||||
and a changed host key waits for the operator."""
|
||||
"""The toolchain step in the loop (D3; M4.1, M4.2): before the driver plans,
|
||||
the build VM is checked (baseline, clone, marker); without a marker the harness
|
||||
starts a toolchain step that sets the VM up from the repository and edits
|
||||
nothing there; the driver can ask for one once work is agreed, whose edits are
|
||||
part of the work; later runs check versions again; a rebuilt VM is re-cloned
|
||||
and set up again, and a changed host key waits for the operator."""
|
||||
|
||||
import io
|
||||
import shutil
|
||||
@@ -102,7 +103,9 @@ def said(transport: InMemoryTransport) -> list[str]:
|
||||
return [m.text for m in transport.outbound]
|
||||
|
||||
|
||||
def test_preflight_comes_before_planning(world: dict[str, Any], project: str) -> None:
|
||||
def test_the_setup_step_comes_before_planning(
|
||||
world: dict[str, Any], project: str
|
||||
) -> None:
|
||||
model = ScriptedModel(ready(), RESTATE)
|
||||
agent, transport = agent_for(world, project, model)
|
||||
agent.intake()
|
||||
@@ -110,21 +113,21 @@ def test_preflight_comes_before_planning(world: dict[str, Any], project: str) ->
|
||||
assert (item.position, loop_of(item).wait) == (WAITING, GO_AHEAD)
|
||||
known = facts(world)
|
||||
toolchain = [
|
||||
f for f in known if f.startswith("preflight: toolchain: python Python 3.")
|
||||
f for f in known if f.startswith("toolchain step: toolchain: python Python 3.")
|
||||
]
|
||||
assert toolchain, known
|
||||
assert known.index(
|
||||
"preflight: getting the project's toolchain ready"
|
||||
"toolchain step: setting the build VM up from the repository"
|
||||
) < known.index("restatement rev-1 put up for a go-ahead")
|
||||
assert loop_of(item).toolchain[0][:2] == ("python", "python3 --version")
|
||||
marker = preflight.check(
|
||||
world["host"], project, str(world["repo"].root), markers(world)
|
||||
).marker
|
||||
assert marker is not None and marker["complete"]
|
||||
assert not [t for t in said(transport) if t.startswith("Preflight:")] # quiet
|
||||
assert not [t for t in said(transport) if t.startswith("Toolchain step:")] # quiet
|
||||
|
||||
|
||||
def test_preflight_runs_once_then_versions_are_checked(
|
||||
def test_the_setup_step_runs_once_then_versions_are_checked(
|
||||
world: dict[str, Any], project: str, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
model = ScriptedModel(ready(), RESTATE, [*WORK, *FINISH])
|
||||
@@ -179,12 +182,12 @@ def test_what_was_installed_or_is_missing_is_said(
|
||||
)
|
||||
agent, transport = agent_for(world, project, model)
|
||||
agent.intake()
|
||||
[line] = [t for t in said(transport) if t.startswith("Preflight:")]
|
||||
[line] = [t for t in said(transport) if t.startswith("Toolchain step:")]
|
||||
assert "installed: libfoo-dev" in line
|
||||
assert "missing: protoc (no package for this OS)" in line
|
||||
|
||||
|
||||
def test_an_unfinished_preflight_is_said_and_work_goes_on(
|
||||
def test_an_unfinished_step_is_said_and_work_goes_on(
|
||||
world: dict[str, Any], project: str
|
||||
) -> None:
|
||||
from nisse_dev.canonical import AssistantText
|
||||
@@ -194,7 +197,7 @@ def test_an_unfinished_preflight_is_said_and_work_goes_on(
|
||||
agent, transport = agent_for(world, project, model)
|
||||
agent.intake()
|
||||
assert loop_of(the_item(world)).wait == GO_AHEAD # it planned anyway
|
||||
assert any("the preflight didn't finish" in t for t in said(transport))
|
||||
assert any("the step didn't finish" in t for t in said(transport))
|
||||
marker = preflight.check(
|
||||
world["host"], project, str(world["repo"].root), markers(world)
|
||||
).marker
|
||||
@@ -213,7 +216,10 @@ def test_a_wiped_vm_is_cloned_again_and_set_up(
|
||||
say(transport, world, "go")
|
||||
assert any("cloned the workspace again" in f for f in facts(world))
|
||||
assert (Path(world["repo"].root) / ".git").exists()
|
||||
assert sum("getting the project's toolchain ready" in f for f in facts(world)) == 2
|
||||
assert (
|
||||
sum("setting the build VM up from the repository" in f for f in facts(world))
|
||||
== 2
|
||||
)
|
||||
assert world["forge"].prs # and the work went on to a PR
|
||||
|
||||
|
||||
@@ -254,3 +260,81 @@ def test_a_changed_host_key_waits_for_the_operators_word(
|
||||
say(transport, world, "trust new host key")
|
||||
assert forgotten == ["yes"]
|
||||
assert loop_of(the_item(world)).wait == GO_AHEAD # set up, then planned
|
||||
|
||||
|
||||
# --- M4.2: the step's modes, and the go-ahead ----------------------------------------
|
||||
|
||||
|
||||
def test_the_setup_step_edits_nothing_in_the_repository(
|
||||
world: dict[str, Any], project: str
|
||||
) -> None:
|
||||
step = [
|
||||
call("run_command", command="echo stray > stray.txt"),
|
||||
*ready(),
|
||||
]
|
||||
agent, _ = agent_for(world, project, ScriptedModel(step, RESTATE))
|
||||
agent.intake()
|
||||
[said] = [f for f in facts(world) if f.startswith("toolchain step: toolchain")]
|
||||
assert "changed files it shouldn't have, left for the work: stray.txt" in said
|
||||
tools = [
|
||||
e.event.tool
|
||||
for e in world["store"].transcript(the_item(world).id, "toolchain").read()
|
||||
if e.actor == "toolchain" and hasattr(e.event, "tool")
|
||||
]
|
||||
assert "write_file" not in tools # not offered in setup
|
||||
|
||||
|
||||
def test_a_requested_step_brings_the_projects_files_in_step(
|
||||
world: dict[str, Any], project: str
|
||||
) -> None:
|
||||
requested = [
|
||||
call("write_file", path="README.md", content="# demo\n\nNeeds Python 3.12.\n"),
|
||||
*ready(installed=["httpx"]), # type: ignore[arg-type]
|
||||
]
|
||||
model = ScriptedModel(
|
||||
ready(),
|
||||
RESTATE,
|
||||
[
|
||||
call("start_milestone", name="m"),
|
||||
call("toolchain_step", reason="it needs httpx, and the README says so"),
|
||||
],
|
||||
requested,
|
||||
[
|
||||
call("write_file", path="w.py", content="import httpx\n"),
|
||||
call("finish_milestone", summary="uses httpx", final="yes"),
|
||||
],
|
||||
)
|
||||
agent, transport = agent_for(world, project, model)
|
||||
agent.intake()
|
||||
say(transport, world, "go")
|
||||
forge = world["forge"]
|
||||
[pr] = [n for n, p in forge.prs.items() if not p["head"].startswith("nisse-check")]
|
||||
assert sorted(forge.files(pr)) == ["README.md", "w.py"] # reviewed with the work
|
||||
driver = [
|
||||
e.event for e in world["store"].transcript(the_item(world).id, "driver").view()
|
||||
]
|
||||
results = [getattr(e, "result", "") for e in driver]
|
||||
assert any(r.startswith("toolchain step done: toolchain: python") for r in results)
|
||||
# The step's own turns stay in its transcript.
|
||||
calls = [getattr(e, "arguments", {}).get("path") for e in driver]
|
||||
assert "README.md" not in calls
|
||||
|
||||
|
||||
def test_before_the_go_ahead_the_driver_only_looks(
|
||||
world: dict[str, Any], project: str
|
||||
) -> None:
|
||||
looking = [
|
||||
call("run_command", command="python3 --version"),
|
||||
call("run_command", command="pip install httpx"),
|
||||
]
|
||||
restate = [call("restate", text="add count_words to text.py; it needs httpx")]
|
||||
agent, _ = agent_for(world, project, ScriptedModel(ready(), looking, restate))
|
||||
agent.intake()
|
||||
results = [
|
||||
e.event.result
|
||||
for e in world["store"].transcript(the_item(world).id, "driver").read()
|
||||
if isinstance(e.event, ToolResult) and e.actor == "driver"
|
||||
]
|
||||
assert "Python 3." in results[0] # looking runs
|
||||
assert results[1].startswith("not run: not before the go-ahead")
|
||||
assert loop_of(the_item(world)).wait == GO_AHEAD
|
||||
Reference in new issue
Block a user