| description | Shared instructions for AI coding assistants in Nullius. |
|---|
Shared instructions for AI coding assistants working in Nullius. Keep this file specific, factual, and cross-tool. Prefer nearby code over assumptions. Describe current behavior and constraints; remove obsolete code and historical commentary unless the user explicitly asks to keep it.
README.md carries the design and rationale. This file is the map.
Nullius is an application: bring anything, run it, prove it. A
benchmark, an agent or any capability comes into its catalogues by one prompt
(the import skills), is paired by the ports it has proven (the Nullius
standard, docs/standard.md), runs here or on many machines, and every score
opens into the evidence behind it - the agent's failure or the benchmark's.
It reads Harbor's output too, from any agent, and ATIF and OpenTelemetry
exports. It runs a benchmark only on an explicit Run from the builder's Review step - a
standard recipe (a benchmark's own scripts included, as its Original setup)
or a Harbor recipe - on this machine or a host (app/nullius/execution.py). A Harbor recipe pins its Harbor - a
release from PyPI, or a repository (a private fork included) at a commit,
built on this machine and its wheels sent to a host - and runs it through
uvx, outside this application's interpreter (app/nullius/harbor/source.py);
a recipe without a pin runs whatever harbor is on the target's PATH.
The job records which one ran.
Application code lives under app/; topic documentation lives under docs/.
New code goes under app/, never loose at the root. README.md
and AGENTS.md sit at the top because that is where people look for them;
there is no CLAUDE.md, assistants read this file. Root pyproject.toml
builds the nullius-eval distribution: nullius is the backend, _nullius_app is the UI
and its resources. The source layout stays under app/.
app/app.py— the entry point: the application object fromapp/web/core.py, the capability modules imported so each registers its routes,main().app/web/— the Flask side, one module per capability:core(the application object),shared(the open workspace and its jobs, path resolution, the catalogue cache, the query helpers, the form-token guard),filters(template filters, page context), thenruns,traces,compare,builder,catalogue,agents,settings,workspaces,execute,judge,feasibility,reconcile,judge_tasks,onboarding(agentsis the Agents and Capabilities tabs and conformance Kits catalogue). Cross-module names are reached as attributes (shared.STATE,traces._trial_evidence): patch one, patched for all.app/nullius/— its backend, one package per purpose:runs/(reading one Harbor job tree:parse,classify,live,artifacts,feasibility,launches,verifier- a verifier's report read by its benchmark's report block - and thecli),traces/(trace,trajectory,conversation,charts,behavior,otel),compare/(comparisonand itscli),catalogue/(catalog, which loads one folder per benchmark undercatalogue/benchmarks/, named by its key: itsbenchmark.jsonwith its datasets nested in it, itsreportblock and itsstandard.jsonwhen it runs on the standard;verifierinruns/reads a verifier's report by that block;harbor_entry, a Harbor benchmark added from the Benchmarks page's form;citation, an entry's BibTeX read into the paper its page shows;storage, whole-folder user overlays and restoring bundled entries;sources, where each dataset's data comes from for each way it runs),recipes/(launch, the builder's vocabulary),providers(where a run's model calls go, and the builder's steps),harbor/(source: the Harbor a recipe pins - built, kept, scanned for its agents;feed: the live feed any Harbor writes, from its own trial hooks),importers/(atifATIF,otelOpenTelemetry, and thecli),judge/(questions,state,client,store,batch,reconcile,lanes),standard/(the Nullius standard,docs/standard.md:ports,names(capability vocabulary and legacy aliases),definitions(with an agent's or capability's versions),match,jsonl,acp,cancellation(interruptible protocol waits),relay,install,run,container(the docker backend),forward(a licence server's one door from it, or a benchmark's MCP server's),mcp_machineandmcp_bridge(a benchmark's MCP server run on the machine, in its driver's environment, and the agent's bridge to it),hostreq(what a benchmark's options need of the machine its driver runs on: the check and the install) andegress(an own image's check's proxy to public addresses),job,conform,fidelity(ACP against the agent's own CLI),prove(a benchmark's controls and proofs),shardandmerge(one run over many machines),recipe(the builder's standard recipe),howto(the How to add steps) and thecli; itspayloadsnapshots the running backend and effective catalogue for a remote host; conformance kits understandard/kits/, each a benchmark: adriverand, for the tools kit, an MCP servermcp_kit) - the definitions it reads are in the catalogue: a benchmark'sstandard.jsonanddriverin its folder (τ³-bench's also the MCP server of its domains' tools,mcp_server: the upstream's own, run on the machine),catalogue/agents/<key>/(the referenceagent; JiuwenSwarm'sagentand mini-swe-agent'srun, the wrappers that start them) andcatalogue/capabilities/<key>/(the keyword-memorycapability);workspace(andworkspace_cli, its shell command),cliand__main__(the unifiednulliusandpython -m nulliuscommands),paths(user config, data and cache locations),migrate(explicit copying of legacy checkout storage),settings,hosts,execution,progress(what a run is doing, one line per stage),prerequisites(what a recipe needs of each machine it runs on, asked before Run and again as it starts),onboarding(the Getting started checklist: what this machine says is done, and where each step is done),gitrefs(a repository's branches and tags, bygit ls-remote),secrets(a benchmark's licences, kept per benchmark outside the repository),sandbox(Settings' docker: its Docker Hub login and the images runs keep) andredactat the top.app/templates/,app/static/— the interface, grouped by the same capability names (runs/,traces/,builder/, …;base.html,style.cssand the fonts at the top), including the command builder's JavaScript, which owns the generated recipes.app/tests/,app/ui_tests/— the backend suite and the route suite. Beside the backend tests sit scripts run by hand:app/tests/field_contract.py, which refreshes thefield_contract.jsonthe suite checks, andapp/tests/catalogue_docs.py, which writes the Supported sections underdocs/from explicit bundled catalogue roots, never user overlays: each section's README block and a page per benchmark, agent and capability (test_catalogue_docs.pyfails when they are stale). The fixtures underapp/tests/fixtures/are small and hand-made; no recorded run is kept.app/site/— the static website: MkDocs configuration and source staging. It publishesdocs/, including the metric field guide.diagrams.pydraws the figures underdocs/diagrams/(--writeafter a change;test_site.pyfails when they are stale).readme_media.pydraws the README’s animated SVGs underdocs/assets/nullius/readme/(--writeto refresh,--checkfor drift). Build dependencies are isolated inapp/site/requirements.txt; build, preview and publishing instructions are inapp/site/README.md.docs/— the introduction, quickstart, concepts and results guides;settings/,benchmarks/,agents/andcapabilities/hold focused guides; plus topic pages (builder.md,judgement.md,settings/README.md,cli/, one page per topic of the CLI) and the example of a new benchmark. Validations, captures and rendered reports are not kept here; tests are the proof.
Benchmark integrations expose one current entry with supported behavior as options. Internal driver changes do not create selectable versions. Reserve benchmark versions for explicit upstream releases or requested forks; retain only applicable proofs.
Flask is the application's only direct runtime dependency. Nullius itself
is installed with uv tool install nullius-eval from PyPI, selecting the latest
version unless one is explicitly pinned. Downloaded wheels and source builds
remain available; the executable is nullius and storage paths keep that name. The
Harbor a recipe runs is its pin's, run through uvx (or the target's own, for
a recipe without a pin), and the viewer never imports it.
nullius paths lists persistent locations. On Linux, settings and benchmark
licences live under ~/.config/nullius/, user catalogue entries, workspaces,
runs and standard installs under ~/.local/share/nullius/, and caches under
~/.cache/nullius/. XDG variables move those roots; NULLIUS_HOME selects a
separate profile with config/, data/ and cache/. Existing storage overrides
still apply. Settings saves owner-only JSON; process environment values win.
No personal data belongs inside the installed package. Tests isolate these
locations with temporary profiles. See docs/getting-started.md for installation
and migration, and docs/settings/README.md for paths and precedence.
- Follow system, tool, and user instructions first, then this file, then
the rules under
.claude/rules/(running.md: what runs and installs, how to read a result;traces.md: what the pages do with a trace;ui.md: the owner's rules for every page and the scope of UI checks). - Prefer small, targeted diffs. Do not refactor unrelated areas opportunistically.
Three dependency rules. Each is load-bearing and each has a test.
app/nullius/imports no Flask. The backend is exercisable from a bare shell or a notebook.test_charts.NoFlaskchecks every module in the package, not one file. If a test inapp/testsneeds Flask, the layering has broken, not the test.- One name, one implementation per interpreter.
app/tests/test_dependency_surface.pycounts providers ofnulliusand fails when there is more than one. A local package can shadow an installed distribution, so successful imports alone do not prove this constraint. Source tests use the checkout; wheel checks use an isolated interpreter outside it. The wheel carries the backend once, separate from_nullius_app. - The templates' field contract is pinned. Templates read Python data objects (such as Job, Trial and Verdict)
by attribute name, and no import catches a renamed field —
it surfaces as a blank cell or an
UndefinedErrorat request time.app/tests/field_contract.jsonpins them andtest_field_contract.pyfails on a removal. After a deliberate change to a rendered class, refresh the pin withpython3 app/tests/field_contract.py --write; it refuses when a module cannot be imported, so a refresh cannot bless a loss.
Bundled resources resolve from __file__; personal storage resolves through
nullius.paths. The source app/ directory moves as a unit; moving it also
requires updating the package mapping in pyproject.toml and the test-discovery
paths in .github/workflows/tests.yml.
The backend suite imports only the standard library; its offline Harbor build
fixtures also need git and uv/uvx on PATH. Only the route suite needs Flask,
the one requirement in app/requirements.txt. Use uv run --no-project for
source commands so the root packaging metadata does not install a second copy
of Nullius into the source test interpreter. Run everything from the
repository root. app/tests/parallel.py runs a suite one module per process,
several at a time; python3 -m unittest discover -s app/tests -t app runs the
same tests in one process.
The parallel runner selects a temporary profile before importing test modules.
When invoking unittest directly, set NULLIUS_HOME to a temporary directory
and unset personal catalogue/storage overrides before discovery.
# no Python dependencies; git and uv/uvx are test tools
python3 app/tests/parallel.py
# with the docker tier: the tests that build and run containers for real
# (app/tests/docker_tier.py), skipped otherwise; CI runs them nightly
python3 app/tests/parallel.py --docker
# only the route suite needs Flask
uv run --no-project --with-requirements app/requirements.txt \
python app/tests/parallel.py app/ui_tests
# docs drift is the /docs-drift skill (.claude/skills/docs-drift/SKILL.md),
# run by hand: its script checks links and paths, then the prose is read
# the installed app (or use the source command below)
nullius serve
# from a source checkout
uv run --no-project --with-requirements app/requirements.txt app/app.py jobs
# the templates' field contract: refresh the pin after a deliberate change to a
# rendered class (the backend suite checks it; the refresh refuses while any
# module cannot be imported)
python3 app/tests/field_contract.py --write
# the Supported sections, a page per benchmark, agent and capability: refresh them after
# a change to a definition under app/nullius/catalogue (the backend suite checks them)
python3 app/tests/catalogue_docs.py --write
# the importer registry: ATIF and OpenTelemetry
PYTHONPATH=app python3 -m nullius.importers.cli --listDebug mode is off by default. Use --debug only for development; --no-debug
overrides an explicit DEBUG=1 environment setting.
CI runs the experimental PR judge, backend and route suites, and a temporary
wheel install/reinstall check on pull requests. app/packaging/verify.py checks
runtime assets, one backend provider, installed commands and pages, and user
storage preservation. The tests workflow also runs on pushes to main and by hand.
The manual draft release workflow (.github/workflows/release.yml) runs both
suites and uses app/packaging/release.py to build and verify a release
candidate's wheel and source archive, then attaches them and SHA256SUMS to a
draft prerelease. It does not publish or upload to PyPI; see
app/packaging/README.md and the versioned notes under docs/releases/.
The separate manual publish to PyPI workflow (.github/workflows/publish.yml)
requires confirmation and a reviewed, published GitHub prerelease. It verifies
the downloaded assets before a separate pypi environment job uploads the
same bytes through Trusted Publishing. PyPI and environment configuration
must be completed before that workflow can publish.
The docker tests workflow (.github/workflows/docker-tests.yml) builds the
standard's base image and runs the backend suite with the docker tier, nightly
and by hand, never on a pull request. The documentation site workflow
is run by hand and only checks: the host builds and publishes the site from
docs-release, advanced to the release tag only after the PyPI upload succeeds.
Preview deployments are disabled; bash app/site/host-build.sh is the same check locally. See
app/site/README.md.
UI checks: routine interface changes require relevant unit and route tests
plus focused browser checks of the affected pages and interactions, including
the themes and viewport sizes the change affects. Full screenshot coverage is
nightly or manually requested, not required after every UI change. The
UI sweep workflow (.github/workflows/ui-sweep.yml) is configured for 02:17 UTC
nightly (17 2 * * *) and workflow_dispatch, with no push or pull-request
trigger. Once it is on the default branch, use Actions → UI sweep → Run
workflow, optionally supplying a benchmark key; leave it empty for full
coverage. The isolated Flask/Playwright runner prints the report to the log,
keeps no artifact (artifacts count against the account's storage), and fails
on page JavaScript errors or clipped text. Screenshots are the local sweep's
and need visual review; there is no pixel-baseline comparison.
The /ui-sweep skill (.claude/skills/ui-sweep/SKILL.md) documents the local CLI
and optional --only KEY for focused checks.
The PR judge (.github/workflows/judge-pr.yml, running
.github/scripts/judge_pr.py, standard library only, nothing of the
application imported) comments on every pull request: two ratings, then a
table of questions. Merge risk (minor, medium, significant, major,
severe) is the TypeSafe judge's reading of what happens if the change merges
and is wrong, floored by the script (a deleted test, a key-shaped string or a
workflow change is severe at least; the standard, execution, hosts, secrets,
the parser or the classifier is major at least). Priority (low, medium,
high, extreme) is how deep the diff reaches, computed from its paths alone.
The questions, asked k=3 times with medians: each acceptance criterion of the
linked issue (the ## Acceptance checkbox list, one row per box; a body
without one is judged whole), whether the issue and the PR describe the same
change, whether the description matches the diff, whether the diff stays in
scope, whether tests or the ## Verified section cover it, whether the docs
follow, whether ## Risk names what could break; ## Before and after is
counted by the script when the diff touches what is visible. A PR that links
no issue reads – on the issue rows. Folded: the per-criterion table and the
script's own checks (no machine attribution trailer, new code under app/,
no Flask import under the backend, no key-shaped string). Everything the
judge reads has been through redact first: key-shaped strings and email
addresses never leave the runner. The verdict is a new comment on every run
with the earlier ones collapsed as outdated, never deleted, and the step
summary (cost and calls there, never in the comment). Automatic judging skips
draft PRs; a /judge comment from an owner, member or collaborator asks again
on the same commit, including a draft. An eligible newer judge job cancels the
older one for that PR; other comments cannot cancel it. The test workflow also
cancels older runs for the same ref. Experimental:
never a required check; without the secret TYPESAFE_API_KEY (or
API_KEY_TYPESAFE, the older name) it reports "not configured" and stays
green. Offline: python3 .github/scripts/judge_pr.py --pr-file pr.md --diff-file pr.diff [--issue-file issue.md] --dry builds the states and asks
nothing.
Run analysis is the /run-analysis skill
(.claude/skills/run-analysis/SKILL.md): triage of a run that failed or looks
wrong, an analysis of one job, or a comparison of several, written up as one
self-contained page. Its script, .claude/skills/run-analysis/scripts/runs.py
(standard library only, no import from app/, reads the job trees and writes
only what --out names), prints health verdicts, computes the metrics and
renders the page from the template beside it; the findings and the prose are
the assistant's, by the procedure in the SKILL.md. Nothing in it is specific to
one assistant or one benchmark.
Use nullius.thinkflowlab@proton.me for Nullius-owned contact email addresses,
including security reports.
No machine attribution in commit messages or pull request descriptions — no
Co-Authored-By: Claude … or any assistant variant, no Claude-Session: or
other session metadata, no generated-by signature. Keep the message about the
change and why it was made.
Legitimate human co-authors are preserved. A Co-Authored-By naming a real
person who contributed is correct and expected; this rule removes machine
trailers, not people.
Two things enforce it, so it does not depend on anyone remembering:
.claude/settings.json sets attribution to empty for commit and pr with
sessionUrl off, and a commit-msg hook in the shared Git directory rejects
machine signatures while allowing human co-authors. Do not bypass the hook
(no --no-verify).
Existing commits are not rewritten. Do not amend, rebase or force-push to strip trailers from history; what is already there is the repository owner's to audit. The rule applies to the next commit, not the last one.
The metric field guide (docs/field-guide.md, published in the documentation
under Reference → Metrics) is a living document: a change that alters what it
defines updates it in the same change. app/tests/test_field_guide.py fails
when a field it names leaves the backend, when its stated counts stop
matching its tables, or when its outcome vocabulary stops being the
classifier's; what each metric means is still read by a person.
The builder saves and imports recipes as JSON. Review runs them through the
execution backend; script previews and downloads are not part of the interface.
app/ui_tests/test_recipe_execution_fixtures.py pins the compiled execution plans.
Before opening anything, search existing issues and pull requests — across the repositories involved, not just this one. A duplicate costs more than the search does, because it splits the evidence.
An issue carries enough to act on without a conversation. The forms under
.github/ISSUE_TEMPLATE/ (Bug, Enhancement) hold the fields: Priority
(how deep it reaches: low the surface, medium the web layer or the
catalogue, high the backend, extreme what runs and proves), Components,
Evidence (what was observed, quoted rather than characterised), a
Screenshot when it is visible, Reproduction (the smallest steps or
input that show it), Expected, Acceptance (one checkbox per
criterion; the judge gives each its own row) and Dependencies and
related. A pull request's body follows .github/pull_request_template.md:
Closes #n, What, Touches, Before and after (two images when the change is
visible), Verified, Risk. An assistant files either with the /file-issue
skill (.claude/skills/file-issue/SKILL.md), which produces the same shape
with gh. No key, address, personal message or private link in any of it.
Where it goes:
- File a bug in the repository that owns the cause, not the one where the symptom appeared: a cause in Harbor is Harbor's issue, with a link from here.
- Minor or unconfirmed observations go to the parent, rather than becoming a speculative issue in a component repository.
How it is kept:
- Set the native Type and Priority when the issue is created. An empty
field is invisible in a list; where the priority is genuinely undecided,
choose the lowest that is defensible and say in the body that it is a
placeholder. Reuse the labels that exist (
bug,enhancement). - Assign
aitianshiwhere that is the real ownership; an inappropriate assignee is worse than none, and a session identifier is not an account. - Every implementation pull request references its issue, and the issue's
Development panel shows it. Body text is the means, not the proof: read it
back with
closedByPullRequestsReferencesin the GraphQL API before calling the work done. Stacked pull requests link the same way; a base is never retargeted for the panel's sake. - Update the metadata at every change of scope, ownership, priority, dependencies, implementation PR, validation evidence or completion.
Recording something is not a licence to fix it. Writing an issue for a defect found in passing is the correct response to noticing it; widening the current change to address it is not. Repositories stay private, and nothing merges without the user's word.
When evaluating an agent's capabilities, inspect the pinned upstream's callable
interfaces; do not infer memory or retrieval from conversation support. Keep
native, augmented and replacement setups distinct. A component's port proof
does not prove an agent uses it, and adding a tool is not necessarily replacing
a native capability. The current runner has no generic agent-plus-capability
recipe. Follow docs/standard.md, "Native capabilities and composition", and
the import skills for evidence requirements and reporting integration gaps.
One line each; the code and the pages carry the rest.
- The product, what each page does and why —
README.md, then the topic pagesdocs/builder.md,docs/judgement.md,docs/settings/README.md,docs/cli/README.md - What each metric means and how portable it is —
docs/field-guide.md - Open work — the GitHub issues
- The benchmark catalogue: a folder per benchmark, what an entry holds, the
listing rule and its checks — the docstring of
app/nullius/catalogue/catalog.py - The builder's vocabulary and the agents it offers —
app/nullius/recipes/launch.py; the recipe coreapp/static/builder/command-builder-core.js - The Harbor a recipe pins: the pin, the build here, the cache, the scan, the
harborscript a run calls — the docstring ofapp/nullius/harbor/source.py;docs/builder.md, "Which Harbor" - Bringing a new benchmark in, either way, starts at the
/import-benchmarkskill (.claude/skills/import-benchmark/SKILL.md), which asks the route - Harbor, or the Nullius standard through/import-standard, where a paper's own scripts run as its Original setup; by hand,docs/examples/new-benchmark/- a Harbor entry to copy, and the steps; a Harbor benchmark also from the Benchmarks page, + Add a benchmark - a prompt for any assistant - or by hand Add a Harbor benchmark (app/nullius/catalogue/harbor_entry.py) - The Nullius standard - benchmarks, agents and capabilities matched by proven
ports, run over ACP, MCP and the relay; importing each by hand and with the
/import-standardskill for a benchmark,/import-agentfor an agent and/import-capabilityfor a capability (.claude/skills/<name>/SKILL.md; the Agents and Capabilities tabs' Add button writes the prompt for any assistant) -docs/standard.md; the code underapp/nullius/standard/. Nullius is a facilitator, not a re-implementer: a benchmark, agent or capability runs its upstream's own code at a pin, its dependencies in its own environment, and any code of ours beyond glue is asked about first - Settings, its variables and their validation —
app/nullius/settings.py; run hosts, keys and the connection test —app/nullius/hosts.py - Removing a benchmark, agent or capability throughout the app, including
generated artifacts and requested runtime data —
/remove-integration(.claude/skills/remove-integration/SKILL.md) - Running a recipe: the plan, the parts, a host, the mirror, the record a
restart reads — the docstrings of
app/nullius/execution.py; the routes inapp/web/execute.py - Judgement: questions, state, client, store, batch, reconcile, lanes —
app/nullius/judge/ - The live board, and the four rules it must keep — the docstring of
app/nullius/runs/live.py - Machine-specific notes (confinement, disk) —
.claude/environment.local.md, gitignored and per checkout