The step between "a well-tested library" and "something you can talk to in Element". main.py was still the hello-world placeholder from the scaffold, and no code outside the test suite had ever constructed an Orchestrator.
That gap was not cosmetic. It is how two fully built, fully tested components came to be reachable from nothing but their own tests — the delegation harness (found last week) and the Gitea comment poller (found while planning this). Unit tests that construct a component directly cannot catch that class of bug; only assembly can. This PR is as much a regression test for that as it is wiring.
What it adds
config.py validates everything before anything is built and reports all missing variables at once, rather than failing at first use twenty minutes into a milestone on the one call that needed it. Whitespace-only counts as missing, because a token of spaces passes a truthiness check and then fails at the first request — a test caught that.
agent.py, the composition root, owning startup and shutdown order: reconcile against external truth before accepting commands; on stop, stop watching, then stop talking, then close the store last so nothing writes to a closed file.
The Gitea account comes from whoami rather than another config variable. It names the identity every permission is evaluated against, and reading it proves the token works before anything depends on it.
Poller lifecycle, which is genuinely new rather than wiring: watch the plan issue while a gate stands, watch the PR while it is open, stop on leaving. The orchestrator owns when to watch, since that is a state-machine question; an injected factory owns how to build one, so control.py gains no dependency on poller machinery.
Per-turn progress reports from the executor. It runs synchronously inside the transport callback, so without this the agent goes silent for an entire milestone — and a working run is indistinguishable from a hung one. Cheaper than a worker thread, and it makes the demonstration watchable.
Test changes worth flagging
nisse-dev no longer greets, so the entry-point test now asserts it refuses to start unconfigured and names what is missing. Both entry-point tests run with a deliberately empty environment and a .env path that does not exist — the working directory's .env holds live credentials, and a test must never be one cwd away from starting a real agent against real services.
Known limitations, deliberate for a first demonstration
The executor still runs inside the transport callback, so during a milestone the sync loop is blocked: status will not answer and the run cannot be interrupted. The progress messages make it legible; a worker thread would make it interactive. And the workspace is cloned once at startup from config rather than resolved from use <project> — that is issue #27.
16 new unit tests. 273 unit + 17 integration green locally.
The step between "a well-tested library" and "something you can talk to in Element". `main.py` was still the hello-world placeholder from the scaffold, and **no code outside the test suite had ever constructed an `Orchestrator`**.
That gap was not cosmetic. It is how two fully built, fully tested components came to be reachable from nothing but their own tests — the delegation harness (found last week) and the Gitea comment poller (found while planning this). Unit tests that construct a component directly cannot catch that class of bug; only assembly can. This PR is as much a regression test for that as it is wiring.
### What it adds
- **`config.py`** validates everything before anything is built and reports **all** missing variables at once, rather than failing at first use twenty minutes into a milestone on the one call that needed it. Whitespace-only counts as missing, because a token of spaces passes a truthiness check and then fails at the first request — a test caught that.
- **`agent.py`**, the composition root, owning startup and shutdown *order*: reconcile against external truth before accepting commands; on stop, stop watching, then stop talking, then close the store last so nothing writes to a closed file.
- **The Gitea account comes from `whoami`** rather than another config variable. It names the identity every permission is evaluated against, and reading it proves the token works before anything depends on it.
- **Poller lifecycle**, which is genuinely new rather than wiring: watch the plan issue while a gate stands, watch the PR while it is open, stop on leaving. The orchestrator owns *when* to watch, since that is a state-machine question; an injected factory owns *how* to build one, so `control.py` gains no dependency on poller machinery.
- **Per-turn progress reports from the executor.** It runs synchronously inside the transport callback, so without this the agent goes silent for an entire milestone — and a working run is indistinguishable from a hung one. Cheaper than a worker thread, and it makes the demonstration watchable.
### Test changes worth flagging
`nisse-dev` no longer greets, so the entry-point test now asserts it *refuses to start unconfigured and names what is missing*. Both entry-point tests run with a deliberately empty environment and a `.env` path that does not exist — the working directory's `.env` holds live credentials, and a test must never be one `cwd` away from starting a real agent against real services.
### Known limitations, deliberate for a first demonstration
The executor still runs inside the transport callback, so during a milestone the sync loop is blocked: `status` will not answer and the run cannot be interrupted. The progress messages make it legible; a worker thread would make it interactive. And the workspace is cloned once at startup from config rather than resolved from `use <project>` — that is issue #27.
16 new unit tests. 273 unit + 17 integration green locally.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Nothing had ever assembled the agent. main.py was still the hello-world
placeholder from the scaffold, and no code outside the test suite
constructed an Orchestrator — which is how two fully built, fully
tested components came to be reachable from nothing: the delegation
harness, and the Gitea comment poller. Unit tests that construct a
component directly cannot catch that; only assembly can.
- config.py validates every variable before anything is built and
names all of them at once, rather than failing at first use twenty
minutes into a milestone on the call that mattered. Whitespace-only
counts as missing, since a token of spaces passes a truthiness check
and fails at the first request instead
- agent.py builds the whole graph and owns startup and shutdown order:
reconcile before accepting commands, and on stop, stop watching,
then stop talking, then close the store last
- The Gitea account is read via whoami rather than configured: it names
the identity every permission is evaluated against, and proves the
token works before anything depends on it
- Poller lifecycle is now a state-machine concern: watch the plan issue
while a gate stands, the PR while it is open, and stop on leaving.
The orchestrator owns when to watch; an injected factory owns how to
build one
- The executor reports each turn, because it runs synchronously inside
the transport callback: without it the agent is silent for a whole
milestone and a working run looks exactly like a hung one
- main.py loads .env, builds, and shuts down cleanly on SIGINT/SIGTERM
The entry-point tests changed shape: nisse-dev no longer greets, it
refuses to start unconfigured and says everything that is missing. Both
tests now run with a deliberately empty environment and a .env path
that does not exist, because the working directory's .env holds live
credentials and a test must never be one cwd away from starting a real
agent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Answering "is the workspace where the agent runs, or what it works
on?" — it is what it works on, and conflating the two is dangerous
rather than merely untidy.
The agent commits its workspace with git add -A and pushes it, so
anything inside the clone reaches the project repository. A .env
placed there would publish the LiteLLM, Matrix and Gitea tokens as a
side effect of the agent working correctly: silently, once, and
probably unnoticed. Startup now refuses both that and a state store
inside the clone, and .env.example documents the intended layout.
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.
The step between "a well-tested library" and "something you can talk to in Element".
main.pywas still the hello-world placeholder from the scaffold, and no code outside the test suite had ever constructed anOrchestrator.That gap was not cosmetic. It is how two fully built, fully tested components came to be reachable from nothing but their own tests — the delegation harness (found last week) and the Gitea comment poller (found while planning this). Unit tests that construct a component directly cannot catch that class of bug; only assembly can. This PR is as much a regression test for that as it is wiring.
What it adds
config.pyvalidates everything before anything is built and reports all missing variables at once, rather than failing at first use twenty minutes into a milestone on the one call that needed it. Whitespace-only counts as missing, because a token of spaces passes a truthiness check and then fails at the first request — a test caught that.agent.py, the composition root, owning startup and shutdown order: reconcile against external truth before accepting commands; on stop, stop watching, then stop talking, then close the store last so nothing writes to a closed file.whoamirather than another config variable. It names the identity every permission is evaluated against, and reading it proves the token works before anything depends on it.control.pygains no dependency on poller machinery.Test changes worth flagging
nisse-devno longer greets, so the entry-point test now asserts it refuses to start unconfigured and names what is missing. Both entry-point tests run with a deliberately empty environment and a.envpath that does not exist — the working directory's.envholds live credentials, and a test must never be onecwdaway from starting a real agent against real services.Known limitations, deliberate for a first demonstration
The executor still runs inside the transport callback, so during a milestone the sync loop is blocked:
statuswill not answer and the run cannot be interrupted. The progress messages make it legible; a worker thread would make it interactive. And the workspace is cloned once at startup from config rather than resolved fromuse <project>— that is issue #27.16 new unit tests. 273 unit + 17 integration green locally.
🤖 Generated with Claude Code