Per-project state and clone-on-use (A2/B1 debt) #27

Open
opened 2026-09-08 22:06:29 +00:00 by dev-agent · 0 comments
Collaborator

Deferred by decision — filed so it is not lost. This is debt against the spec, not new scope.

What the spec already requires

  • A2: "Must persist: active project/task, per-project state-machine position, ... (dev) git clone locations." Plural in the spec.
  • B1: IDLE → [use <project>] "clone repo, begin architecture planning".

What we built instead

  • agent_state is a single row (id = 1). It holds the position, not one per project, so switching projects would overwrite rather than park.
  • use <project> always starts fresh; there is no resuming a project left at, say, PR_OPEN.
  • The repo is global config (NISSE_GITEA_TEST_REPO), so a project name does not resolve to anything and nothing is cloned.

That is sufficient while exactly one project is ever known, which is why it has not bitten.

Work

  • Schema migration: a projects table keyed by project name (state, data, updated-at) plus an active-project pointer; the existing single row migrates in as the first project.
  • Per-project descriptor for what is global config today: repo, workspace/clone path, plan-board issue, open PR, and the last-seen comment id.
  • use <project> resolves the name against the forge (on a single instance the forge is external truth for what exists), clones if needed, and resumes an existing project rather than wiping it.
  • status reports the active project and what is parked elsewhere.

One bug this fixes

CommentPoller.prime() marks existing comments seen so the agent does not re-address old feedback — correct on first attach. But a poller recreated when switching back to a project primes again and silently swallows every comment made while away. The last-seen cursor has to live in per-project durable state rather than poller memory.

Ordering note

Cheaper before the step-10 nisse-core extraction than after, when the same change lands in a library with two consumers.

Deferred by decision — filed so it is not lost. This is **debt against the spec**, not new scope. ### What the spec already requires - **A2**: "Must persist: active project/task, **per-project state-machine position**, ... (dev) **git clone locations**." Plural in the spec. - **B1**: `IDLE → [use <project>]` **"clone repo, begin architecture planning"**. ### What we built instead - `agent_state` is a single row (`id = 1`). It holds *the* position, not one per project, so switching projects would overwrite rather than park. - `use <project>` always starts fresh; there is no resuming a project left at, say, `PR_OPEN`. - The repo is global config (`NISSE_GITEA_TEST_REPO`), so a project name does not resolve to anything and nothing is cloned. That is sufficient while exactly one project is ever known, which is why it has not bitten. ### Work - Schema migration: a `projects` table keyed by project name (state, data, updated-at) plus an active-project pointer; the existing single row migrates in as the first project. - Per-project descriptor for what is global config today: repo, workspace/clone path, plan-board issue, open PR, and the **last-seen comment id**. - `use <project>` resolves the name against the forge (on a single instance the forge is external truth for what exists), clones if needed, and **resumes** an existing project rather than wiping it. - `status` reports the active project and what is parked elsewhere. ### One bug this fixes `CommentPoller.prime()` marks existing comments seen so the agent does not re-address old feedback — correct on first attach. But a poller recreated when switching back to a project primes again and **silently swallows every comment made while away**. The last-seen cursor has to live in per-project durable state rather than poller memory. ### Ordering note Cheaper before the step-10 `nisse-core` extraction than after, when the same change lands in a library with two consumers.
Sign in to join this conversation.