- tools.py: four flat tools (read_file, write_file, list_dir,
run_command) per A15's reduced manifest. Deliberately unsandboxed:
the VM is the trust boundary (B3) and tool use is auto-approved
inside an approved unit of work (A4), so path confinement would add
failure modes without adding safety. What is enforced is
operational — timeout with process-group kill so a backgrounded
server cannot outlive its command, output truncation so a build log
cannot eat the context window, closed stdin so prompts fail fast,
and a scrubbed environment so project code never sees the agent's
credentials
- executor.py: renders the log, calls the executor role with the
reduced manifest, dispatches tool calls, and checkpoints every
result (A13). Escalation is observable, never self-assessed (A12):
consecutive command failures and a hard turn cap. Failed reads do
not count — looking around is not failing
- Until delegation exists (step 7), escalation gates to the human
with the failures attached
- control.py: start milestone now runs the executor; a stopped
milestone gates to AWAIT_INPUT
Two bugs found by running it live against real models:
- A turn-ending gate tool (submit_plan) left a tool_call with no
tool_result, which providers reject outright, so every planning
turn after the first failed with a 400. The planner now records the
gate as the call's outcome, which is also the truthful transcript
- The renderer now emits a placeholder for any tool_call lacking a
result, so a crash between call and result (A3) cannot poison the
transcript permanently
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Empty commit: Gitea 1.27 exposes no rerun API endpoint, and Actions
secrets are read at run start, so a new run is needed to exercise the
live planning test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The virtual key now reaches a Haiku-class route, which holds the
tool-call contract llama3.2 could not: a real planning turn returns a
well-formed submit_plan call with an explicit, low-inference plan.
Wires NISSE_LIVE_PLANNER_MODEL through the CI workflow (add the
matching Actions secret to run it there) and records the finding in
.env.example.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 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>
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>
The integration step now receives the LLM and Matrix env vars from
repo secrets, so the live smoke tests run in CI when the runner can
reach the services; the skip-guards keep a missing secret or
unreachable service as a skip, never a failure. -rs surfaces skip
reasons in the CI log for diagnosis.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 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>
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>
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>
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>
Freezes the clock and asserts the exact bytes emitted per event, so
the on-disk log format is reviewable in the test file itself.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Every durable mutation emits one JSON-lines event, emitted by the
state store itself immediately after the commit returns, with a fixed
envelope (ts, agent, event, task_id correlation ID, cause) and a
dotted event taxonomy covering transitions, intents, gates, boot
reconciliation, and migrations. Logs stay diagnostic-only, never
load-bearing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
- 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>
Part H's repository-layout item is resolved: one repo, three packages.
Naming (S0), scope (S0.1), the step-10 extraction, and the repository
layout section are updated to match.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
nisse-core's history becomes the repo root; the spec moves to docs/ at
top level; nisse-dev and nisse-ops fold in as package directories, their
seed histories (pointer files only) discarded. Pointer READMEs and
CLAUDE.mds are rewritten for in-tree paths.
Rationale: for a solo operator the three-repo split tripled admin
surface (tokens, CI, webhooks, permissions, backups) and imposed a
version-pinning contract on nisse-core, while the repo boundary was
never a trust boundary — the VM is. The spec's Part H is updated in the
next commit to record the decision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Seeds nisse-core as the home for system-level design documentation. The
spec covers the shared library (Part A), both agents (Parts B and C), and
the supporting services, build order, and open decisions.
nisse-core holds this because the document describes the shared design
rather than either agent's implementation; the repo gains code later, at
the extraction step in the build order.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>