Commit Graph
321 Commits
Author SHA1 Message Date
dongar a891932739 Merge pull request 'Spec: prefer one local model for all roles; record host sharing, park vision' (#54) from spec-shared-inference-host into main
CI / checks (push) Successful in 1m16s
Reviewed-on: dongar/nisse#54
Reviewed-by: Don Garrett <dgarrett@acm.org>
2026-09-28 01:47:08 +00:00
dongarandClaude Opus 5.5 2ab86bf0da Spec: OpenSCAD work needs vision, in nisse-dev or its own agent
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m18s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 01:32:20 +00:00
dongarandClaude Opus 5.5 75d96b4197 Spec: restore the blank line before the section rule
CI / checks (push) Skipped
CI / checks (pull_request) Failing after 1m14s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 01:30:36 +00:00
dongarandClaude Opus 5.5 9239a56bd1 Spec: park vision as a need of future agents, not current ones
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m20s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 01:30:18 +00:00
dongarandClaude Opus 5.5 50a2a75342 Spec: prefer one local model; add vision as an open decision
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m19s
The host-sharing item now records the operator's preference: one local
model for every local role, with agents tagging in and out of the host
as the fallback if no single model fits. A new item records vision as a
requirement (OpenSCAD renders, later video files) and what the text-only
agent needs before any vision work.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 01:29:29 +00:00
dongarandClaude Opus 5.5 e5c18b3bf4 Spec: record sharing the local inference host as an open decision
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m15s
nisse-dev and nisse-ops both send local roles to the one 24GB host,
where llama-swap keeps one model resident. Different models per agent
would reload weights on every switch. Per-key rate limits don't help.
Lists the options, simplest first, to decide after the executor
benchmark and the ops evaluation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 01:25:38 +00:00
dev-agent 8467b8747f Merge pull request 'Add four harder benchmark tasks' (#53) from harder-bench-tasks into main
CI / checks (push) Successful in 1m17s
2026-09-28 00:31:17 +00:00
dongarandClaude Opus 5.5 277bff6fcd trace-bug: say how much precision the rates need
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m14s
Calibration showed Haiku fixing the right constant but keeping only four
decimal places, which passes the visible test and still loses the JPY
rate's precision. An explicit plan should say so.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 22:43:39 +00:00
dongarandClaude Opus 5.5 25fd57351d Add four harder benchmark tasks
All seven newer local models passed the original four tasks 20 of 20,
so the benchmark no longer told them apart. The new tasks keep plans
explicit, as the planner is told to write them, and make the work
itself harder:

- trace-bug: a wrong multi-currency total whose cause is a shared
  rate helper; discounts and tax go through it too, so a fix at the
  call site fails the check.
- refactor-rename: rename load_config across a ten-module package,
  leaving load_config_defaults and reload_config alone.
- spec-parser: implement parse_duration from an eight-rule SPEC.md
  given three examples, and write tests for every rule.
- long-milestone: eight dependent steps adding due dates to a task
  tracker, from the model through storage versions to the CLI.

Self-tests prove that a call-site fix, a blind text replace and a
partial rename all fail their checks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 22:43:39 +00:00
dongar f2c48d8b16 Merge pull request 'Keep runs with different tuning apart in the report, and raise max_turns to 40' (#52) from report-keeps-tuning-apart into main
CI / checks (push) Successful in 1m13s
Reviewed-on: dongar/nisse#52
Reviewed-by: Don Garrett <dgarrett@acm.org>
2026-09-27 22:43:26 +00:00
dongarandClaude Opus 5.5 f881a18ca4 Keep runs with different tuning apart in the report, and raise max_turns to 40
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m11s
The report grouped runs by model, serving note and task version, so
runs made at different tuning were silently averaged together. Tuning
is now part of the key, and a tuning column shows how a row's settings
differ from today's defaults; it appears only when some row needs it.

tuning.executor.max_turns goes from 24 to 40. On the harder benchmark
tasks Haiku, the calibration ceiling, hit 24 on both refactor-rename
runs and both long-milestone runs, one of which had finished the work.
The cap guards against a model trying forever; it isn't a budget.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 22:25:14 +00:00
dev-agent a77c84c128 Merge pull request 'Render one assistant message per turn, and make the request timeout a setting' (#51) from fix-transcript-and-timeout into main
CI / checks (push) Successful in 1m11s
2026-09-27 22:24:50 +00:00
dongarandClaude Opus 5.5 d49435d4e5 Render one assistant message per turn, and make the request timeout a setting
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m16s
Two setup failures found while calibrating the new benchmark tasks,
both of which would also fail real milestones.

Devstral's chat template (Mistral) rejected any turn in which the model
wrote some text before a tool call: the OpenAI renderer made one message
per event, so the turn became a text message and a tool-call message in
a row, and llama.cpp answered 500. The renderer now folds consecutive
user or assistant messages into one, which is the OpenAI format for a
turn anyway. Checked against the live server: the captured failing
request no longer occurs, over six real runs.

LiteLLMClient gave up on any request after a fixed 120 s. A dense 27B
model writing a whole module in one turn takes longer, so the executor
escalated work that was going well. llm.request_timeout_s now sets it
for both clients, default 600.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 22:15:30 +00:00
dev-agent f41cbb00cf Merge pull request 'Record rate-limit waits in benchmark runs, and leave them out of time' (#50) from record-rate-limit-retries into main
CI / checks (push) Failing after 1m7s
2026-09-27 21:23:56 +00:00
dongarandClaude Opus 5.5 4e45dfe06a Record rate-limit waits in benchmark runs, and leave them out of time
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m17s
The client waits RATE_LIMIT_RETRY_S and retries once on a 429, silently.
A benchmark run that waited looked slower than the model is: on
2026-09-27, fast models under the unit-test key's 200k tokens/minute
limit had 20-45 s runs that were probably waits.

Both clients now report last_rate_limit_wait_s after every call, even
one that then fails. The benchmark sums it per run into
rate_limit_wait_s, the report's time column leaves it out, and a new
429 column counts the runs that waited. Results stored before this
load unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 20:57:15 +00:00
dev-agent a716d5ec8d Merge pull request 'Benchmark seven new RTX 4090 models, and local-fast on the 4090' (#49) from bench-4090 into main
CI / checks (push) Successful in 1m8s
2026-09-27 20:31:46 +00:00
dev-agent 1cc8df8dab Merge pull request 'Point the example config at Opus 5.5, with Haiku as consultant' (#48) from example-opus-5-5 into main
CI / checks (push) Successful in 1m7s
2026-09-27 20:31:34 +00:00
dongarandClaude Opus 5.5 82f171c2f5 Benchmark seven new RTX 4090 models, and local-fast on the 4090
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m16s
Five runs per task for qwen3.8-27b, qwen3.6-35b-a3b, gemma-4-31b,
gemma-4-26b-a4b, gemma-4-12b, mimo-v2.6-9b and granite-4.2-8b, all
served by llama.cpp b11206 through llama-swap. Every one passed 20 of
20. local-fast (vLLM 0.30.0) passed 3 of 20.

Runs were made under a 350 W power cap, recorded in each note, after
the uncapped card throttled for heat. granite-4.2-8b also has one
uncapped set, kept apart by its note.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 18:43:55 +00:00
dongarandClaude Opus 5.5 3cb70c0656 Point the example config at Opus 5.5, with Haiku as consultant
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m13s
Planner and delegate use claude-opus-5-5 for the high-end judgement
those roles need. The consultant answers single questions with no tools,
so it uses claude-haiku-4-5. Sonnet has no role.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 20:22:49 +00:00
dongar 8a8d8dc27f Merge pull request 'Add an executor benchmark that keeps its results' (#47) from benchmark into main
CI / checks (push) Successful in 1m15s
Reviewed-on: dongar/nisse#47
Reviewed-by: Don Garrett <dgarrett@acm.org>
2026-09-25 02:23:57 +00:00
dongarandClaude Opus 5.5 0cccfe1e94 Add the local baseline: qwen3-8b-awq and qwen3-14b, 5 runs per task
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m14s
The 4070's two local models with default tuning, as the baseline the
4090's candidates will be compared with. Every one of the 22 failures
is an escalation from failure_limit: 2; there were no prose turns,
invented tools, context trims, or finished-but-wrong results.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 01:25:13 +00:00
dongarandClaude Opus 5.5 50b46821d7 Add an executor benchmark that keeps its results
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m11s
Choosing a local executor model, and tuning its knobs, needs measured
success rates across repeated runs, comparable with every model
measured before.

- Four canned milestones under nisse_dev/bench/tasks: write a function,
  fix a bug a failing test exposes, thread an option through several
  files, and find one failure in output far past max_output_chars. Each
  is an approved plan over a seed project, judged by a hidden check the
  model never sees; a reference solution proves each is solvable and
  that its check tells the difference
- Runs use the agent's own client wiring, now shared as
  make_llm_clients, with delegation and ask_expert off: an escalation is
  a failure of the model under test
- Each run is stored as one JSON line in bench/results/, committed, with
  the model's backing name and declared facts, a note on how it was
  served, the task's content hash, the agent's commit and tuning, the
  outcome, turns, misbehaviour counts, context trims, tokens, time and
  LiteLLM's cost; a fallback answer can never count as a success
- The report rebuilds the comparison from every stored run, with Wilson
  95% intervals, marking runs against older task versions
- scripts/benchmark.py runs and reports; scripts/evaluate_model.sh takes
  a new model through the context probe, the capability matrix and the
  benchmark in one command
- Clients keep LiteLLM's per-call cost, and BudgetedClient counts the
  requests it had to trim
- The tuning guide gains "Benchmarking models"

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 00:35:24 +00:00
dongar b2bbf70b7b Merge pull request 'Enforce the tool-call contract, asking servers only where it works' (#45) from enforce-tool-calls into main
CI / checks (push) Successful in 53s
Reviewed-on: dongar/nisse#45
Reviewed-by: Don Garrett <dgarrett@acm.org>
2026-09-25 00:05:46 +00:00
dongarandClaude Opus 5.5 b934c46714 Enforce the tool-call contract, asking servers only where it works
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 56s
The executor ended a milestone on any reply without a tool call, and
nothing held a model to the offered tools. Measured on the 4070:
tool_choice "required" is enforced by vLLM (local-fast) and Anthropic,
but qwen3-14b (local-coder) under it cannot stop and writes until the
output ceiling, a known model bug. So enforcement is asked for per
model, and the loop holds the contract itself everywhere.

- The catalog reads supports_tool_choice from LiteLLM's model_info;
  BudgetedClient sends tool_choice only to models declared true, as
  "required" on the normalized route and {"type": "any"} on the
  Anthropic passthrough, and only when tools are offered
- The executor gains an explicit finish tool, the only way to end a
  milestone. Finishing with nothing changed is refused once; a prose
  reply gets one nudge, then escalates; a call to a tool not offered
  counts toward failure_limit, like a failed command; calls after
  finish are recorded as not run
- Clients record the model that actually answered. ModelFacts.answered
  recognises a proxy fallback, which once credited Claude's answers to
  a local model that could not call tools at all
- The context probe reports a fallback instead of trusting it; rerun,
  both local models now refuse over-long prompts, as they should
- The capability matrix checks every answer came from the model asked,
  that models call only offered tools, and that a declared
  supports_tool_choice is true
- Spec A15 and the tuning guide say how enforcement is requested,
  declared and backed up

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 22:54:49 +00:00
dongar 389b71a5d1 Merge pull request 'Fit every model request to its context window' (#44) from context-budget into main
CI / checks (push) Successful in 57s
Reviewed-on: dongar/nisse#44
Reviewed-by: Don Garrett <dgarrett@acm.org>
2026-09-24 13:41:15 +00:00
dongarandClaude Opus 5.5 494715efa9 Fit every model request to its context window
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 55s
Every turn re-sent the whole transcript with nothing checking it against
the model's window. On a local model that fails silently: Ollama drops
the start of an over-long prompt, which is where the system prompt and
the plan are. Measured on the Jetson's llama3.2, cut at 4096 tokens with
no error.

- The model catalog reads max_input_tokens and max_output_tokens from
  LiteLLM's model_info, and startup refuses a role on a model with no
  declared window
- BudgetedClient wraps both routes. It fits the canonical events before
  rendering: older tool results cut to head and tail, then the oldest
  turns dropped with a note, keeping calls with their results; a request
  that still cannot fit fails with ContextOverflow and is never sent.
  The output ceiling is the smaller of config and the model's limit
- Tokens are estimated from characters, pessimistically, and the
  estimate is corrected from reported prompt sizes, only ever upward
- The executor takes its scope from the log: the latest plan, which is
  the approved milestone plan, goes in its system prompt, and it sees
  only events after it
- Tool output keeps its head and, mostly, its tail, so a test runner's
  failure summary survives truncation
- tuning.context.keep_recent_turns and safety_margin are new knobs
- scripts/probe_context.py checks a declared window against what the
  server does; it confirmed llama3.2 cuts at the declared 4096
- The tuning guide covers the budget, declaring a local model's limits,
  and the probe

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 00:26:30 +00:00
dongar 31851b7396 Merge pull request 'Move configuration to YAML, and make the tuning knobs configurable' (#43) from yaml-config into main
CI / checks (push) Failing after 51s
Reviewed-on: dongar/nisse#43
Reviewed-by: Don Garrett <dgarrett@acm.org>
2026-09-23 23:49:44 +00:00
dongarandClaude Opus 5.5 e1034a2cb7 Show which config key refers to each example secret
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 49s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 23:45:02 +00:00
dongarandClaude Opus 5.5 58b439088f Pass the capability matrix's models as a pytest option
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 51s
NISSE_CAPABILITY_MODELS was neither a secret nor configuration: which
models to measure is a question for one run, so it is now an argument,
--capability-models=a,b, declared in tests/conftest.py where pytest
reads options at startup. With no models the matrix skips as before, so
CI is unchanged. The last non-secret NISSE_* variable other than
NISSE_CONFIG and NISSE_ENV is gone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 23:32:44 +00:00
dongarandClaude Opus 5.5 15d69799bf Make the config file the only home for settings
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 51s
With no production instance yet, there is no reason to carry two ways
of configuring the agent. Every environment variable that duplicated a
config key is gone; the environment now supplies only the secrets the
file references by ${…}, plus NISSE_CONFIG and NISSE_ENV to find the
files.

- AgentConfig loads from a file (load, from_file) or a parsed tree
  (from_tree, for tests); a missing file is an error naming the example
- The from_env constructors on LiteLLMClient, AnthropicPassthroughClient,
  GiteaClient, MatrixConfig and RoleMap are removed, with the env
  reading for max output tokens; clients take their settings from config
- The live integration tests share a committed agent config,
  tests/integration/nisse-test.yaml, aimed at test resources, and one
  conftest that loads it and builds clients. NISSE_LIVE_PLANNER_MODEL
  becomes its planner role; NISSE_CAPABILITY_MODELS stays as a per-run
  switch
- A missing secret now fails the live tests instead of skipping them
- CI passes only the three tokens
- .env.example is secrets only; spec A5 says config comes from the file

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 23:25:58 +00:00
dongarandClaude Opus 5.5 df449c7e34 Move configuration to a YAML file, and make the tuning knobs configurable
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 51s
The thresholds Part H says to tune on the real inference hardware were
hardcoded, so every adjustment would have been a PR. They now live in a
`tuning:` section of a YAML config file, which also replaces flat
environment variables as the home for structure (#32).

- The file (NISSE_CONFIG, default ./nisse-dev.yaml) carries structure
  and holds no secrets: credentials are ${VAR} references resolved from
  the environment, and a reference to an unset variable is an error
- The environment still wins: every existing NISSE_* variable overrides
  its key, and the file is optional, so .env and CI work unchanged
- Validation stays fail-fast and reports every problem at once; unknown
  keys are errors, so a misspelt knob fails at startup instead of
  silently doing nothing
- The config file joins the state store and .env in the check that
  refuses to start when they sit inside the workspace clone
- Tuning covers the executor's turn and failure limits, the consult
  budget, the delegation caps, the command timeout and the tool output
  limit, which Workspace now takes as a setting; the defaults are the
  values the agent already ran with
- llm.max_output_tokens reaches both LLM clients from the config

docs/Tuning-Guide.md explains the knobs for someone learning local
model tuning: how weights, quantization and the KV cache spend VRAM,
what each knob does and what goes wrong at either extreme, and an order
to tune in. nisse-dev.example.yaml documents every key and is checked
by a test so it cannot drift.

Closes #32

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 23:14:07 +00:00
dongar 4a9354ae3b Merge pull request 'Add the operator kill switch, stop (A4)' (#41) from stop-command into main
CI / checks (push) Successful in 50s
Reviewed-on: dongar/nisse#41
Reviewed-by: Don Garrett <dgarrett@acm.org>
2026-09-23 22:50:46 +00:00
dongarandClaude Opus 5.5 abb7fa0216 Add the operator kill switch, stop (A4)
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 49s
`stop` could not have worked by adding the command alone. Planning
turns, milestones and review responses ran synchronously on the thread
that delivered the message, so the Matrix sync loop read nothing until
the work finished. The state store was also opened on the main thread
and used from the transport and poller threads, which SQLite refuses
with its default same-thread check; the in-memory test transport never
showed it.

A single worker thread now runs every handler and owns the store:

- The worker opens the store, so the same-thread check stays on and
  fails loudly if anything touches the store from elsewhere
- Matrix messages are handed to the worker; pollers hand over and wait,
  so their cursor never passes an unhandled comment
- A message that arrives during long work gets an immediate "busy; say
  'stop' to halt" reply and is recorded, not applied later in a state
  the operator was not answering
- Tests keep running inline through InlineWorker, which defers a job
  submitted from inside another as the real worker would queue it

`stop` sets a shared Cancel flag straight from the transport's thread.
The executor and delegation loops check it before each model call and
between tool calls; the planner checks it before each turn. Calls left
unrun get a result saying so, so the transcript still renders. A
stopped delegation is discarded like a failed one. The orchestrator
moves to AWAIT_INPUT and asks why the work was stopped. The reply is
recorded for the model, and a stopped planning turn resumes with it as
the answer. `stop` with nothing running says so and changes nothing; at
a gate it is no longer taken as rejection feedback.

Closes #40

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 22:47:29 +00:00
dongar d2fe3d5642 Merge pull request 'Gate ops writes on approved step sequences' (#39) from ops-gating into main
CI / checks (push) Successful in 1m8s
Reviewed-on: dongar/nisse#39
Reviewed-by: Don Garrett <dgarrett@acm.org>
2026-09-23 22:26:12 +00:00
dongarandClaude Opus 5.5 019fa24517 Wait a grace period before switching away from a waiting item
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 57s
Switching as soon as the active item posts a plan or question means the
operator sees the request and the switch notice seconds apart. A prompt
approval then waits until the other item waits or closes, which can be
a whole milestone. A configurable grace period (default 10 minutes)
lets a watching operator answer before the agent moves on, and still
switches when they are away.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 22:16:40 +00:00
dongarandClaude Opus 5.5 a91252cb77 Let the agent also switch on its own when the active item closes
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 57s
0.2 previously allowed an autonomous switch only while the active item
waits on the operator. Ready work should not sit behind an idle agent
either, so closing an item (completed or abandoned) is now the second
case. This also defines "ready": the agent can act on the item without
the operator, because it has not started or because intake arrived
while it was parked that answers what it was waiting for. A9's
work_item.switch allows a closed or null `from`, and B2 no longer says
only the operator selects the active project.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 22:09:39 +00:00
dongarandClaude Opus 5.5 5b163f0c2e Give calls outside any work item an agent-level scope
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 50s
Letting the model choose which ready item to switch to adds a model
call that belongs to no work item. Duty runs before a finding already
had the same gap. A10 now defines an agent-level scope for these calls,
kept apart from work item transcripts in both directions. The switch
call sees only code-rendered summaries of the ready items, runs as the
executor, and falls back to the oldest item if it fails. A9 gains a
work_item.switch event, and A18 accepts that an injected alert can at
worst reorder work.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 21:57:05 +00:00
dongarandClaude Opus 5.5 854bddc680 Let the model choose which ready work item to switch to
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 52s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 21:54:18 +00:00
dongarandClaude Opus 5.5 b8df5d2066 Gate ops writes on approved step sequences
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 52s
C2's per-tool allow/deny/confirm taxonomy would have to be written in
advance for a tool catalog that will grow large, and every mistake in it
is friction or a hole. An ops write plan is now a list of literal steps
(tool, target, arguments, optional precondition and expected outcome,
retry allowance), and one approval covers exactly that sequence:

- Code renders the approval message from the steps, flagging
  irreversible steps, protected targets, and steps that take down the
  agent's own host
- A matcher checks every call: read-only is free, an elevated call must
  exactly match the next approved step (arguments normalized against the
  tool's schema), and anything else halts to AWAIT_INPUT
- Approvals expire if the plan does not start in time
- Classification shrinks to a versioned policy file: instance privilege,
  protected targets, and each elevated tool's target argument, all
  failing safe; MCP annotations may only tighten
- Bounded matching and standing approvals are deferred, with reasons

Supporting changes:

- A3: an approved sequence resumes after the agent's own restart only on
  a verified expected outcome for the step in flight
- A4: an operator kill switch, `stop`, for every agent
- 0.2: the agent may switch items on its own while the active one waits
  on the operator; parking unmounts elevated instances; a Matrix reply
  to a parked item binds to that item
- A17: step preconditions replace the staleness bounds, the log-checked
  verification precondition and the boot-time re-fetch
- A18 (new): prompt injection, designed around limiting what a
  compromised model can do rather than detecting injection
- Part H records the decision and parks the deferred items

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 21:50:15 +00:00
dongar 182216296d Merge pull request 'Define the work model: duties, work items, scopes' (#38) from work-model into main
CI / checks (push) Successful in 1m1s
Reviewed-on: dongar/nisse#38
Reviewed-by: Don Garrett <dgarrett@acm.org>
2026-09-23 20:31:29 +00:00
dongarandClaude Opus 5.5 2c9a082e5b Define the work model: duties, work items, scopes
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 53s
"Project" was carrying several things at once: the repo, the effort
through arch plan and milestones, and the transcript's scope. The
Sysadmin Agent needs the same distinctions in a different shape, with
standing duties that produce many efforts over one scope. New section
0.2 names them:

- Duty: a standing responsibility that produces work items, and owns
  deduplication of its findings
- Work item: the unit of state, identified by a locally minted id
  (A9's task_id), optionally linked to an external record
- Scope: what a work item operates on, shared across work items
- Park-and-switch: one active work item, switching only at safe
  points, nothing lost while parked; the schema does not assume one

0.1 records this as a bounded exception to extract-after-it-works:
identity is designed ahead, behaviour is not. A2, A4, A9, A10, B1/B2
and C4 now speak in these terms; the transcript is scoped per work
item, and a milestone no longer mints a task_id. Part H parks
concurrency with its costs, and leaves open when a work item must
have an external record.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 20:26:24 +00:00
dongar 889615e3e2 Merge pull request 'Record the MCP server deployment decisions in the spec' (#36) from mcp-deployment into main
CI / checks (push) Successful in 54s
Reviewed-on: dongar/nisse#36
Reviewed-by: Don Garrett <dgarrett@acm.org>
2026-09-23 02:03:59 +00:00
dongarandClaude Opus 5.5 3dbfbc8cf0 Name the site domains the identifiers are stems of
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 54s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 02:00:43 +00:00
dongarandClaude Opus 5.5 cdc3881deb Fold the MCP deployment amendment into the spec
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m4s
Part F is restructured around rules that now apply to every agent:

- F1: every agent gets its own VM, for inter-agent isolation and
  per-agent credential attribution
- F2: a declared, per-instance default-deny egress allowlist that
  covers MCP servers' reach, with the tailnet's 100.64.0.0/10 called
  out
- F3: MCP servers per agent VM and per (service, site, privilege),
  plus the rejected routed-meta-tools alternative
- F4: the existing image pipeline, with third-party MCP images
  digest-pinned and excluded from Watchtower

Consequences land where they belong: site identifiers in Part 0, a
machine account per MCP server instance in A7, B3 and C5 recast as
instances of F1/F2, plan approval required for every write in C2, MCP
as the primary infrastructure path in C3, the network-gear browser
item narrowed to fallback access, and stateful-server relocation
parked in Part H.

The standalone amendment is removed now that its content lives here,
as with the Tiered Inference amendment; it remains in the previous
commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 01:56:30 +00:00
dongarandClaude Opus 5.5 31bef59151 Add the MCP deployment amendment
Records a design session on MCP server deployment as a standalone
amendment, ahead of folding it into the spec:

- Every agent gets its own VM, justified by inter-agent isolation and
  per-agent credential attribution; Docker-in-Docker stays as an extra
  Dev Agent reason rather than the rule's foundation
- Egress is a declared, per-agent-instance default-deny allowlist,
  covering what MCP servers reach on the agent's behalf, with the
  tailnet's 100.64.0.0/10 called out as outside RFC1918
- MCP servers run per agent VM, one instance per (service, site,
  privilege), with site in the server identity rather than a tool
  argument
- Third-party MCP images are digest-pinned and excluded from Watchtower
- Routed meta-tools rejected; stateful-server relocation parked

Two consequences settled with the operator while drafting: each MCP
server instance gets its own Vaultwarden machine account (A7), and any
write, reversible or not, requires an approved plan (C2).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 01:55:31 +00:00
dongar b0d7adb70d Merge pull request 'Discover a model's vendor from LiteLLM instead of its name' (#35) from alias-discovery into main
CI / checks (push) Successful in 55s
Reviewed-on: dongar/nisse#35
Reviewed-by: Don Garrett <dgarrett@acm.org>
2026-09-23 01:49:36 +00:00
dongarandClaude Opus 5 e38adf40ca Discover a model's vendor from LiteLLM instead of its name
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 47s
Route selection sniffed model names: anything starting "claude-" or
"anthropic/" took the vendor-native passthrough, everything else took the
normalized route. That works only while an alias names its vendor. A
semantic role alias like "remote-architecture" is meant to point at
Anthropic today and somewhere else tomorrow, and the name says nothing
either way -- so it would have taken the normalized route regardless of
what was behind it, silently losing the fidelity A13 wants for delegations.

LiteLLM already knows the answer. /model/info reports each alias's backing
model, provider, and declared tool support, so the agent asks at startup and
routes on the provider it discovers. That keeps vendor knowledge where A5
already put it -- in the proxy -- rather than duplicating it as name patterns
here.

There is deliberately no name-based fallback: PASSTHROUGH_PREFIXES and
has_native_adapter are gone, and select_client now requires a catalog. A
second heuristic could only ever disagree with the proxy. An unknown model
raises instead of guessing a route.

Misconfiguration is therefore loud and early. build_agent validates every
role against the catalog before it clones anything, naming all the bad ones
at once. Discovery is per key -- /model/info returns only what the presented
token may use -- so the same check catches a role pointed at a real model
this key cannot reach. An unreachable proxy is reported separately, since an
outage and a typo call for different responses from the operator.

Two things now get recorded rather than inferred: the resolved model is
pinned alongside the role mapping at task start, for the reason A11 already
pins roles, and DelegationStarted carries backing_model so A16 eval history
is not averaged across whatever an alias happened to point at.

A model LiteLLM declares tool-less warns at startup rather than refusing;
A15 expects weak local models to be accommodated, and they are observed
driving tool loops anyway.

Verified live: all four aliases the production key can see route correctly
through discovery, and each chosen route works.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 01:47:58 +00:00
dongar 231b406393 Merge pull request 'Choose the LLM route by model, and add a capability matrix' (#34) from model-routing into main
CI / checks (push) Successful in 58s
Reviewed-on: dongar/nisse#34
Reviewed-by: Don Garrett <dgarrett@acm.org>
2026-09-23 01:47:34 +00:00
dongarandClaude Opus 5 8cfa116aa1 Preserve delegation work through any upstream outage, not just budget
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 52s
An exhausted budget was a special case of something more general: the
work is not at fault. A network blip, a timeout, a 5xx from the proxy,
or a rate limit that did not clear all leave a delegation branch that
may be perfectly good, and discarding it would throw away work for a
reason that has nothing to do with the work.

UpstreamUnavailable now names that family, with BudgetExhausted a
member of it — distinguishable where the difference matters, since a
budget will not clear within the hour while a network blip will. A
delegation stopped by any of them is "interrupted": branch kept,
intent left pending, and A3's resume rule picks it up.

The benefit of the doubt stops at the server's own door. A 4xx that is
not a rate limit means the request itself was wrong, and preserving
branches on the strength of one would hide a real bug — so those still
fail and clean up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 23:57:05 +00:00
dongarandClaude Opus 5 8cfb45aed3 Enforce the token cap, and treat a spent budget as an interruption
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 51s
Exceeding a LiteLLM spend limit takes external models away for the
rest of the day, which makes three existing gaps consequential rather
than untidy.

- A13 calls the delegation caps hard on "turns, tokens, and wall
  clock", and the module docstring repeated it, but the token cap was
  decoration: nothing counted, and result.tokens was always zero. It
  is now metered from the usage the provider reports, so a confused
  delegate cannot spend a day's allowance. A5 keeps the *money* cap on
  the LiteLLM key, which is a different thing; this is a scope cap
- The normalized route sent no ceiling at all, so truncation detection
  there had nothing to detect against. Both routes now send one
- A budget refusal is recognised distinctly. It is not a rate limit:
  retrying cannot help, so the planner no longer burns a second call
  to fail identically

And a delegation cut off by an exhausted budget is now "interrupted"
rather than "failed", which preserves its branch and leaves its intent
pending. The work on that branch may be perfectly good; discarding it
because the budget ran out would throw away work for a reason that has
nothing to do with the work. A3's resume rule already knows how to
pick such a delegation back up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 23:54:57 +00:00