The reviewer is read-only (R3), so Gitea refuses it resolving a thread.
Now each reply says whether it settles its thread, and a settled reply
carries <!-- nisse-review: settled -->. nisse-dev, as the author, resolves
every comment on that thread's line when it watches the PR, and doesn't
take the settled reply as a point to answer. scripts/resolve_settled.py
does the same for PRs made by hand.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
Anyone's comment on an item's PR but an operator's (and its own) is a
peer's point (D5, L11): kept on the item with an id, in the transcript as
a `peer` message labelled with its author, and never an instruction. The
driver answers each with answer_peer, inline on its line or quoting it,
or skips it with skip_peer and a recorded reason. A run going back to
waiting with points open is reminded once; then the harness lists them
on the PR. A peer's comment wakes the item; a wake by peers alone goes
back to the same wait quietly, with one line in the thread. The
operator's inline comments are now answered inline too.
Also corrects ci.yml's header: a push to main runs no end-to-end tests.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
Shared parts, not a shared scheduler (agreed): nisse-review will have its
own small scheduler; a common one waits until two exist to compare.
- nisse_core.loop.driver: the driver run, moved; the agent names its
waiting tools per run (nisse-dev's are WAITING_TOOLS), and execution
goes to any Executor (ToolOutcome moves with it).
- nisse_core.loop.tools: the tool helper and the tools any agent offers
(notes, memory, memory_done, ask_expert); nisse_core.loop.expert.
- nisse_core.knowledge.handlers: Knowledge, the vault and memory as
control tools, the index and prompt text, out of LoopAgent.
- nisse_core.config: file reading, references, people, tuning sections of
the agent's own, LLM numbers, the vault.
- New core tests: a driver run with a non-workspace executor and the
agent's own waiting set; Knowledge alone; the shared config pieces.
Docs: the Development Plan marks M7.2 done; Architecture Part L and the
nisse-core README name the new parts.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
What isn't the dev agent's moves to nisse-core, unchanged in behaviour,
with its tests: canonical, render, render_anthropic, llm, catalog, probe,
context, views, state, reconcile, worker, cancel, dump, transport, matrix,
execution, testing, vault, memory, oplog, envfile and gitea (whole).
- nisse_core.state.StateStore holds the tables every agent has; an
agent's subclass adds its tables (version, migrations), roles and
views. nisse_dev.state.StateStore adds the build VM blocks (v5).
- views.project takes what each role is shown, rather than a fixed table.
- Vault takes the agent's name (agent=), for provenance.
- A test that nisse_core never imports an agent.
- The loop, config, git, delegation, expert, tools and evals stay in
nisse-dev until nisse-ops has a loop to share.
Docs: the Development Plan marks M7.1 done and notes that Phase 6 waits on
the agent host and Terraform; Architecture Part L names the package.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
A project's trust.merge is operator (the default) or approved (L3, D5,
#65). Set to approved, the harness, never the model, merges a PR in the
PR watch once an approving review from a gitea.operators account is on
its current commit, its checks pass (none at all counts; pending
waits) and it's mergeable; the item then goes on as after any merge.
Each reason to hold off (failed checks, conflicts, a refused merge) is
said once per commit.
Gitea enforces it: protection requires approvals from an operators-only
whitelist and dismisses stale ones. At start, for each approved
project, the agent checks by behaviour that its own unapproved merge is
refused, with a throwaway branch and PR made through the API (never the
workspace); if it isn't refused, the operator merges until a restart.
- gitea: approvals, combined check status, a PR's head commit and
mergeability, API-made branches and their deletion.
- Test and bring-up repositories get the protection.
- Tests: the decision (unit), the hard wall on a real Gitea
(test_trust_live), an e2e test where the operator only approves, and
a walk property, merges_only_approved, checked against a mutant that
ignores which commit was approved.
Closes#65
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
- A near miss for abandon ("abort", "cancel", "drop it", ...) in an item's
thread is asked about, and changes nothing.
- The driver is told only the operator stops or closes an item, so it
doesn't claim to have done either.
- An issue assigned again after its item closed starts a new item that
notes the one before (Gitea's timeline gives the assignment time);
item_for_issue returns the latest item.
- Abandoning an issue Gitea won't close because of open dependencies says
so, naming them.
- An agreed item isn't offered restate.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
- roles.driver (unset: the planner's model) and roles.classifier (unset:
the expert's); the mapping is pinned when an item opens, and the driver
and the go-ahead reader run on the item's own (L9).
- profiles: in configuration (tools, strengths, weaknesses), used in
briefings: the driver's instructions say what its helper and expert are
good at, the helper's brief who reads its handback; never a model name.
- delegate(task) and ask_expert(question) for the driver. A helper works
on a branch off the milestone's; on a handback its work comes back as
uncommitted changes, otherwise it is discarded, so harness branches
never hold commits only the clone has. ask_expert keeps its budget.
- Triggers: failure_limit failed commands in a row (Driver's new "stuck"
outcome) or the turn cap, mid-milestone, call a helper in once per
milestone; if it fails too, the item waits on the operator. A
delegation cut off by a crash is discarded at boot.
- Evaluation capture: delegations and ask_expert as before, and the
reader's verdicts as classifier pairs.
Tests: help tests (merge-back, discard, needs a milestone, the stuck
trigger and its once-per-milestone limit, the expert and its budget,
pinning, briefings by profile, boot discarding a cut-off delegation),
config tests for roles and profiles, and the walk delegating with a
scripted helper that sometimes fails, with a delegations_settle property.
The walk found helper commits stranded on a merged PR's branch, fixed by
returning a helper's work uncommitted.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
- propose_project(name, purpose, decisions) in a conversation goes up for
a go-ahead (checked: a slug, not an existing project, no trailing
question).
- On go, the provisioning checklist (D1): a repository with a README,
protection on its default branch, the agent as a collaborator, a build
VM, a deploy key, and the project entry as YAML with <...> to fill in.
- On "done", the agent reads its configuration again and adds the new
project live (build_loop_agent(load=...); only additions), then checks
what was provided (D4, verified, not assumed): permissions and
protection as it sees them, then on a throwaway branch with an empty
commit, a branch push must succeed and a push to the default branch and
merging its own PR must be refused. The check's PR is closed and its
branch deleted. Failures are said per item; nothing starts.
- Checked: the tracking issue (the distilled decisions, carrying the new
item's ID), and the project's item, agreed and labelled `design`, whose
first milestone is ARCHITECTURE.md and the build plan. A conversation
waiting on provisioning doesn't close as idle.
- The walk replays exactly from its seed (item IDs too), and event IDs
stay unique across simulated restarts, as a server's are.
- The test config holds output to 8192 tokens, under the test key's
per-request cost cap.
Tests: new-project unit tests (checklist and its YAML parse as config, done
before the entry, what isn't set up, a checked project's record and first
PR, an existing name), protection enforced by a hook on the fake forge,
the walk proposing projects and saying done, and an e2e new project in a
repository the test operator provisions from the agent's checklist.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
- 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
- Config: `projects: {name: entry}` with repo, base_branch, workspace,
build_host, push credential, labels (defaults added to every issue),
secrets by ${VAR} reference with visibility (commands | harness), and
egress (recorded, not enforced). Every problem in every entry is
reported at once; an inline secret, a bad visibility, a repeated repo or
a non-slug name is an error.
- The old loop runs exactly one project (only_project) and refuses more.
- The new loop is per project: each its own clone, workspace (with its
commands-visible secrets, redacted on return), push credential and
milestones; intake polls every project's repository; items are scoped
to their project for every run, publish and PR watch. Issues are named
project#14 when there is more than one project, and a bare #14 that
matches two is asked about. Switching projects parks the one worked on
last (all others after a boot).
- A restatement that ends in a question is sent back to the driver, not
posted: the e2e driver kept asking "Is this the right interpretation?",
which a plain yes can't answer.
Tests: config entries and errors, two-project assembly, two-project loop
tests (intake, own workspaces and repos, #14 in both, do-next, label
defaults, parking across projects), the property walk over two projects
with a new own_repository_only property (an item using the first
project's repo fails 15 of 16 seeds), and the restatement check.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
Several work items on the new loop (L1, D4):
- Switching parks the item on the workspace: its work is committed and its
branch pushed; resuming checks the item's branch back out. Branches of
open items are never set aside by a fresh workspace.
- Next item: the operator's "do #N next" first, otherwise the oldest.
- Issues with open Gitea dependencies are blocked until they close, checked
at intake and on every round; status shows blocked items and the line.
Tests: queue unit tests, the property walk over several issues with
dependencies and a new parked_work_safe property (it catches parking
disabled), and an end-to-end test with two issues where B depends on A.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
From the new loop's first live run (sandbox #255, PR #256):
- A question asked on the PR is answered on the PR (an inline one quoted
with its file and line, since the API can't reply in its thread), with
a pointer in the item's Matrix thread; one asked in Matrix stays there.
- wait_for_review: when a review needs no change, or only the operator's
merge is left, the item waits on the merge instead of on an answer.
- finish_milestone takes the PR's title from the driver.
The e2e test's merge waits out Gitea's brief 'try again later' after a
push.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
The first live run of the new loop's e2e test found that a fact recorded
while start_milestone was in flight landed between the tool call and its
result; both API formats reject that, so every later request in the run
failed. Views now keep each result right after its call. Scripted test
models check the APIs' ordering rules, so this can't pass unit tests
again.
The e2e tests no longer depend on the sandbox's history: each run asks for
a uniquely named function and works against a base branch of its own,
deleted afterwards. The review step asks for a change that can't already
be there. The scripted operator gives up after five fruitless answers, and
each run keeps its state store for inspection. The driver's prompt says
review fixes stay in the current milestone.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
test_loop_properties.py walks the new loop with a seeded mix of operator
and Gitea actions (go-aheads, stale go-aheads, answers, comments, merges,
the comment-then-merge race, closes, abandon, a crash and restart) and an
adaptive scripted model, checking every loop.qnt property after every
step. It found two bugs, both fixed with regression tests: abandoning an
item left its open PR open (the model's fix 1, now in code), and after a
review fix became a follow-up PR, the next fix went to the merged branch.
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