With no production instance yet, there is no reason to carry two ways
of configuring the agent. Every environment variable that duplicated a
config key is gone; the environment now supplies only the secrets the
file references by ${…}, plus NISSE_CONFIG and NISSE_ENV to find the
files.
- AgentConfig loads from a file (load, from_file) or a parsed tree
(from_tree, for tests); a missing file is an error naming the example
- The from_env constructors on LiteLLMClient, AnthropicPassthroughClient,
GiteaClient, MatrixConfig and RoleMap are removed, with the env
reading for max output tokens; clients take their settings from config
- The live integration tests share a committed agent config,
tests/integration/nisse-test.yaml, aimed at test resources, and one
conftest that loads it and builds clients. NISSE_LIVE_PLANNER_MODEL
becomes its planner role; NISSE_CAPABILITY_MODELS stays as a per-run
switch
- A missing secret now fails the live tests instead of skipping them
- CI passes only the three tokens
- .env.example is secrets only; spec A5 says config comes from the file
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
- tools.py: four flat tools (read_file, write_file, list_dir,
run_command) per A15's reduced manifest. Deliberately unsandboxed:
the VM is the trust boundary (B3) and tool use is auto-approved
inside an approved unit of work (A4), so path confinement would add
failure modes without adding safety. What is enforced is
operational — timeout with process-group kill so a backgrounded
server cannot outlive its command, output truncation so a build log
cannot eat the context window, closed stdin so prompts fail fast,
and a scrubbed environment so project code never sees the agent's
credentials
- executor.py: renders the log, calls the executor role with the
reduced manifest, dispatches tool calls, and checkpoints every
result (A13). Escalation is observable, never self-assessed (A12):
consecutive command failures and a hard turn cap. Failed reads do
not count — looking around is not failing
- Until delegation exists (step 7), escalation gates to the human
with the failures attached
- control.py: start milestone now runs the executor; a stopped
milestone gates to AWAIT_INPUT
Two bugs found by running it live against real models:
- A turn-ending gate tool (submit_plan) left a tool_call with no
tool_result, which providers reject outright, so every planning
turn after the first failed with a 400. The planner now records the
gate as the call's outcome, which is also the truthful transcript
- The renderer now emits a placeholder for any tool_call lacking a
result, so a crash between call and result (A3) cannot poison the
transcript permanently
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>