The new loop's core: intake, restatement, go-ahead, threads (M2.1, PR B) #97

Merged
dongar merged 3 commits from loop-core into main 2026-10-03 16:42:57 +00:00
Collaborator

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.ai/code/session_01PPQ14L3LikyefCMFBqEbKE

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
dongar changed target branch from matrix-threads to main 2026-10-03 16:42:41 +00:00
dongar added 3 commits 2026-10-03 16:42:41 +00:00
The new loop's core: intake, restatement, go-ahead, threads (M2.1, PR B)
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m44s
f84bb70839
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
Milestones and review in the new loop (M2.1, PR C)
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m48s
21744efac1
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
Reviewed-on: dongar/nisse#98
Reviewed-by: Don Garrett <dgarrett@acm.org>
dongar approved these changes 2026-10-03 16:42:53 +00:00
dongar merged commit d05d000dd5 into main 2026-10-03 16:42:57 +00:00
dongar deleted branch loop-core 2026-10-03 16:42:57 +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#97