Commit Graph
347 Commits
Author SHA1 Message Date
dongarandClaude Opus 5 acee8b96f0 Do not believe a completion claim before any work was done
CI / checks (push) Successful in 44s
CI / checks (pull_request) Successful in 41s
CI caught the executor recording an empty milestone as a success: the
model answered with prose on its first turn, and the loop reads "no
tool call" as "the plan is done". It had therefore finished a
milestone having touched nothing, and the milestone would have gone
on to publish with no changes.

A completion claim is now challenged once when no tool has been
called yet — the same shape as the planner's nudge — and a model that
persists escalates instead of reporting success. After the first tool
call the claim is taken at face value, as before.

The live milestone test failed roughly one run in five on this; four
consecutive runs pass now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 22:19:15 +00:00
dongarandClaude Opus 5 d508e9a9b7 Externalise plans to Gitea and accept approvals there (A4, Part H)
CI / checks (push) Successful in 43s
CI / checks (pull_request) Successful in 41s
- approvals.py: PlanBoard opens one issue per project and posts each
  plan revision as a comment carrying its rev tag. The state store
  keeps only a pointer, so gate state can be re-derived from Gitea
  after a state-volume loss — external truth wins
- Gitea comments are a second approval source: revision binding, the
  typed word approve, and first-approval-wins work exactly as over
  transport, with a per-channel allowlist because a Gitea login is
  not a Matrix MXID
- Outcomes are echoed to the other channel, so the two records cannot
  silently disagree about what was approved
- A Gitea outage degrades durability of the record, not the ability
  to approve: planning proceeds and Matrix remains a complete channel
- Live test now asserts the server refuses the agent's own merge and
  its push to the protected branch. That property was previously
  true only because our code did not attempt it; branch protection
  makes it structural, and this test fails if the protection is ever
  relaxed
- Fixed a race in the process-group test: killing a group is
  asynchronous and the grandchild is an orphan, so its exit is not
  synchronised with our return; the test now waits for death

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 22:15:08 +00:00
dongarandClaude Opus 5 affdb9e8aa Add PR comment intake and the review loop (B2) — step 6b
CI / checks (push) Successful in 42s
CI / checks (pull_request) Successful in 43s
- poller.py: CommentPoller polls PR comments rather than taking
  webhooks, keeping the VM outbound-only (B3). On an outage it backs
  off exponentially, caps, and retries indefinitely while logging:
  polling is pure intake, so an outage delays a reaction but never
  fails a task or forces a transition
- prime() marks the existing thread as seen without delivering it, so
  starting a watch does not re-address feedback already handled
- Review loop: a comment in PR_OPEN enters the canonical transcript,
  the executor responds, and the fix is pushed to the same branch with
  a reply on the PR. No intent is recorded for a follow-up push — the
  PR already exists, so a repeated push is harmless, unlike opening a
  second PR
- Comment authors are checked against a separate Gitea operator
  allowlist: a PR comment is an instruction channel into an agent that
  writes code, so it is operator-scoped, and a Gitea login is not a
  Matrix MXID (A4 per-channel allowlists)
- Orchestrator now holds a lock: transport and the poller deliver on
  separate threads, and state transitions must not interleave
- Backoff timings are injectable, so the outage path is tested in
  milliseconds instead of being untestable

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 21:59:50 +00:00
dongar 914bc26221 Merge pull request 'Publish finished milestones as PRs (B2/A3) — step 6a, part 2' (#16) from pr-open into main
CI / checks (push) Successful in 34s
Reviewed-on: dongar/nisse#16
Reviewed-by: Don Garrett <dgarrett@acm.org>
2026-09-07 20:45:57 +00:00
dongar 3b1b84162a Merge pull request 'Add a manual CI trigger (workflow_dispatch)' (#15) from ci-scheduled into main
CI / checks (push) Successful in 35s
Reviewed-on: dongar/nisse#15
Reviewed-by: Don Garrett <dgarrett@acm.org>
2026-09-07 20:44:48 +00:00
dongarandClaude Opus 5 3117565928 Publish finished milestones as PRs (B2/A3) — step 6a, part 2
CI / checks (push) Successful in 41s
CI / checks (pull_request) Successful in 38s
Completes the B1 loop past EXECUTING, and answers the question raised
reviewing #6: PR acceptance is a milestone's final gate, so there is
no separate FINISHED state — completion returns to milestone planning
or to IDLE.

- publish.py: MilestonePublisher commits the work on a task-namespaced
  branch, pushes, and opens a PR carrying the approved plan. Intent is
  recorded before the first externally visible act, so a crash
  anywhere in the sequence leaves a durable record and boot
  reconciliation asks Gitea whether the PR exists rather than opening
  a second one
- Nothing to commit is reported, not raised: the executor may
  legitimately conclude there was nothing to change
- control.py: PR_OPEN state with EXECUTING -> PR_OPEN and
  PR_OPEN -> {MILESTONE_PLANNING, IDLE}; operator commands
  'next milestone' and 'project complete'
- A configured publisher always contributes its own reconciler at
  boot, so an interrupted publish is settled against external truth
  without the caller having to remember to register it
- A publish failure gates to the operator rather than retrying into a
  possible duplicate

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 20:43:20 +00:00
dongarandClaude Opus 5 4d0b24794b Drop the scheduled run, keep the manual trigger
CI / checks (push) Successful in 35s
CI / checks (pull_request) Successful in 37s
Operator does not want nightly or weekly runs. Pushes to main already
cover code changes, so the remaining addition is workflow_dispatch: a
manual re-run for after a credential rotation or a budget top-up,
which also retires the empty-commit trick used to pick up new secrets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 20:40:12 +00:00
dongarandClaude Opus 5 3e664984ac Run CI nightly and on manual dispatch
CI / checks (pull_request) Successful in 35s
CI / checks (push) Successful in 33s
Pushes to main already trigger a full run, so code changes were
covered. What was not: the live tests depend on external state that
drifts with no commit at all — the LiteLLM budget filling up, a model
withdrawn from the proxy, a token revoked or expired. A nightly run
against the default branch surfaces that the morning it happens
instead of mid-task.

workflow_dispatch adds a manual trigger, which also removes the need
for the empty-commit trick used earlier to pick up new secrets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 20:38:06 +00:00
dev-agent 4025ac7875 Merge pull request 'Add executor tool loop (A12/A15) — step 5b' (#12) from executor-loop into main
CI / checks (push) Successful in 34s
2026-09-07 20:35:33 +00:00
dev-agent 1ebd2a9988 Merge pull request 'Add Gitea client and git operations (B2) — step 6a, part 1' (#14) from gitea-client into main
CI / checks (push) Successful in 15s
2026-09-07 20:35:17 +00:00
dev-agent ad45e33517 Merge pull request 'Assert the live Matrix tests run as a testing identity' (#13) from matrix-identity-guard into main
CI / checks (push) Successful in 17s
2026-09-07 20:35:09 +00:00
dongarandClaude Opus 5 8dd5e3eb15 Page to the end when searching for an existing PR
CI / checks (push) Successful in 19s
CI / checks (pull_request) Successful in 15s
find_pull_request read only the newest 50 per state, so once a repo
passed that many pull requests a "not found" would mean "not in the
newest page" — and the A3 reconciler would read that as "the PR was
never opened" and open a duplicate. Exactly the side effect the
intent log exists to prevent.

It now pages until a short page proves the end, and raises rather
than answering "absent" from an incomplete search; the reconciler
turns that into unverifiable and gates to the human.

Found while wiring CI: every CI run opens a PR in the sandbox, so the
threshold would have been crossed in weeks rather than never.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 20:31:31 +00:00
dongarandClaude Opus 5 2071e6ab6d Add Gitea client and git operations (B2) — step 6a, part 1
CI / checks (push) Successful in 16s
CI / checks (pull_request) Successful in 14s
The standalone half of the Gitea layer; orchestrator wiring follows
once the executor loop lands, to avoid colliding with it in
control.py.

- gitea.py: GiteaClient over httpx — open PRs, list and post issue
  comments, close PRs. find_pull_request is the A3 external-truth
  query that makes opening a PR idempotent across a crash, and it
  checks closed PRs too, since a crash after opening must not be
  answered with "nothing is open, so open another"
- pull_request_reconciler: an unreachable Gitea returns None rather
  than a guess, so the agent gates to the human instead of risking a
  duplicate side effect
- git.py: branch, commit, push for the workspace. The token is passed
  per-invocation rather than stored as a remote, so it never lands in
  .git/config, and anything raised is scrubbed of it
- commit_all returns None when there is nothing to commit: an empty
  milestone is a fact to report, not an error
- Live test does the real round trip against the sandbox repo and
  cleans up after itself

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 20:27:34 +00:00
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