Conversations on the new loop (M2.5, PR A)
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 5m14s

- A top-level message that isn't a command is a conversation: an item in
  the `general` scope (L1), threaded under the operator's message, with
  `reply`, `ask_human` and `propose_work` and no workspace or execution.
  `general` is reserved as a project name.
- propose_work(project, title, summary) goes up as a revision for a
  go-ahead (or a plain yes). On go, under a file_issue intent (L8), the
  record issue is opened in that project, assigned to the agent and
  carrying the new item's ID; its item starts agreed, with the summary as
  its brief. Intake skips any issue carrying an ID (D1); boot adopts a
  filed issue or puts the proposal up again.
- A conversation closes after tuning.loop.general_idle_s (default a day);
  a reply in its thread then starts a new conversation that continues it.

Tests: conversation unit tests (threading, filing, the marker, an unknown
project, changes, idle close and continuation, a plain yes, both crash
cases, commands at top level); the walk with conversations, proposals and
idle time, and records_have_one_item / conversations_do_no_work; an e2e
conversation that becomes filed work.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
This commit is contained in:
dongarandClaude Opus 5.5 committed 2026-10-06 04:26:40 +00:00
1 parent d70d43a8cc
commit 3f54896f65
14 files changed
+811 -35

No files matched your search

+53
View File
@@ -482,3 +482,56 @@ def test_a_two_milestone_feature(
operator.merge_and_close(issues[0])
finally:
operator.tidy(issues, prs)
def test_a_conversation_becomes_filed_work(
running: tuple[RunningLoop, InMemoryTransport, str],
) -> None:
"""M2.5: a top-level message is a conversation; the work it turns into
is proposed, agreed, filed as a record issue, and started as agreed."""
agent, transport, run = running
operator = Operator(agent, transport)
gitea, repo = agent.gitea, agent.config.gitea_repo
issues: list[int] = []
prs: list[int] = []
def conversation() -> Any:
items = agent.worker.call(agent.store.work_items)
return next(i for i in items if i.scope == "general")
try:
root = transport.receive(
OPERATOR,
f"I've been thinking the sandbox could use this: {task(run)} "
"Could you file that as an issue and start on it?",
)
operator.settle()
deadline = time.monotonic() + DEADLINE_S
answers = 0
while loop_of(conversation()).wait != GO_AHEAD:
assert time.monotonic() < deadline, f"never proposed: {operator.last()}"
answers += 1
assert answers <= MAX_ANSWERS, f"never proposed: {operator.last()}"
transport.receive(
OPERATOR,
"Yes, propose it as an issue in the sandbox project.",
thread=root,
)
operator.settle()
assert operator.last().startswith(f"rev-{loop_of(conversation()).rev}:")
transport.receive(OPERATOR, "go", thread=root)
operator.settle()
[filed] = loop_of(conversation()).became
number = int(filed.split("#")[1])
issues.append(number)
data = gitea._request("GET", f"/repos/{repo}/issues/{number}")
assert "<!-- nisse-item: " in data["body"]
assert agent.account in [a["login"] for a in data["assignees"] or []]
item = operator.drive_until(MERGE, number)
assert loop_of(item).started # no second restatement
prs.append(loop_of(item).pr)
pr = gitea._request("GET", f"/repos/{repo}/pulls/{prs[0]}")
assert f"Closes #{number}" in pr["body"]
finally:
operator.tidy(issues, prs)