M2.1, PR B of 4: the new loop's core.Stacked on #96 (PR A, threads); the base is matrix-threads, so the diff is only B's changes.
What's in it
A new package, nisse_dev/loop/, built alongside control.py. Config loop: new chooses it in main.py; the default stays old until the switch-over (M2.8).
agent.py, the scheduler: one worker thread, inputs in order.
Intake (D1): issues assigned to the agent become work items, once each.
Only an operator's issue, or one an operator assigned (read from the issue timeline), counts (L11).
The agent posts "Queued repo#N: title", which roots the item's thread.
An operator's issue text reaches the driver as the operator's; anyone else's is a labelled fact.
Messages (L5): a reply in an item's thread goes to that item.
go or approve is a go-ahead bound to the revision on screen when it arrived (L3); a stale one is refused with which revision is up.
abandon closes the item and its issue.
Anything else is an answer or feedback, and the driver reads it next run.
At top level: status, and a pointer to assigning an issue.
One active item at a time; it runs until a waiting tool. Waiting is a safe point (L1), and conversation never interrupts a run.
Boot (L8): an item cut off mid-run waits on the operator with a note, never resumed blindly. Waiting points are put up again, including what's needed to stamp a go-ahead.
driver.py, a driver run: one model with tools, looping until a waiting tool, the turn cap, stop, or a model failure.
It reads the driver's view of the item's transcript (L6).
Calls after a waiting tool get "not run" results.
Prose gets one nudge.
tools.py, control tools:restate, ask_human and close_item. Milestones come in PR C.
Before the go-ahead the driver can look but not change anything: no write_file, no close_item. That's enforced by what it's offered (P6, D1).
items.py: an item's state (ready / running / waiting / closed) and its thread, issue and revision, in the work-items table.
build.py: assembles it from the same services as the old agent. The driver uses the planner model until M2.6.
Gitea:assigned_issues, and assigned_by from the timeline. I probed the real format on the sandbox (issue #229, closed).
Tests
test_loop.py, 19 tests with a scripted model:
intake: once only, an operator's issue vs someone else's, how the text is labelled;
restating in the thread;
only look-tools before the go-ahead;
go → work → close_item closes the issue;
a stale go refused;
a top-level go not taken as a go-ahead;
a question and its answer;
prose twice;
abandon and status;
boot after a crash mid-run, and a restatement put up again in a new process;
the driver run's cap, an invented tool, stop, and calls after a waiting tool.
Assembly:loop: new builds over the same services.
Config: the loop setting.
580 unit tests pass. The e2e test on the new loop, and the first live run, come in PR D.
M2.1, **PR B of 4: the new loop's core.** **Stacked on #96** (PR A, threads); the base is `matrix-threads`, so the diff is only B's changes.
## What's in it
A new package, `nisse_dev/loop/`, built alongside `control.py`. Config **`loop: new`** chooses it in `main.py`; the default stays `old` until the switch-over (M2.8).
- **`agent.py`, the scheduler:** one worker thread, inputs in order.
- **Intake (D1):** issues assigned to the agent become work items, once each.
- Only an operator's issue, or one an operator assigned (read from the issue timeline), counts (L11).
- The agent posts "Queued repo#N: title", which roots the item's thread.
- An operator's issue text reaches the driver as the operator's; anyone else's is a labelled fact.
- **Messages (L5):** a reply in an item's thread goes to that item.
- `go` or `approve` is a go-ahead **bound to the revision on screen when it arrived** (L3); a stale one is refused with which revision is up.
- `abandon` closes the item and its issue.
- Anything else is an answer or feedback, and the driver reads it next run.
- At top level: `status`, and a pointer to assigning an issue.
- **One active item at a time;** it runs until a waiting tool. Waiting is a safe point (L1), and conversation never interrupts a run.
- **Boot (L8):** an item cut off mid-run waits on the operator with a note, never resumed blindly. Waiting points are put up again, including what's needed to stamp a go-ahead.
- **`driver.py`, a driver run:** one model with tools, looping until a waiting tool, the turn cap, `stop`, or a model failure.
- It reads the driver's view of the item's transcript (L6).
- Calls after a waiting tool get "not run" results.
- Prose gets one nudge.
- **`tools.py`, control tools:** `restate`, `ask_human` and `close_item`. Milestones come in PR C.
- **Before the go-ahead the driver can look but not change anything:** no `write_file`, no `close_item`. That's enforced by what it's offered (P6, D1).
- **`items.py`:** an item's state (ready / running / waiting / closed) and its thread, issue and revision, in the work-items table.
- **`build.py`:** assembles it from the same services as the old agent. The driver uses the `planner` model until M2.6.
- **Gitea:** `assigned_issues`, and `assigned_by` from the timeline. I probed the real format on the sandbox (issue #229, closed).
## Tests
- **`test_loop.py`, 19 tests with a scripted model:**
- intake: once only, an operator's issue vs someone else's, how the text is labelled;
- restating in the thread;
- only look-tools before the go-ahead;
- `go` → work → `close_item` closes the issue;
- a stale `go` refused;
- a top-level `go` not taken as a go-ahead;
- a question and its answer;
- prose twice;
- `abandon` and `status`;
- boot after a crash mid-run, and a restatement put up again in a new process;
- the driver run's cap, an invented tool, `stop`, and calls after a waiting tool.
- **Assembly:** `loop: new` builds over the same services.
- **Config:** the `loop` setting.
580 unit tests pass. The e2e test on the new loop, and the first live run, come in PR D.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
nisse_dev.loop runs alongside control.py, chosen with `loop: new`.
Issues assigned to the agent (by an operator, or filed by one) become work
items, each with a Matrix thread. A driver run is one model with tools
until it calls a waiting tool: restate puts rev-N up for a go-ahead, bound
to the revision on screen when the reply arrived; ask_human waits for an
answer; close_item closes the item and its issue. Before the go-ahead the
driver can look but not change or close anything. One item is active at a
time; replies in an item's thread go to it. On boot, an item cut off
mid-run waits on the operator, and waiting points are put up again.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
A work item starts from a fresh workspace: fetch, set aside (never
discard) leftover changes, branches and unpushed commits, a clean default
branch, and a branch of its own (D3, #62). start_milestone cuts the
milestone's branch; finish_milestone commits only the work, pushes and
opens a PR that closes the issue, under an intent, adopting an open PR and
treating a closed one as the operator's answer (D4). Between runs the
loop watches each item's PR: operator comments and inline review notes
(#69) reach the item, a merge or close hands back to the driver. A review
fix is checked after it's pushed and goes out as a new PR if the PR merged
first (D5). At boot an interrupted publish is settled against Gitea (L8).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
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.
M2.1, PR B of 4: the new loop's core. Stacked on #96 (PR A, threads); the base is
matrix-threads, so the diff is only B's changes.What's in it
A new package,
nisse_dev/loop/, built alongsidecontrol.py. Configloop: newchooses it inmain.py; the default staysolduntil the switch-over (M2.8).agent.py, the scheduler: one worker thread, inputs in order.goorapproveis a go-ahead bound to the revision on screen when it arrived (L3); a stale one is refused with which revision is up.abandoncloses the item and its issue.status, and a pointer to assigning an issue.driver.py, a driver run: one model with tools, looping until a waiting tool, the turn cap,stop, or a model failure.tools.py, control tools:restate,ask_humanandclose_item. Milestones come in PR C.write_file, noclose_item. That's enforced by what it's offered (P6, D1).items.py: an item's state (ready / running / waiting / closed) and its thread, issue and revision, in the work-items table.build.py: assembles it from the same services as the old agent. The driver uses theplannermodel until M2.6.assigned_issues, andassigned_byfrom the timeline. I probed the real format on the sandbox (issue #229, closed).Tests
test_loop.py, 19 tests with a scripted model:go→ work →close_itemcloses the issue;gorefused;gonot taken as a go-ahead;abandonandstatus;stop, and calls after a waiting tool.loop: newbuilds over the same services.loopsetting.580 unit tests pass. The e2e test on the new loop, and the first live run, come in PR D.
🤖 Generated with Claude Code
https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE