Fix AWAIT_INPUT deadlock and make the live milestone test robust
CI / checks (push) Successful in 36s
CI / checks (pull_request) Successful in 29s

Found while chasing a flaky live test, but the flakiness was a real
bug: only the ask_human path recorded a pending item, so AWAIT_INPUT
reached any other way — planning failure, stopped milestone, boot
reconciliation — refused every operator reply with "no pending
question to answer" and the agent could never be recovered. The
operator had no way back short of wiping state.

Every path into AWAIT_INPUT now records what a reply should resume,
and answering dispatches on it: planning failures and questions
resume planning, a stopped milestone returns to its still-approved
gate, and the fallback says what commands work instead of
dead-ending. Two regression tests cover the recovery paths.

The live test also scripted a fixed approve/approve/start sequence,
which a real model breaks by legitimately calling ask_human at a
gate. It now plays the operator adaptively, responding to whatever
the agent actually did. Five consecutive live runs pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
dongarandClaude Opus 5 committed 2026-09-07 16:46:07 +00:00
1 parent 999a38a705
commit bacf65dbe2
3 files changed
+149 -33

No files matched your search

+35 -5
View File
@@ -12,7 +12,13 @@ from pathlib import Path
import pytest
from nisse_dev.control import EXECUTING, Orchestrator
from nisse_dev.control import (
AWAIT_ARCH_SIGNOFF,
AWAIT_INPUT,
AWAIT_MILESTONE_SIGNOFF,
EXECUTING,
Orchestrator,
)
from nisse_dev.envfile import load_env_file
from nisse_dev.executor import LLMExecutor
from nisse_dev.llm import LiteLLMClient, RoleMap
@@ -35,6 +41,32 @@ pytestmark = [
]
def _drive_to_execution(
store: StateStore, transport: InMemoryTransport, max_steps: int = 10
) -> None:
"""Play the operator. A scripted approve/approve/start sequence is not
robust against a live model: ask_human is a legitimate move at any gate,
and a question shifts the state machine under a fixed script. So respond
to whatever the agent actually did."""
for _ in range(max_steps):
state = store.get_state()
assert state is not None
if state.state == EXECUTING:
return
gate = state.data.get("gate") or {}
if state.state == AWAIT_INPUT:
transport.receive("don", "Use your best judgement and proceed.")
elif state.state == AWAIT_MILESTONE_SIGNOFF and gate.get("approved"):
transport.receive("don", "start milestone 1")
elif state.state in (AWAIT_ARCH_SIGNOFF, AWAIT_MILESTONE_SIGNOFF):
transport.receive("don", "approve")
else:
raise AssertionError(f"unexpected state {state.state}")
raise AssertionError(
f"never reached execution; last message: {transport.outbound[-1]}"
)
def test_plan_approve_execute_produces_a_real_file(tmp_path: Path) -> None:
assert os.environ.get("NISSE_LLM_TOKEN"), (
f"{SENTINEL} is set, so the live path is expected to work, "
@@ -63,13 +95,11 @@ def test_plan_approve_execute_produces_a_real_file(tmp_path: Path) -> None:
"use greet. Milestone 1: create greet.py in the workspace root defining "
"greet(name) returning 'Hello, <name>!', and verify it by running python.",
)
transport.receive("don", "approve") # architecture gate
transport.receive("don", "approve") # milestone gate
transport.receive("don", "start milestone 1")
_drive_to_execution(store, transport)
state = store.get_state()
assert state is not None
assert state.state in (EXECUTING, "AWAIT_INPUT")
assert state.state in (EXECUTING, AWAIT_INPUT)
created = list(workspace.root.rglob("*.py"))
assert created, f"executor created no Python file; said: {transport.outbound[-1]}"