Add the composition root and a runnable entry point #31

Merged
dongar merged 2 commits from agent-entrypoint into main 2026-09-08 22:33:45 +00:00
Collaborator

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

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)
dev-agent added 1 commit 2026-09-08 22:26:45 +00:00
Add the composition root and a runnable entry point
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m14s
2fb25b8195
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>
dev-agent added 1 commit 2026-09-08 22:31:40 +00:00
Refuse to start with the state store or .env inside the clone
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m14s
dfa574aeec
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>
dongar approved these changes 2026-09-08 22:33:41 +00:00
dongar merged commit 1da94d2581 into main 2026-09-08 22:33:45 +00:00
dongar deleted branch agent-entrypoint 2026-09-08 22:33:45 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dev/nisse#31