Commit Graph
7 Commits
Author SHA1 Message Date
dongarandClaude Opus 5.5 1ad0b2147d Retire the old build spec, and point every reference at the architecture (M0.1)
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m22s
Development Plan M0.1. Every section reference in code, tests, specs/,
the example config, the tuning guide, and the CLAUDE.md and README files
now cites docs/Architecture.md (P, L, D, O, X, Q) instead of the old
spec's A-H, and docs/Architecture-Build-Spec.md is deleted (git keeps
it). The plan's old-to-new table goes with it.

Mostly mechanical, with a per-site choice where one old section split:
A4 became L2 where it meant stop/abandon/the loop and L3 where it meant
approvals; A10 became L6 in the context-budget code and L7 elsewhere; B2
became D4 for outbound Gitea and publishing and D5 for review intake;
'A3 step N' became the matching step of L8's boot sequence. Comments
describing behaviour the architecture drops (planning always escalating,
thresholds tuned at an old build step) now describe the current code and
point at the Development Plan.

docs/Architecture.md gains one line the rewrite missed: intake outages
delay but never fail work (old B2), now in L5, where the poller and
Matrix code cite it.

Comments and docs only; no behaviour change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
2026-10-01 00:17:48 +00:00
dongarandClaude Opus 5.5 97863c1939 Bind approvals and feedback to the revision they were about
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m16s
Fixes counterexamples 1-4 from specs/orchestrator.qnt: a repeated
'approve', 'approve rev-1' meant for the architecture, approving on both
channels, and 'approve' plus an afterthought all landed on the next
plan, one the operator had not seen.

- Revisions are numbered across the work item (A4 already required
  monotonic tags; the code numbered per gate), so rev-N names one plan.
- Each reply is stamped on arrival with the revision that was up: in
  Matrix the one whose approval request had been sent (a plain value
  the transport thread reads, restored on boot), in Gitea the latest
  plan comment older than the reply (board.posts).
- Gate.evaluate applies a reply that names no revision only if its stamp
  is the current revision. Otherwise an approval is not applied and
  feedback rejects nothing (it is still in the transcript), and the
  agent says which revision is up. 'approve rev-N' binds by its number.
- Spec A4 states both rules. The model applies them; scenarios 1-4 now
  expect the properties to hold, and both held over 200,000 random
  traces and Apalache's exhaustive check to depth 8.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
2026-09-29 19:13:20 +00:00
dongarandClaude Opus 5.5 6afd0cc914 Match approvals and commands regardless of case and trailing punctuation
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m15s
A phone keyboard's "Approve" did not match the approval pattern, so it
counted as rejection feedback and threw the plan away; "Stop" did not
reach the cancel flag; other capitalised commands got a hint instead of
acting.

gates.command_form() is how a message reads as a command: case,
surrounding whitespace, inner runs of whitespace and a trailing '.' or
'!' do not matter. The approval gate and every command match in the
orchestrator (stop, status, abandon, use, start milestone, next
milestone, project complete, the restart choices) use it. Only matching
changes: the project name in 'use <name>' keeps its case, and feedback
and answers are recorded and passed on as written. A stale revision is
still refused, and approval still needs the whole message to be the
command.

Closes #64.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
2026-09-29 17:57:34 +00:00
dongarandClaude Opus 5.5 92e5452161 Never leave the agent stranded in a working state
CI / checks (push) Skipped
CI / checks (pull_request) Failing after 1m20s
The first live run found four dead ends one at a time: states where no
operator message could move the agent. Most had one shape: work ended,
by an exception or by a path that never transitioned, and left the agent
in PLANNING_* or EXECUTING, where every message is answered "busy".
The worker only prints an exception, so that would keep happening.

- Every job the orchestrator runs for an operator message or a Gitea
  comment now goes through _settled(). If the job leaves a working state
  behind, whatever the reason, the agent moves to AWAIT_INPUT with the
  continue / re-plan choice a restart offers, and says why. An exception
  in a waiting state is reported and the state kept, since its commands
  still lead on.
- A sign-off state whose gate record is missing asks to plan again
  instead of crashing on every reply.
- The stub configurations (no executor, no publisher) complete the
  milestone and wait in PR_OPEN instead of resting in EXECUTING.

test_no_stranding.py drives a whole two-milestone project with a fault
injected at each planner, executor and publisher call, and asserts after
every message that the agent is not in a working state and that the
project still reaches 'project complete'.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
2026-09-29 16:39:20 +00:00
dongarandClaude Opus 5.5 b0c5f48be4 Handle leading mentions in Matrix messages
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m24s
Clients put the addressee's name in front of a message that mentions
it ("Nisse Dev Testing: re-plan"), and the transport passed that text
on whole, so no command matched: the live run's re-plan was asked again,
and an addressed 'approve' would have counted as rejection feedback.

- A leading mention of the agent (an HTML pill, its user ID, or its
  display name followed by ':') is removed before commands are matched.
  The display name is learnt from the agent's own member event.
- A message whose leading mention is someone else is skipped: in a
  shared room it is addressed to another agent or person.
- Only the start counts; a mention later in the text is left alone.
- The transcript records the message as sent (InboundMessage.original).

The sender is unaffected: it still comes from the event's homeserver-set
sender field, never from the text.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 15:59:27 +00:00
dongarandClaude Opus 5.5 e5450e3e77 Keep a pending question across restarts, and make the restart question answerable
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m18s
A restart in AWAIT_INPUT replaced the saved pending item with a generic
"continue or re-plan?" whose answer nothing handled: the reply cleared
it and left the agent in AWAIT_INPUT with nothing to resume, where 'use'
was refused too. The first live run hit this after a planning failure
and could not be moved by any message.

- Waiting on the operator is a committed state (A3 step 8): on boot, ask
  the saved question again instead of replacing it. _await_input keeps
  the prompt with the pending item for this.
- The restart question (A3 step 7) records the state it interrupted, and
  'continue' / 're-plan' now do what they offer: re-run the interrupted
  planning turn, or return an interrupted milestone to its approved plan.
  Any other reply asks again rather than being swallowed.
- With nothing pending, 'use <project>' starts over, as the reply
  already advised.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 15:03:37 +00:00
dongarandClaude Fable 5 a251104a44 Add control loop, gating primitives, and transport interface (A4/B1)
CI / checks (push) Successful in 9s
CI / checks (pull_request) Successful in 9s
Step-2 PR A: the approval loop end to end with a stub agent, no LLM
and no network.

- transport.py: Transport interface per A1 (send_message,
  send_approval_request, ask_question, on_message) with an
  InMemoryTransport; no platform SDK outside an implementation
- gates.py: PlanRevision with monotonic rev-N tags; Gate.evaluate
  enforces revision binding (stale tags refused with a notice),
  operator allowlisting, and approve-word-only semantics; any other
  reply is rejection feedback. Channel seam ready for Gitea/Vikunja
  approval sources
- control.py: B1 state machine subset with structural gating — the
  agent's only actions are SubmitPlan (ends its turn) and AskHuman;
  no proceed tool exists. Gate state, revision counters, task_id,
  and pending questions checkpoint through StateStore, and boot
  resumes an open gate or gates to AWAIT_INPUT, never blind-resumes
- gate.open/approve/reject A9 events complete the log taxonomy
- CLAUDE.md: record the plan-first working convention
- Tests: 15 unit (gates + orchestrator flows), 2 integration
  (mid-gate restart with revisions intact; unresolved intents gate
  the whole boot)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-05 21:28:07 +00:00