First half of build-order step 5: the stub agent's seam now has a real LLM behind it. The executor tool loop (5b) follows.
planner.py — LLMPlanningAgent offers the model exactly two tools, submit_plan and ask_human, so A4's gating is structural at the tool layer rather than enforced by prompt discipline. There is no proceed tool, so the model has no path to advance past a gate. The system prompt demands explicit, low-inference plans, because A13 says a materially weaker model executes them.
Truthful transcript — model output is appended to the canonical log before being interpreted, so a crash or a rejected tool call still leaves an honest record.
Bad turns gate, never guess — prose instead of a tool call, malformed arguments, or a failed request retries once with a nudge, then raises PlanningError; the orchestrator moves to AWAIT_INPUT and asks. It never invents a plan or loops.
Orchestrator now appends every operator message, plan_submitted, and gate_opened to the transcript. Rejection feedback therefore reaches the next planning turn as conversation rather than as an out-of-band parameter — a test asserts the re-plan sees "too vague, name the files". The A11 role mapping is pinned at task start into durable state.
llm.py hardening — non-JSON or non-object tool arguments now raise LLMError instead of escaping as a raw JSONDecodeError.
Finding: llama3.2 does not hold the tool-call contract
I ran the live planning turn against the local model and it failed. Probing the raw response shows why — it does emit a tool call, but with truncated arguments:
so the "plan" is the single character {; other attempts returned no tool call at all. This is exactly what A15 anticipates ("constrained decoding for tool-call JSON", "reduced tool schema for the executor role") and is consistent with A12's phase policy, which escalates planning to a frontier model unconditionally — planning was never meant to run locally.
Consequences in this PR: the client hardening above, and the live planning test is opt-in on NISSE_LIVE_PLANNER_MODEL (documented in .env.example) rather than on the usual NISSE_LLM_BASE_URL sentinel. That is a deliberate skip awaiting a frontier route, not the accidental kind we just fixed — the LLM and Matrix live tests keep their sentinels and still run.
What I'd like from you eventually: a LiteLLM route to a frontier model plus a virtual key scoped to reach it. Then set NISSE_LIVE_PLANNER_MODEL and this test proves the contract end to end. Not blocking 5b, which targets the executor role where llama3.2 belongs.
18 new unit tests (tool-call interpretation, retry/nudge, malformed arguments, no-proceed-tool invariant, transcript shape, role pinning, failure gating, feedback round-trip). 90 unit + 7 integration green locally, 1 deliberate skip.
First half of build-order step 5: the stub agent's seam now has a real LLM behind it. The executor tool loop (5b) follows.
- **`planner.py`** — `LLMPlanningAgent` offers the model exactly two tools, `submit_plan` and `ask_human`, so A4's gating is structural at the tool layer rather than enforced by prompt discipline. There is no proceed tool, so the model has no path to advance past a gate. The system prompt demands explicit, low-inference plans, because A13 says a materially weaker model executes them.
- **Truthful transcript** — model output is appended to the canonical log *before* being interpreted, so a crash or a rejected tool call still leaves an honest record.
- **Bad turns gate, never guess** — prose instead of a tool call, malformed arguments, or a failed request retries once with a nudge, then raises `PlanningError`; the orchestrator moves to `AWAIT_INPUT` and asks. It never invents a plan or loops.
- **Orchestrator** now appends every operator message, `plan_submitted`, and `gate_opened` to the transcript. Rejection feedback therefore reaches the next planning turn *as conversation* rather than as an out-of-band parameter — a test asserts the re-plan sees "too vague, name the files". The A11 role mapping is pinned at task start into durable state.
- **`llm.py` hardening** — non-JSON or non-object tool arguments now raise `LLMError` instead of escaping as a raw `JSONDecodeError`.
### Finding: llama3.2 does not hold the tool-call contract
I ran the live planning turn against the local model and it failed. Probing the raw response shows why — it does emit a tool call, but with truncated arguments:
```
"arguments": "{\"type\": \"application\", \"content\": \"{\"}"
```
so the "plan" is the single character `{`; other attempts returned no tool call at all. This is exactly what A15 anticipates ("constrained decoding for tool-call JSON", "reduced tool schema for the executor role") and is consistent with A12's phase policy, which escalates planning to a frontier model unconditionally — planning was never meant to run locally.
Consequences in this PR: the client hardening above, and the live planning test is **opt-in on `NISSE_LIVE_PLANNER_MODEL`** (documented in `.env.example`) rather than on the usual `NISSE_LLM_BASE_URL` sentinel. That is a deliberate skip awaiting a frontier route, not the accidental kind we just fixed — the LLM and Matrix live tests keep their sentinels and still run.
**What I'd like from you eventually**: a LiteLLM route to a frontier model plus a virtual key scoped to reach it. Then set `NISSE_LIVE_PLANNER_MODEL` and this test proves the contract end to end. Not blocking 5b, which targets the executor role where llama3.2 belongs.
18 new unit tests (tool-call interpretation, retry/nudge, malformed arguments, no-proceed-tool invariant, transcript shape, role pinning, failure gating, feedback round-trip). 90 unit + 7 integration green locally, 1 deliberate skip.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
- planner.py: LLMPlanningAgent offers exactly two tools, submit_plan
and ask_human, so gating stays structural at the tool layer; no
proceed tool exists. System prompt requires explicit, low-inference
plans because a weaker model executes them (A13)
- Model output is appended to the canonical log as it arrives, before
being interpreted, so a crash mid-planning leaves a truthful
transcript
- A bad turn (prose instead of a tool call, malformed arguments, or a
failed call) retries once, then raises PlanningError; the
orchestrator gates to AWAIT_INPUT rather than inventing a plan
- Orchestrator: appends every operator message, plan_submitted, and
gate_opened to the transcript, so re-planning sees rejection
feedback as conversation rather than out-of-band parameters; pins
the A11 role mapping at task start
- llm.py: non-JSON or non-object tool arguments now raise LLMError
instead of escaping as JSONDecodeError — observed from llama3.2,
which emits truncated argument strings
- Live planning test is opt-in on NISSE_LIVE_PLANNER_MODEL: planning
escalates unconditionally (A12) and the local model does not hold
the tool-call contract, so it awaits a frontier route
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The virtual key now reaches a Haiku-class route, which holds the
tool-call contract llama3.2 could not: a real planning turn returns a
well-formed submit_plan call with an explicit, low-inference plan.
Wires NISSE_LIVE_PLANNER_MODEL through the CI workflow (add the
matching Actions secret to run it there) and records the finding in
.env.example.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Empty commit: Gitea 1.27 exposes no rerun API endpoint, and Actions
secrets are read at run start, so a new run is needed to exercise the
live planning test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
First half of build-order step 5: the stub agent's seam now has a real LLM behind it. The executor tool loop (5b) follows.
planner.py—LLMPlanningAgentoffers the model exactly two tools,submit_planandask_human, so A4's gating is structural at the tool layer rather than enforced by prompt discipline. There is no proceed tool, so the model has no path to advance past a gate. The system prompt demands explicit, low-inference plans, because A13 says a materially weaker model executes them.PlanningError; the orchestrator moves toAWAIT_INPUTand asks. It never invents a plan or loops.plan_submitted, andgate_openedto the transcript. Rejection feedback therefore reaches the next planning turn as conversation rather than as an out-of-band parameter — a test asserts the re-plan sees "too vague, name the files". The A11 role mapping is pinned at task start into durable state.llm.pyhardening — non-JSON or non-object tool arguments now raiseLLMErrorinstead of escaping as a rawJSONDecodeError.Finding: llama3.2 does not hold the tool-call contract
I ran the live planning turn against the local model and it failed. Probing the raw response shows why — it does emit a tool call, but with truncated arguments:
so the "plan" is the single character
{; other attempts returned no tool call at all. This is exactly what A15 anticipates ("constrained decoding for tool-call JSON", "reduced tool schema for the executor role") and is consistent with A12's phase policy, which escalates planning to a frontier model unconditionally — planning was never meant to run locally.Consequences in this PR: the client hardening above, and the live planning test is opt-in on
NISSE_LIVE_PLANNER_MODEL(documented in.env.example) rather than on the usualNISSE_LLM_BASE_URLsentinel. That is a deliberate skip awaiting a frontier route, not the accidental kind we just fixed — the LLM and Matrix live tests keep their sentinels and still run.What I'd like from you eventually: a LiteLLM route to a frontier model plus a virtual key scoped to reach it. Then set
NISSE_LIVE_PLANNER_MODELand this test proves the contract end to end. Not blocking 5b, which targets the executor role where llama3.2 belongs.18 new unit tests (tool-call interpretation, retry/nudge, malformed arguments, no-proceed-tool invariant, transcript shape, role pinning, failure gating, feedback round-trip). 90 unit + 7 integration green locally, 1 deliberate skip.
🤖 Generated with Claude Code