Commit Graph
334 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 bacf65dbe2 Fix AWAIT_INPUT deadlock and make the live milestone test robust
CI / checks (push) Successful in 36s
CI / checks (pull_request) Successful in 29s
Found while chasing a flaky live test, but the flakiness was a real
bug: only the ask_human path recorded a pending item, so AWAIT_INPUT
reached any other way — planning failure, stopped milestone, boot
reconciliation — refused every operator reply with "no pending
question to answer" and the agent could never be recovered. The
operator had no way back short of wiping state.

Every path into AWAIT_INPUT now records what a reply should resume,
and answering dispatches on it: planning failures and questions
resume planning, a stopped milestone returns to its still-approved
gate, and the fallback says what commands work instead of
dead-ending. Two regression tests cover the recovery paths.

The live test also scripted a fixed approve/approve/start sequence,
which a real model breaks by legitimately calling ask_human at a
gate. It now plays the operator adaptively, responding to whatever
the agent actually did. Five consecutive live runs pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 16:46:07 +00:00
dongarandClaude Opus 5 999a38a705 Fix process-group kill test to work without an init process
CI / checks (push) Failing after 23s
CI / checks (pull_request) Successful in 30s
os.kill(pid, 0) succeeds on a zombie, and a killed orphan stays a
zombie until reaped — which never happens inside a CI container with
no init. The test read that as "still alive" and failed there while
passing locally. It now reads the process state from /proc, with a
control assertion so a broken liveness check cannot pass silently.
The group kill itself was correct.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 16:37:38 +00:00
dongarandClaude Opus 5 0fd0b2e4a5 Add executor tool loop (A12/A15) — step 5b
CI / checks (push) Failing after 13s
CI / checks (pull_request) Failing after 13s
- 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>
2026-09-07 16:35:39 +00:00
dongar 47b2bdfdbd Merge pull request 'Add LLM-backed planning agent (A4/A11/A13) — step 5a' (#11) from llm-planning into main
CI / checks (push) Successful in 22s
Reviewed-on: dongar/nisse#11
Reviewed-by: Don Garrett <dgarrett@acm.org>
2026-09-07 16:15:47 +00:00
dongarandClaude Opus 5 fb35ca774d Trigger CI to pick up the NISSE_LIVE_PLANNER_MODEL secret
CI / checks (push) Successful in 20s
CI / checks (pull_request) Successful in 18s
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>
2026-09-07 16:02:56 +00:00
dongarandClaude Opus 5 6c17f2bf57 Enable the live planning test against claude-haiku-4-5
CI / checks (push) Successful in 13s
CI / checks (pull_request) Successful in 12s
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>
2026-09-07 16:01: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
dev-agent ac9ae27bdc Merge pull request 'Wire live-test credentials into CI from Actions secrets' (#10) from ci-live-secrets into main
CI / checks (push) Successful in 11s
2026-09-07 14:41:26 +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 968347a6be Wire live-test credentials into CI from Actions secrets
CI / checks (pull_request) Successful in 16s
CI / checks (push) Successful in 10s
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>
2026-09-07 14:28:11 +00:00
dongar daf1163b44 Merge pull request 'Add MatrixTransport (A1) — step-2 PR B' (#9) from matrix-transport into main
CI / checks (push) Successful in 9s
Reviewed-on: dongar/nisse#9
Reviewed-by: Don Garrett <dgarrett@acm.org>
2026-09-06 23:55:49 +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
dongar 5dad5cf90f Merge pull request 'Add LLM client interface with LiteLLM adapter (A5/A11)' (#8) from llm-client into main
CI / checks (push) Successful in 10s
Reviewed-on: dongar/nisse#8
Reviewed-by: Don Garrett <dgarrett@acm.org>
2026-09-06 22:31:35 +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
dongar e185bb7c88 Merge pull request 'Add canonical event log and renderer skeleton (A10)' (#7) from canonical-log into main
CI / checks (push) Successful in 11s
Reviewed-on: dongar/nisse#7
Reviewed-by: Don Garrett <dgarrett@acm.org>
2026-09-06 20:46:46 +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
dongar b05a402c02 Merge pull request 'Add control loop, gating primitives, and transport interface (A4/B1)' (#6) from control-loop-gating into main
CI / checks (push) Successful in 9s
Reviewed-on: dongar/nisse#6
Reviewed-by: Don Garrett <dgarrett@acm.org>
2026-09-06 15:10:24 +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
dongar 78122c407e Merge pull request 'Implement A9 state-change logging in the state store' (#5) from state-change-logging into main
CI / checks (push) Successful in 7s
Reviewed-on: dongar/nisse#5
Reviewed-by: Don Garrett <dgarrett@acm.org>
2026-09-05 21:22:46 +00:00
dongarandClaude Fable 5 7e89a791d2 Add literal-format test showing exact log lines
CI / checks (push) Successful in 8s
CI / checks (pull_request) Successful in 8s
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>
2026-09-05 21:20:41 +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
dongar ba848281ec Merge pull request 'Amend A9 with state-change log conventions' (#4) from logging-conventions-a9 into main
CI / checks (push) Successful in 8s
Reviewed-on: dongar/nisse#4
Reviewed-by: Don Garrett <dgarrett@acm.org>
2026-09-05 21:10:20 +00:00
dongarandClaude Fable 5 41e103c35d Amend A9 with state-change log conventions (issue #3)
CI / checks (push) Successful in 7s
CI / checks (pull_request) Successful in 7s
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>
2026-09-05 19:29:27 +00:00
dongar 99a8c7b3a2 Merge pull request 'Add durable state store and boot-time reconciliation (A2/A3)' (#2) from state-store-durability into main
CI / checks (push) Successful in 8s
Reviewed-on: dongar/nisse#2
Reviewed-by: Don Garrett <dgarrett@acm.org>
2026-09-05 19:25:17 +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
dongar 5962aaee57 Merge pull request 'Scaffold hello-world apps, test frameworks, and Gitea CI' (#1) from scaffold-hello-world-ci into main
CI / checks (push) Successful in 7s
Reviewed-on: dongar/nisse#1
Reviewed-by: Don Garrett <dgarrett@acm.org>
2026-09-05 18:51:16 +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
dongarandClaude Fable 5 b0efbe7b4c Add working conventions to CLAUDE.md
Ask when unclear, script repeated tasks, and require automated tests
with every feature or fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-04 22:26:41 +00:00
dongarandClaude Fable 5 69375c0da1 Record the monorepo decision in the spec
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>
2026-09-04 22:13:45 +00:00
dongarandClaude Fable 5 94aa51135f Merge the three repositories into one monorepo
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>
2026-09-04 22:13:06 +00:00
dongarandClaude Opus 5 defa0a9214 Add README
Names the artifact and points at the specification in docs/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 21:58:01 +00:00
dongarandClaude Opus 5 bafe4cc9e6 Drop reference to nonexistent Architecture.md
The companion rationale document was never written; this spec stands alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 21:57:11 +00:00
dongarandClaude Opus 5 be04c28542 Add Nisse build specification
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>
2026-09-04 21:56:23 +00:00