| name | nous |
| description | Drive Nous, the hypothesis-driven experimentation framework, to investigate software systems with the scientific method via the `nous` CLI. Use when the user wants to run a Nous campaign, author or scaffold a campaign.yaml, kick off / monitor / resume / stop / report on hypothesis-driven experiments, or systematically investigate why a system (LLM server, DB optimizer, scheduler, router, cache) behaves the way it does through controlled experiments. |
Nous โ hypothesis-driven experimentation
Nous runs the scientific method on software systems. An AI planner formulates
falsifiable hypotheses about a target system, designs controlled experiments,
an executor runs them, and Nous extracts reusable principles from the
results โ whether the hypothesis is confirmed or refuted. Knowledge accumulates
across iterations so the same mistake is not repeated.
It fits systems with observable metrics, controllable knobs, reproducible
execution, and decomposable mechanisms: LLM serving, query optimizers,
schedulers, routers, caches, load balancers.
Repo: https://github.com/AI-native-Systems-Research/agentic-strategy-evolution
This is a Nous agent pod
nous is pre-installed in this pod (pinned, on PATH) and claude is
authenticated through the platform's credential gateway โ Nous's Claude Agent
SDK calls work with no API key in this pod. Never ask the user for a
credential, never write one to disk, and never pip install Nous yourself.
The pod-level workflow (per-campaign directories, unique run ids, always
--auto-approve, running campaigns in the background, resume-on-restart) is
defined in this pod's system context (AGENTS.md). This skill is the CLI and
campaign-authoring reference; follow AGENTS.md for how to drive a campaign
in this environment.
The loop
Each iteration is a deterministic state machine; the LLM only acts inside the
phases, never the orchestration:
INIT โ DESIGN โ HUMAN_DESIGN_GATE โ EXECUTE_ANALYZE โ HUMAN_FINDINGS_GATE โ DONE โ (next iteration)
- DESIGN (planner, Opus by default) โ authors a
bundle.yaml: a hypothesis
with multiple falsifiable arms (see below) plus an experiment plan.
- HUMAN_DESIGN_GATE โ approve the design before expensive compute runs.
- EXECUTE_ANALYZE (executor, Sonnet by default) โ runs each arm in an
isolated git worktree, collects metrics, classifies prediction errors.
- HUMAN_FINDINGS_GATE โ approve findings before they enter the knowledge base.
This pod always runs --auto-approve (both gates auto-pass) so campaigns run
unattended to completion. Front-loading locked_parameters is what keeps a run
defensible โ see below.
Quick start (full workflow)
nous --help
nous create-campaign --to ./campaign.yaml \
--target-name "Your System" \
--research-question "What mechanism drives the primary bottleneck?" \
--target-repo-path ./repo
nous schema campaign
nous run campaign.yaml --auto-approve --max-iterations 3
nous status campaign.yaml --watch
nous report campaign.yaml
nous cost campaign.yaml --cache-stats
nous package campaign.yaml
Worked example: a BLIS campaign end-to-end
A concrete campaign against BLIS (the inference-sim discrete-event
LLM-serving simulator) โ the whole arc: clone, author, run, then harvest the
findings into the wiki.
run_id="blis-prefix-ttft"
dir="$NOUS_CAMPAIGN_PARENT/$run_id"
mkdir -p "$dir"
git clone https://github.com/inference-sim/inference-sim.git "$dir/repo"
$dir/campaign.yaml:
research_question: >
With total input length held fixed, does increasing the prefix portion
(cached tokens) reduce TTFT under moderate load?
run_id: blis-prefix-ttft
max_iterations: 2
target_system:
name: "BLIS โ LLM Inference Serving Simulator"
description: >
BLIS is a discrete-event simulator for LLM inference serving.
It models request arrivals, scheduling, and KV-cache management.
repo_path: /home/agent/nous-campaigns/blis-prefix-ttft/repo
observable_metrics: [ttft_p50_ms, throughput, scheduling_delay_p99_ms]
controllable_knobs: [prefix_fraction, concurrency, cache_policy]
iterations:
- mode: rehearsal
- mode: real
locked_parameters:
model: meta-llama/Llama-3-8B
concurrency_per_tenant: 8
duration_seconds: 120
warmup_seconds: 20
ground_truth:
pre_registered: true
primary_metric: "P50(ttft)"
direction_claim: "P50(ttft) decreases as prefix fraction increases at fixed total length"
pass_condition: "direction holds in median across seeds AND in >=7 of 10 seeds"
seeds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
models:
design: "claude/aws/claude-opus-4-8"
execute_analyze: "claude/aws/claude-sonnet-4-6"
report: "claude/aws/claude-sonnet-4-6"
prompts:
methodology_layer: "prompts/methodology"
domain_adapter_layer: null
cd "$dir"
nohup nous run campaign.yaml --auto-approve --max-iterations 2 > campaign.log 2>&1 &
echo $! > run.pid
nous status "$run_id" --watch
/post-campaign "$dir"
Authoring a campaign
nous schema campaign is the single source of truth. The minimal required
shape:
research_question: >
One falsifiable sentence. e.g. "With total input length fixed, does increasing
the cached prefix portion reduce TTFT under moderate load?"
run_id: my-campaign
max_iterations: 2
target_system:
name: "My System"
description: >
What the system does, its architecture, exact paths, baselines, data-schema
gotchas, and statistical guardrails. Put domain context HERE.
repo_path: ./repo
observable_metrics: [latency, throughput, error_rate]
controllable_knobs: [batch_size, cache_policy]
prompts:
methodology_layer: "prompts/methodology"
domain_adapter_layer: null
Key authoring discipline (see reference/campaign-schema.md for the full field list):
locked_parameters / locked_workload โ hard-pin every knob whose
deviation would invalidate the experiment (model, concurrency, duration,
warmup, KV blocks, workload distributions). Mismatches are hard validation
failures even under --auto-approve. In this pod every campaign runs
auto-approved, so this inventory is mandatory โ a campaign with no
locked_parameters will refuse to run. Front-load it; adding locks reactively
after each review round turns a 2-week campaign into a 5-round dance.
iterations: [{mode: rehearsal}, {mode: real}] โ schedule iter-1 as a
cheap rehearsal (does the apparatus parse? does the regime engage the
mechanism?) and iter-2+ as full real runs.
live_target: true (under target_system) โ for probing a running
system (cluster, service, non-git dataset) with no code to evolve. No
per-iteration worktree; bundles must contain no code_changes arms. The
target must be reachable from this pod's egress rules.
ground_truth โ pre-register direction_claim, pass_condition,
primary_metric, and seeds before any iteration so the agent can't move the goalposts.
models โ always set this explicitly (see below). Per-phase:
design, execute_analyze, report.
Selecting models (don't trust the defaults)
Nous's built-in defaults are claude-opus-4-6 / claude-sonnet-4-6, but the
model gateway in this pod fronts a specific upstream whose catalog usually
doesn't include those exact IDs โ and a campaign that requests a model the
gateway can't serve hangs. So discover the catalog and pin models: to it:
echo "opus=$ANTHROPIC_DEFAULT_OPUS_MODEL sonnet=$ANTHROPIC_DEFAULT_SONNET_MODEL"
curl --noproxy '*' -fsS 'http://127.0.0.1:24180/v1/models?limit=1000' | jq -r '.data[].id'
Use the IDs verbatim (they may be namespaced, e.g.
claude/aws/claude-opus-4-8): design โ newest opus, execute_analyze and
report โ newest sonnet. If a tier is absent, fall back to the most capable
model the catalog does list (opus โ sonnet โ anything).
Author with the user (assume they're a Nous beginner)
Assume the user has never run a Nous campaign and doesn't know its knobs. Don't
silently pick everything and launch โ propose, explain briefly, confirm, then
run. A good flow:
- Gather what you can decide yourself. Read
reference/campaign-schema.md for the full
field set, and discover the gateway model catalog (above, "Selecting models")
so you already know which model IDs are usable โ never make the user supply a
model. Skim the target repo for likely observable_metrics /
controllable_knobs.
- Draft a full
campaign.yaml from what you inferred โ research question,
target_system.description, gateway-served models, a rehearsal+real
iterations schedule, and a first pass at locked_parameters.
- Ask the user to make the vague parts concrete, in plain language and one
short round โ e.g. how many real iterations (more = more confidence, more
cost/time), how long each run / how many seeds, which knob they actually
care about, which parameters must stay fixed. Explain the tradeoff behind
each question rather than dumping schema jargon on them.
- Show the final
campaign.yaml and get an explicit go-ahead before
nous run. Call out the rough cost/time and the model IDs you pinned. Never
launch a campaign the user hasn't seen and confirmed.
The five hypothesis arms
Every DESIGN bundle tests one mechanism from multiple angles:
| Arm | Purpose |
|---|
H-main | Validates the primary mechanism |
H-ablation | Isolates individual component contributions |
H-super-additivity | Detects interaction effects between components |
H-control-negative | Confirms specificity (the effect shouldn't appear here) |
H-robustness | Tests generalization across conditions |
Fast-fail rules skip wasted compute on ablations/robustness when H-main is refuted.
Running, monitoring, controlling
nous run campaign.yaml --auto-approve --max-iterations 10 --timeout 1800 --max-cli-retries 50
nous run campaign.yaml --bundle ./bundle.yaml --auto-approve
nous resume campaign.yaml --auto-approve
nous status campaign.yaml --watch
nous status campaign.yaml --line
nous stop campaign.yaml --reason "regime looks wrong"
nous stop campaign.yaml --immediate
nous cost campaign.yaml --cache-stats
nous report campaign.yaml
Notes:
--agent sdk is the default backend (Claude Agent SDK, gateway-authenticated
here). --agent inline emits prompts to stdout for an enclosing agent framework.
- Default per-phase timeout is 1800s (30 min);
--max-cli-retries default 10,
-1 = unbounded.
- If
--auto-approve refuses to proceed, the run is missing required
locked_parameters โ declare the locks first. (NOUS_ALLOW_AUTO_APPROVE=1 is
already set in this image, so that gate is not the cause here.)
Reading liveness (don't be fooled): nous status --line gives phase/iteration;
for fine-grained progress watch the executor log mtime under runs/iter-N/ (e.g.
runs/iter-N/inputs/executor_log.jsonl) and the result-file count โ they advance
continuously during EXECUTE_ANALYZE. A backgrounded run's campaign.log only
writes at phase transitions, so it looks frozen mid-phase โ don't read "no new log
lines" as "stuck". The STUCK marker is a ~5-min-silence heuristic that fires
during legitimate long batches: treat it as "look closer", not "it died". Phases
are long (DESIGN ~10โ15 min) โ poll infrequently with wide spacing; looking more
often doesn't make it go faster.
Reporting progress to Slack/Telegram (the channel bridge)
Nous's native channels: feature POSTs a markdown summary at every DESIGN/FINDINGS
gate โ and it fires under --auto-approve (the notify runs before the gate
auto-passes), so it serves as unattended progress reporting. In this pod, don't
point it at an external webhook (that needs egress allowlisting + a secret on
disk); point it at the in-pod channel bridge, which relays each summary to
the agent's bound Slack/Telegram thread via the platform's send_channel_message
โ no external egress, no secret.
The agent wires this in automatically when a channel is bound to the agent
(it checks describe_channel before each run); you don't have to ask. See
AGENTS.md for the operate-in-this-pod steps (incl. resume-on-restart).
One bridge serves the whole pod โ it's stateless per request (each POST carries
its own channel + text and does a fresh MCP call), so every campaign and
session shares the single 127.0.0.1:8765 listener. Launches are idempotent and
a duplicate start is a harmless no-op (the bridge exits cleanly if the port is
already taken). Bridges in other agent pods are isolated โ 127.0.0.1 is
pod-local, and each posts only to its own agent's bound channel.
nohup nous-channel-bridge > "$NOUS_CAMPAIGN_PARENT/.bridge.log" 2>&1 &
channels:
- kind: webhook
url: http://127.0.0.1:8765/gate?channel=slack
How it holds together: NO_PROXY=127.0.0.1 (set in the image) keeps Nous's POST
local; the bridge's own call to the MCP endpoint routes back out through the
egress gateway and is authorized by the pod's mesh identity (no token). Delivery
is best-effort โ a hiccup logs a warning and never blocks the campaign. See
AGENTS.md for the operate-in-this-pod steps (incl. resume-on-restart).
Output artifacts
Under $NOUS_CAMPAIGN_PARENT/<run_id>/:
state.json โ orchestrator checkpoint (drives resume)
principles.json โ accumulated, reusable knowledge across iterations
ledger.json โ decision/event ledger; handoff.md โ human-readable summary
runs/iter-N/bundle.yaml โ the iteration's hypothesis + experiment plan
runs/iter-N/findings.json โ results, validation, prediction-error taxonomy
meta_findings.json โ cross-iteration synthesis & deployment recommendation
Post-campaign knowledge (the wiki)
When a campaign finishes, harvest its ledger.json / principles.json into a
cross-campaign wiki at ~/.nous/wiki/ so knowledge compounds across runs.
These are Claude Code slash commands shipped with this agent (in
~/.claude/commands/); the rendering scripts live at ~/scripts/:
/post-campaign ~/nous-campaigns/<run_id>
/visualize-campaign <campaign-name>
/visualize-registry
/suggest-next <repo-path-or-name> "your research question"
The wiki commands turn raw ledger.json / principles.json into structured
knowledge โ dead-ends (refuted approaches), frontiers (boundary
conditions), and untested interactions โ plus interactive HTML
visualizations. Knowledge compounds: /suggest-next draws on findings from
all indexed campaigns to point the next campaign at the highest-value open
questions.
The viz commands invoke python scripts/<name>.py; run them from $HOME
(where the shipped scripts/ live) or call ~/scripts/<name>.py directly.
The scripts read/write only under ~/.nous/wiki/.
Full CLI surface (13 subcommands)
Run nous <cmd> --help for exact flags. Grouped by purpose:
Lifecycle โ run & control
nous run <campaign> โ run end-to-end. Flags: --max-iterations, --model,
--run-id, --auto-approve, --timeout (default 1800s), --max-cli-retries
(default 10, -1=unbounded), --agent {sdk,inline} (default sdk),
--sandbox {bypass,default}, --bundle (skip DESIGN), --problem-md, --handoff-md.
nous resume <target> โ pick up an interrupted run at the last checkpoint
(--max-iterations, --model, --auto-approve, --timeout, --max-cli-retries, --agent).
nous stop <target> โ halt cleanly at the next phase boundary; --reason,
--immediate (abort mid-turn within seconds).
Author & inspect
nous create-campaign --to <path> โ scaffold a commented campaign.yaml
(--target-name, --target-description, --research-question, --run-id,
--target-repo-path, --force).
nous schema [campaign|bundle|findings] โ print the authoritative artifact
schema (--format {md,json,yaml}).
nous validate {design|execution} --dir <DIR> โ validate a work-dir against
the schema (the same gate the orchestrator runs internally).
Monitor & report
nous status <target> โ phase/iteration/principles snapshot; --watch,
--line, --interval.
nous cost <target> โ token/cost totals; --cache-stats for cache hit-rate.
nous report <target> โ (re)generate the LLM markdown findings report
(--model, --agent, --timeout; costs tokens).
nous reports <target> โ re-emit meta_findings.json deterministically,
zero LLM tokens; works on legacy/aborted runs. (Note the trailing s โ different command.)
Provenance & reproducibility
nous lineage <target> โ derivation chain + per-iteration cumulative.patch
availability (what derived_from inherited); --json.
nous replay <target> --iter <N> โ replay a specific recorded iteration from its artifacts.
nous package <target> โ tarball work_dir + reproduce.sh + Dockerfile +
README for paper artifact evaluation; --output.
Housekeeping
nous clean โ remove stale nous-exp-* worktrees/branches. --orphaned
(default: prune worktrees whose owning run is dead), --target-repo,
--campaign, --dry-run.
<target> is generally a campaign.yaml, a work_dir, or a run_id resolvable
under $NOUS_CAMPAIGN_PARENT.
When NOT to use Nous
Skip it for one-off tweaks, systems with no observable metrics, or
non-reproducible environments. Nous pays off when the question is a real
mechanism question and you want defensible, cumulative findings.