M4.2: the toolchain step
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 2m36s
CI / e2e (pull_request) Successful in 11m13s

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:
dongarandClaude Opus 5.5 committed 2026-10-08 17:37:21 +00:00
1 parent 97ea451629
commit d9f2ebd5aa
10 files changed
+283 -62

No files matched your search

+6
View File
@@ -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