Nnora

Chapter 7

Run Your First Observer

Start with a local journal, move to a board, and connect a measured change to a controlled follow-up.

Chapter 7 of 8 / 4 min read
Browse chapters

This chapter is the operational path from nothing to a board that studies its own measurements. Everything up to hardware runs on a laptop with Python 3 alone. No board, no install, no keys.

Part 1: On your laptop

Run one analysis

From the repository root, ask a question about a file you make yourself:

mkdir -p /tmp/nora-lab && python3 - <<'PY'
import json, datetime as dt
now = dt.datetime.now(dt.timezone.utc)
rows = [
    {
        "timestamp": (now - dt.timedelta(hours=(36 - i))).isoformat(),
        "weight_g": round(200 + (0 if i < 18 else 14) + (i % 3) * 0.4, 2),
    }
    for i in range(36)
]
open("/tmp/nora-lab/scale.jsonl", "w").write("\n".join(json.dumps(r) for r in rows))
PY
printf '%s' '{"mode":"investigate","state_dir":"/tmp/nora-lab/state","analysis":"level_shift","subject":"scale","params":{"source":{"kind":"journal","path":"/tmp/nora-lab/scale.jsonl"},"key":"weight_g"}}' | ./skills/research_agent/run.sh

The verdict is material_unresolved: the second half of the record sits about 14 g above the first, far beyond the within-half spread. The finding names its own limitation: a level shift is a change in the record, not an explanation for it.

Let it find the question itself

Point a cycle at the directory instead of naming an analysis:

printf '%s' '{"mode":"cycle","state_dir":"/tmp/nora-lab/state","journal_dirs":"/tmp/nora-lab"}' | ./skills/research_agent/run.sh

raised shows the question the scan wrote for itself. Run the command a second time: raised and investigated are both empty. A question it already knows is not news, and a question it could not close is re-checked on an interval, every six hours by default, rather than re-derived every cycle.

Read the state

printf '%s' '{"mode":"questions","state_dir":"/tmp/nora-lab/state"}' | ./skills/research_agent/run.sh
printf '%s' '{"mode":"reportable","state_dir":"/tmp/nora-lab/state"}' | ./skills/research_agent/run.sh

reportable is the list an adapter would deliver to a human. Everything else stays on the board.

Part 2: On the board

Build

On a development machine with Go 1.21 or later:

GOOS=linux GOARCH=riscv64 CGO_ENABLED=0 go build -o nano-os-agent main.go

Copy the binary and repository runtime to the configured LicheeRV Nano using INSTALL_BOARD.md.

Verify the services

wget -qO- http://127.0.0.1:18790/health
wget -qO- http://127.0.0.1:18790/ready
/opt/picoclaw/picoclaw skills list

/health shows that the gateway process responds, /ready that the runtime can serve requests, and the skills list shows the routes to inspect before answering any board question. The same evidence is served at http://BOARD_IP:18800/runtime and http://BOARD_IP:18800/nano-os-agent.

Execute a read-only task

Save this as /tmp/tutorial_system_snapshot.yaml:

- id: tutorial_system_snapshot
  name: "Tutorial system snapshot"
  priority: 1
  status: pending
  steps:
    - id: read_system
      action: call_skill
      save_as: system
      parameters:
        skill_name: system_info
      expect:
        ram_total_mb: ">=1"
      timeout: 15
      on_fail: block

Run and inspect it:

/root/nano-os-agent/nano-os-agent --once /tmp/tutorial_system_snapshot.yaml
tail -1 /root/nano-os-agent/experiments.jsonl 2>/dev/null || tail -1 /tmp/experiments.jsonl

Confirm the task ID, step counts, verdict, duration, and before/after metrics. Do not evaluate success from prose alone.

Extend it into a monitor

Replace the one-shot system skill with a sensor or scene-observation skill and add a bounded repeat block:

repeat:
  interval_sec: 600
  max_iterations: 144
  journal_path: /tmp/monitors/tutorial_observation.jsonl
  continue_on_fail: true

Keep the task at status: template in version control. Create a pending copy only when the board, sensor, interval, storage budget, and stop condition are confirmed.

Let the board research the monitor

The journal your monitor writes is already a valid research input. Install the cycle task:

cp tasks/029_autonomous_research_cycle.yaml /root/nano-os-agent/tasks/

It calls research_agent mode=cycle every fifteen minutes against /tmp/monitors, bounded to three questions and twenty seconds, and appends each finding to the same experiments.jsonl the executor uses. Findings are therefore visible through the existing routes:

printf '%s' '{"mode":"reportable"}' | /root/nano-os-agent/skills/research_agent/run.sh
grep autonomous_research /root/nano-os-agent/experiments.jsonl | tail -3

Deliver a finding to a human

The engine never sends messages. An adapter reads reportable, phrases the finding in the operator's language, and asks. Vineyard Guard does this through the Telegram outbox; a bench rig might print to a dashboard or open an issue. The rule is the same everywhere: state the numbers, order the options by cost, and accept "nothing for now" as an answer.

Cross-domain examples

Each of these is the same engine with different parameters. The next chapter shows how a domain declares them.

Next: adapt it to your own domain.