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
This commit is contained in:
dongarandClaude Opus 5.5 committed 2026-10-01 00:17:48 +00:00
1 parent 890708594b
commit 1ad0b2147d
73 files changed
+259 -1786

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
"""The `abandon` command: ending a task from any waiting state (A4, B2)."""
"""The `abandon` command: ending a task from any waiting state (L2)."""
import io
from pathlib import Path
+6 -6
View File
@@ -137,8 +137,8 @@ def test_the_whole_graph_assembles(tmp_path: Path, origin: Path, monkeypatch) ->
assert orchestrator._board is not None
assert orchestrator._delegate is not None
assert orchestrator._poller_factory is not None
assert orchestrator._executor._delegate is not None # A12 escalation path
assert orchestrator._executor._expert is not None # A14 consult path
assert orchestrator._executor._delegate is not None # L9 escalation path
assert orchestrator._executor._expert is not None # L9 consult path
agent.stop()
@@ -181,7 +181,7 @@ def test_poller_factory_ignores_the_agents_own_comments(
def test_delegation_uses_the_passthrough_route(
tmp_path: Path, origin: Path, monkeypatch
) -> None:
# A13: delegations run vendor-native; the normalized route is for the
# L9: delegations run vendor-native; the normalized route is for the
# places where fidelity is cheap.
from nisse_dev.llm import AnthropicPassthroughClient, LiteLLMClient
@@ -203,7 +203,7 @@ def test_httpx_clients_are_not_shared_between_routes(
agent.stop()
# --- startup model discovery (A5/A11) ------------------------------------
# --- startup model discovery (L9) ------------------------------------
def test_a_typod_role_refuses_to_boot(tmp_path: Path, monkeypatch) -> None:
@@ -233,7 +233,7 @@ def test_a_role_this_key_cannot_reach_refuses_to_boot(
def test_a_tool_less_model_warns_but_still_boots(tmp_path: Path, monkeypatch) -> None:
# A15 expects weak local models to be accommodated, and models LiteLLM
# L9 expects weak local models to be accommodated, and models LiteLLM
# declares tool-less are observed driving tool loops anyway.
import io
@@ -285,7 +285,7 @@ def test_the_resolved_model_is_pinned_at_task_start(
def test_delegation_records_what_the_alias_resolved_to(
tmp_path: Path, origin: Path, monkeypatch
) -> None:
# A16 eval history keyed on an alias alone would average across whatever
# L9 eval history keyed on an alias alone would average across whatever
# that alias pointed at over time.
agent = build_offline(tmp_path, origin, monkeypatch)
assert agent.orchestrator._delegate._backing_model == "anthropic/claude-opus-5"
+1 -1
View File
@@ -1,4 +1,4 @@
"""Approvals and feedback bind to the plan revision they were about (A4)."""
"""Approvals and feedback bind to the plan revision they were about (L3)."""
import io
from pathlib import Path
+4 -4
View File
@@ -1,5 +1,5 @@
"""Plan externalisation to Gitea and approvals arriving on either channel
(spec A4, Part H)."""
(architecture L3, D2)."""
import io
from pathlib import Path
@@ -96,8 +96,8 @@ def test_project_start_opens_an_issue_and_posts_the_plan(tmp_path: Path) -> None
issue, body = gitea.comments[0]
assert issue == 1
assert "plan 1" in body
assert "approve rev-1" in body # the tag is printed for copy-paste (A4)
# The store keeps a pointer, not the plan record itself (Part H).
assert "approve rev-1" in body # the tag is printed for copy-paste (L3)
# The store keeps a pointer, not the plan record itself (D2).
assert store.get_state().data["board"]["issue"] == 1
@@ -136,7 +136,7 @@ def test_non_operator_gitea_comment_cannot_approve(tmp_path: Path) -> None:
def test_agents_own_comment_cannot_approve_its_plan(tmp_path: Path) -> None:
# The agent posts plans as nisse-dev-testing; that login is not on the
# operator allowlist, so its own comments can never satisfy its gate (A4).
# operator allowlist, so its own comments can never satisfy its gate (L3).
gitea = FakeGitea()
orch, store, transport, _ = build(tmp_path, gitea)
transport.receive("don", "use demo")
+1 -1
View File
@@ -77,7 +77,7 @@ def test_unknown_fields_rejected() -> None:
# The v1 schema as it shipped (PR #2), frozen here to prove a newer image
# opens and upgrades a database written by the older one (A2).
# opens and upgrades a database written by the older one (L8).
V1_SCHEMA = [
"CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)",
"""
+3 -3
View File
@@ -1,4 +1,4 @@
"""Runtime model discovery (spec A5/A11/A13).
"""Runtime model discovery (architecture L9).
The point of these tests is that nothing here reads the model's *name*. A
semantic alias like "remote-architecture" is free to point anywhere, so the
@@ -78,7 +78,7 @@ def test_a_semantic_alias_routes_by_its_provider() -> None:
def test_the_same_alias_reroutes_when_it_is_repointed() -> None:
# A13: provisionally accept LiteLLM's normalization for a vendor with no
# L9: provisionally accept LiteLLM's normalization for a vendor with no
# native adapter, rather than sending it Anthropic-shaped bodies.
cat = catalog(entry("remote-architecture", "openai/gpt-6-astra", "openai"))
normalized, passthrough = object(), object()
@@ -189,7 +189,7 @@ def test_discovery_asks_the_proxy_with_the_token() -> None:
def test_pinning_records_what_the_alias_resolved_to() -> None:
# An alias may point somewhere else tomorrow; eval history (A16) keyed on
# An alias may point somewhere else tomorrow; eval history (L9) keyed on
# the alias alone would average across whatever it pointed at.
cat = catalog(entry("remote-architecture", "anthropic/claude-opus-5", "anthropic"))
pinned = cat.pinned({"planner": "remote-architecture"})
+1 -1
View File
@@ -281,7 +281,7 @@ def test_the_example_file_is_valid() -> None:
def test_state_db_and_workspace_are_separate() -> None:
# A2: build artefacts must not share a volume with agent state.
# L8: build artefacts must not share a volume with agent state.
config = build()
assert config.state_db.parent != config.workspace
+1 -1
View File
@@ -1,4 +1,4 @@
"""The context budget (spec A10)."""
"""The context budget (architecture L6)."""
import io
from pathlib import Path
+1 -1
View File
@@ -180,7 +180,7 @@ def test_ask_human_pauses_and_answer_resumes(tmp_path: Path) -> None:
assert "monorepo" in h.transport.outbound[-1].text
# --- restarts (A3 steps 7 and 8) --------------------------------------------
# --- restarts (L8 boot steps 4 and 5) --------------------------------------------
class FailsOnce:
+12 -12
View File
@@ -121,7 +121,7 @@ def test_delegation_works_on_its_own_branch(
def test_abort_discards_the_branch_and_its_work(
store: StateStore, ws: Workspace, repo: GitRepo
) -> None:
# A13: abort must leave nothing to resume on top of.
# L9: abort must leave nothing to resume on top of.
client = ScriptedClient(
[
[call("write_file", path="half-done.py", content="oops")],
@@ -138,7 +138,7 @@ def test_abort_discards_the_branch_and_its_work(
def test_stop_discards_the_branch_and_settles_the_intent(
store: StateStore, ws: Workspace, repo: GitRepo
) -> None:
# A4 stop is treated like a failure, not like an outage: the operator
# L2's stop is treated like a failure, not like an outage: the operator
# stopped it for a reason, so nothing is kept for resuming.
cancel = Cancel()
client = ScriptedClient(
@@ -213,7 +213,7 @@ def test_cap_breach_is_not_usable_and_discards_work(
brief="b", branch="d"
)
assert not result.usable # the caller must gate, not resume (A13)
assert not result.usable # the caller must gate, not resume (L9)
assert not (repo.root / "partial.py").exists()
@@ -251,7 +251,7 @@ def test_composite_events_record_the_brief_caps_and_outcome(
assert started.brief == "the brief"
assert started.model == "claude-haiku-4-5"
assert started.caps == {"turns": 7, "tokens": 1234, "wall_clock_s": 42.0}
# A16: this pair is the eval record, so it must carry the outcome.
# L9: this pair is the eval record, so it must carry the outcome.
assert completed.exit_status == "handback"
assert "greet()" in completed.handback
@@ -259,7 +259,7 @@ def test_composite_events_record_the_brief_caps_and_outcome(
def test_hand_back_tool_is_offered_alongside_the_executor_tools() -> None:
names = [t["function"]["name"] for t in DELEGATE_TOOLS]
assert names[-1] == "hand_back"
assert "run_command" in names # full tool access, not a consult (A13)
assert "run_command" in names # full tool access, not a consult (L9)
def test_malformed_handback_is_rejected_and_the_delegation_continues(
@@ -348,7 +348,7 @@ def test_failure_may_be_reported_without_doing_work(
assert len(client.calls) == 1 # believed immediately, not challenged
# --- A3 step 5: the resume rule -----------------------------------------
# --- L8 boot step 5: the resume rule -----------------------------------------
def pending_delegation_intent(store: StateStore, branch: str = "d") -> Any:
@@ -446,7 +446,7 @@ def test_resume_continues_the_same_delegation(
store: StateStore, ws: Workspace, repo: GitRepo
) -> None:
# Interrupt after one turn, then resume: no second delegation_started,
# because A10 defines a session as the delegation, not the process.
# because L7 defines a session as the delegation, not the process.
repo.checkout_new_branch("d")
store.append_event(
DelegationStarted(brief="b", role="delegate", model="m", caps={})
@@ -497,7 +497,7 @@ def test_recover_resumes_a_clean_cut_with_a_notice(
result = build(client, store, ws, repo).recover(notices.append)
assert result is not None and result.usable
# A3: a notice, not a question — a clean cut needs no operator decision.
# L8: a notice, not a question — a clean cut needs no operator decision.
assert notices and "resuming the delegation" in notices[0]
@@ -550,7 +550,7 @@ def test_recover_leaves_an_unclean_cut_for_the_boot_gate(
assert [i.kind for i in store.pending_intents()] == [DELEGATION_INTENT]
# --- the token cap, which A13 calls hard and was decoration -------------
# --- the token cap, which L9 calls hard and was decoration -------------
class MeteredClient(ScriptedClient):
@@ -599,7 +599,7 @@ def test_tokens_spent_are_reported_not_left_at_zero(
completed = next(
e.event for e in store.read_events() if isinstance(e.event, DelegationCompleted)
)
# A16 evaluates cost against outcome, which needs a real number.
# L9 evaluates cost against outcome, which needs a real number.
assert completed.tokens == 2000
@@ -639,7 +639,7 @@ def test_budget_exhaustion_preserves_the_work_for_later(
# The work survives, on its branch.
assert repo.current_branch() == "d"
assert (repo.root / "progress.py").exists()
# And the intent stays pending, which is what A3's resume rule keys on.
# And the intent stays pending, which is what L8's resume rule keys on.
assert [i.kind for i in store.pending_intents()] == [DELEGATION_INTENT]
@@ -647,7 +647,7 @@ def test_an_ordinary_failure_still_discards(
store: StateStore, ws: Workspace, repo: GitRepo
) -> None:
# Only external interruptions are preserved; a delegate that made a mess
# is still cleaned up (A13).
# is still cleaned up (L9).
client = ScriptedClient([[hand_back(succeeded=False, changed="cannot")]])
result = build(client, store, ws, repo).run(brief="b", branch="d")
+1 -1
View File
@@ -54,7 +54,7 @@ def test_pairs_a_start_with_its_completion(store: StateStore) -> None:
def test_unpaired_start_is_skipped(store: StateStore) -> None:
# A crash mid-delegation leaves a start with no completion; a half-formed
# record would pollute the set A16 exists to make trustworthy.
# record would pollute the set L9 exists to make trustworthy.
store.append_event(
DelegationStarted(brief="died", role="delegate", model="m", caps={})
)
+1 -1
View File
@@ -317,7 +317,7 @@ def test_ask_expert_is_offered_only_when_a_desk_exists(
names = [t["function"]["name"] for t in client2.calls[0]["tools"]]
assert "ask_expert" in names
assert names[-1] == "finish"
# A12: in addition to the observable triggers, not instead of them.
# L9: in addition to the observable triggers, not instead of them.
assert "run_command" in names
+3 -3
View File
@@ -109,7 +109,7 @@ def test_consult_is_stateless_and_uses_the_consultant_role(
desk(client, store).ask("Is this API deprecated?")
call = client.calls[0]
# A14: no repo access, no session state — exactly the question, and none
# L9: no repo access, no session state — exactly the question, and none
# of the log the caller was working from.
assert [e.event.text for e in call["events"]] == ["Is this API deprecated?"]
assert all("secret repo context" not in e.event.text for e in call["events"])
@@ -131,7 +131,7 @@ def test_retry_succeeds_after_a_bad_first_reply(store: StateStore) -> None:
def test_consults_are_recorded_as_composite_events(store: StateStore) -> None:
# A16 wants a consult/delegation eval set; reusing the pair gets consult
# L9 wants a consult/delegation eval set; reusing the pair gets consult
# evaluation for free.
desk(ScriptedClient([GOOD]), store).ask("q")
events = [e.event for e in store.read_events()]
@@ -183,4 +183,4 @@ def test_budget_counts_only_after_the_task_marker(store: StateStore) -> None:
def test_default_budget_is_small() -> None:
assert DEFAULT_BUDGET <= 5 # A14: do not let this become a second subsystem
assert DEFAULT_BUDGET <= 5 # L9: do not let this become a second subsystem
+2 -2
View File
@@ -44,7 +44,7 @@ def test_unknown_sender_is_silently_ignored() -> None:
def test_approve_with_trailing_words_is_rejection() -> None:
# "approve, but rename the module" is feedback, not approval (A4: the
# "approve, but rename the module" is feedback, not approval (L3: the
# typed word approve, nothing else).
result = gate().evaluate("don", "approve, but rename the module", "transport")
assert isinstance(result, Rejected)
@@ -69,7 +69,7 @@ def test_a_sentence_containing_approve_is_still_feedback() -> None:
assert result.feedback == "I approve of most of it"
# --- binding by arrival (A4): a reply that names no revision is about the one
# --- binding by arrival (L3): a reply that names no revision is about the one
# that was up when it arrived.
+1 -1
View File
@@ -163,7 +163,7 @@ def test_list_comments_passes_since() -> None:
assert seen["since"] == "2026-09-07T00:00:00Z"
# --- reconciler (A3 external truth) --------------------------------------
# --- reconciler (L8 external truth) --------------------------------------
def test_reconciler_reports_existing_pr_as_done() -> None:
+1 -1
View File
@@ -360,7 +360,7 @@ def test_budget_refusal_is_a_kind_of_upstream_trouble() -> None:
assert issubclass(BudgetExhausted, UpstreamUnavailable)
# --- tool_choice (A15: constrained decoding) --------------------------------
# --- tool_choice (L9: constrained decoding) --------------------------------
TOOLS = [
{
+1 -1
View File
@@ -1,4 +1,4 @@
"""State-change log conventions (spec A9): every durable mutation emits one
"""State-change log conventions (architecture L12): every durable mutation emits one
JSON-lines event with the fixed envelope, from the store's own write path."""
import io
+2 -2
View File
@@ -88,7 +88,7 @@ def test_outage_does_not_raise_out_of_the_loop() -> None:
time.sleep(0.05)
poller.stop()
# It recovered and delivered rather than dying on the outage (B2: an
# It recovered and delivered rather than dying on the outage (L5: an
# outage delays a reaction, never fails a task).
assert [c.id for c in seen] == [1]
assert "gitea.poll_failed" in stream.getvalue()
@@ -118,7 +118,7 @@ def test_backoff_grows_then_caps() -> None:
delays = [json.loads(line)["retry_in_s"] for line in stream.getvalue().splitlines()]
assert delays[:4] == [0.001, 0.002, 0.004, 0.008]
assert max(delays) == 0.008 # capped, and retried indefinitely (B2)
assert max(delays) == 0.008 # capped, and retried indefinitely (L5)
assert MAX_BACKOFF_S <= 300.0 # the shipped cap is "a few minutes"
+1 -1
View File
@@ -1,4 +1,4 @@
"""The B1 loop past EXECUTING: publish, review gate, then next milestone or
"""The loop past EXECUTING (D4, D5): publish, review gate, then next milestone or
project completion."""
import io
+1 -1
View File
@@ -74,7 +74,7 @@ def test_compaction_replaces_its_range_only() -> None:
def test_tool_call_without_a_result_gets_a_placeholder() -> None:
# Providers reject a dangling tool_use; this happens after a crash (A3)
# Providers reject a dangling tool_use; this happens after a crash (L8)
# or with turn-ending gate tools like submit_plan.
events = stored(
[
+1 -1
View File
@@ -1,4 +1,4 @@
"""Reacting to PR review comments while in PR_OPEN (spec B2)."""
"""Reacting to PR review comments while in PR_OPEN (architecture D5)."""
import io
from pathlib import Path
+1 -1
View File
@@ -1,4 +1,4 @@
"""The operator's kill switch, `stop` (spec A4)."""
"""The operator's kill switch, `stop` (architecture L2)."""
import io
import threading
+2 -2
View File
@@ -13,7 +13,7 @@ def ws(tmp_path: Path) -> Workspace:
def test_manifest_is_small_and_flat() -> None:
# A15: fewer tools, flatter arguments for the local executor.
# L9: fewer tools, flatter arguments for the local executor.
names = [t["function"]["name"] for t in EXECUTOR_TOOLS]
assert names == ["read_file", "write_file", "list_dir", "run_command"]
for tool in EXECUTOR_TOOLS:
@@ -62,7 +62,7 @@ def test_run_command_success_reports_exit_code_and_stdout(ws: Workspace) -> None
def test_nonzero_exit_is_a_result_not_an_error(ws: Workspace) -> None:
outcome = ws.run_command("exit 3")
assert not outcome.success # feeds the A12 failure count
assert not outcome.success # feeds the L9 failure count
assert "exit code: 3" in outcome.result