Commit Graph
10 Commits
Author SHA1 Message Date
dongarandClaude Opus 5 e04d786840 Assert the live Matrix tests run as a testing identity
CI / checks (pull_request) Successful in 12s
CI / checks (push) Successful in 14s
Two invariants only a live homeserver can check, now enforced rather
than verified by hand:

- The configured NISSE_MATRIX_USER_ID must be the token's real owner.
  MatrixTransport filters its own messages by comparing sender to that
  value, so a mismatch would make the bot read its own posts as
  operator input.
- The account must be a testing identity, so a stray live run can
  never act as the production agent.

Both hold today; this keeps them holding when the production
nisse-dev account is created.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 20:16:18 +00:00
dongarandClaude Opus 5 cf85648719 Add LLM-backed planning agent (A4/A11/A13) — step 5a
CI / checks (push) Successful in 12s
CI / checks (pull_request) Successful in 11s
- planner.py: LLMPlanningAgent offers exactly two tools, submit_plan
  and ask_human, so gating stays structural at the tool layer; no
  proceed tool exists. System prompt requires explicit, low-inference
  plans because a weaker model executes them (A13)
- Model output is appended to the canonical log as it arrives, before
  being interpreted, so a crash mid-planning leaves a truthful
  transcript
- A bad turn (prose instead of a tool call, malformed arguments, or a
  failed call) retries once, then raises PlanningError; the
  orchestrator gates to AWAIT_INPUT rather than inventing a plan
- Orchestrator: appends every operator message, plan_submitted, and
  gate_opened to the transcript, so re-planning sees rejection
  feedback as conversation rather than out-of-band parameters; pins
  the A11 role mapping at task start
- llm.py: non-JSON or non-object tool arguments now raise LLMError
  instead of escaping as JSONDecodeError — observed from llama3.2,
  which emits truncated argument strings
- Live planning test is opt-in on NISSE_LIVE_PLANNER_MODEL: planning
  escalates unconditionally (A12) and the local model does not hold
  the tool-call contract, so it awaits a frontier route

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 14:47:12 +00:00
dongarandClaude Opus 5 10aa77899e Gate live tests on one sentinel var each, fail on partial config
CI / checks (push) Successful in 16s
CI / checks (pull_request) Successful in 10s
Skipping now keys solely on NISSE_LLM_BASE_URL and
NISSE_MATRIX_HOMESERVER: if the sentinel is set, the live path is
expected to work and any other missing variable is an assertion
failure naming it, not a silent skip. A misnamed or misplaced CI
secret previously produced a green run with no live coverage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 14:33:14 +00:00
dongarandClaude Fable 5 5db6d9d96e Add MatrixTransport (A1) — step-2 PR B
CI / checks (push) Successful in 10s
CI / checks (pull_request) Successful in 10s
- matrix.py: Transport implementation over the Matrix client-server
  HTTP API directly (token auth, /sync long-poll, /send) using the
  existing httpx dependency — no Matrix SDK; four endpoints total.
  Deviation from the presented plan (matrix-nio) flagged in the PR
- First sync learns position and skips backlog, so room history is
  never delivered as context (A1); own messages and non-message
  events filtered; sender MXID passed through for the orchestrator's
  operator allowlist
- Sync loop backs off (capped at 60s) and retries indefinitely on
  homeserver errors, mirroring B2's polling posture
- Encrypted rooms unsupported: bot rooms are created unencrypted
  until E2E lands (operator decision; room recreated accordingly)
- NISSE_MATRIX_TEST_ROOM_ID added: the live smoke test binds only
  to the dedicated test room, never the production room
- 8 unit tests over a mocked HTTP layer; live integration test
  verified against the real homeserver

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-06 23:52:45 +00:00
dongarandClaude Fable 5 0c2021fced Add LLM client interface with LiteLLM adapter (A5/A11)
CI / checks (pull_request) Successful in 10s
CI / checks (push) Successful in 9s
PR 2 of the steps-3/4 plan.

- llm.py: LLMClient protocol — complete(events, tools, model) ->
  canonical events — present from the first real LLM call.
  LiteLLMClient renders requests via the A10 renderer, targets the
  proxy's OpenAI-compatible route, and maps provider tool-call IDs
  to locally-minted canonical IDs at the boundary
- RoleMap (A11): executor/planner/delegate/consultant resolved from
  env; pinned() returns the mapping to record at task start
- Config strictly from env: NISSE_LLM_BASE_URL, NISSE_LLM_TOKEN;
  envfile.py is a minimal .env loader where real env wins; .env now
  gitignored with a committed .env.example documenting all variables
  (Matrix ones included for the upcoming transport PR)
- Live smoke test against the proxy is integration-marked and skips
  without credentials — CI stays credential-free per operator
  decision; the test runs locally once .env exists
- httpx added as nisse-dev's HTTP client

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-06 20:57:51 +00:00
dongarandClaude Fable 5 ac81dd6234 Add canonical event log and renderer skeleton (A10)
CI / checks (push) Successful in 9s
CI / checks (pull_request) Successful in 8s
Build-order step 4, sequenced before the LLM client (step 3) because
the client interface consumes the canonical log.

- canonical.py: typed events for the full A10 table (user_message
  through compaction); canonical tool-call IDs minted locally,
  provider IDs never a join key; strict payload validation on load
- StateStore: canonical_events table via schema v2 — the first real
  migration, exercised in a test against a hand-built v1 database —
  with append_event/read_events on the same fsync-per-commit path
- render.py: request bodies rendered from the log at call time;
  first adapter targets the OpenAI-compatible shape of LiteLLM's
  normalized route; compaction events replace their id range at
  render time while staying in the durable log
- Crash integration test now also verifies transcript appends
  survive process death

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-06 19:51:58 +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
dongarandClaude Fable 5 18d4fcd701 Implement A9 state-change logging in the state store
CI / checks (push) Successful in 9s
CI / checks (pull_request) Successful in 8s
Retrofit for the conventions merged in #4 (issue #3):

- OpLog: JSON-lines emitter with the fixed envelope (ts, agent,
  event, task_id, cause); task_id set by the orchestrator at
  unit-of-work start
- StateStore owns emission: transition, intent begin/commit/abort,
  and schema migrations each emit their event right after the
  durable write commits; callers pass the cause
- reconcile_on_boot emits boot.reconcile with action and verdicts,
  and resolves intents with reconciliation-verdict causes
- Failed resolutions emit nothing (a line exists iff the write
  committed)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-05 21:13:36 +00:00
dongarandClaude Fable 5 4319882f35 Add durable state store and boot-time reconciliation (A2/A3)
CI / checks (push) Successful in 8s
CI / checks (pull_request) Successful in 7s
Build-order step 1: the critical path everything else sits on.

- StateStore: SQLite in WAL mode with synchronous=FULL, so every
  state transition and intent-log write is committed and fsynced
  before returning; a power cut cannot tear state
- Durable intent log: begin_intent records a unit of work before it
  starts; commit/abort resolve it; pending intents survive restart
- schema_version stamped from the first commit, with a sequential
  migration hook; a newer state file than the code supports is
  refused rather than misread
- reconcile_on_boot (A3 first cut): pending intents are checked
  against external truth by registered reconcilers (observable
  checks, never model self-assessment); verified effects are
  committed, verified no-ops aborted, anything unverifiable gates
  to AWAIT_INPUT — never blind-resume
- Lives in nisse_dev per spec 0.1: the library is extracted after
  the dev agent works, not designed up front
- Integration test kills a subprocess mid-work with os._exit and
  verifies state integrity and intent visibility on reopen

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-05 18:59:15 +00:00
dongarandClaude Fable 5 c1041a5ba1 Scaffold hello-world apps, test frameworks, and Gitea CI
CI / checks (push) Successful in 45s
CI / checks (pull_request) Successful in 6s
- uv workspace tying nisse-core, nisse-dev, nisse-ops together
  (src layouts, workspace-sourced nisse-core dependency)
- Placeholder hello-world entry points for both agents, calling
  through nisse-core to exercise the package boundary
- pytest unit tests per package; integration tests (marker-gated)
  running the installed console scripts as subprocesses
- ruff lint + format, nisse_* declared first-party
- Gitea Actions workflow running lint, format check, unit and
  integration tests; scripts/check.sh mirrors it locally

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-05 18:48:51 +00:00