Files
nanobot-runtime/skills/note/SKILL.md
2026-07-22 12:32:02 +02:00

179 lines
9.6 KiB
Markdown

---
name: note
description: >
Capture notes, texts, URLs, or whole articles into a personal knowledge base and
answer questions against it. Triggers on "note X", "/note cron X", "search my
notes for X", "delete/edit the note about X", "forget X". For filing reference
material to search later — not a short durable fact/preference to just
remember, and not a bare URL saved only to read later with no filing.
---
# Note
A personal capture-to-knowledge-base skill. The user throws in notes, texts, URLs, or
whole articles from any channel; each input is captured raw, then reformulated and filed
into one structured markdown document (`notes/notes.md`) organized into thematic sections
that the LLM owns and grows. Search = load the whole document and answer from it.
## Architecture — read this first
Two-stage pipeline, one shared compile step:
- **Capture (always, instant, dumb).** `note_capture.py` writes the raw input verbatim
into `notes/inbox/` (atomic) plus one line to `log/note.log`. No reformulation, no
reading of `notes.md`, no fetching. This is all capture ever does.
- **Compile (reformulate + file into `notes/notes.md`).** Runs either **inline** in the
immediate mode, or in the **background** cron drain. Same workflow either way.
Storage layout under the workspace root (fixed locations):
```text
notes/
├── notes.md ← THE structured doc (thematic ## sections, LLM-owned)
├── inbox/ ← pending captures (one file each); compile drains this
├── done/ ← successfully compiled captures (sibling of inbox/)
├── hard/ ← held back: paywalled / unreadable / ambiguous — for manual review
└── .compile.lock ← concurrency lock shared by inline compile and cron drain
log/note.log ← append-only audit of every capture
```
**No database — ever.** Notes live *only* in `notes/notes.md` (prose) plus the `notes/`
pipeline dirs above. There is **no** SQLite/DB backend. To find a note, `grep` or read
`notes/notes.md` — **never search `db/`, never run `sqlite3`, never create or open any
`.sqlite`/`.db` file.** (The AGENTS.md "store SQLite under `db/`" convention does **not**
apply to notes — that is for other skills.) An older version of this skill used a
database; it is gone. If you catch yourself opening a DB, stop — the answer is in
`notes/notes.md`.
**Separate store.** `notes/` is not agent memory: keep it distinct from `keep`,
`MEMORY.md`, and the llm-wiki store (`cml/`). Never cross-read or cross-write between
them. The Dream processor must not touch `notes/`.
**Run scripts with `uv run`, workspace-relative paths** (exec runs from the workspace
root, not the skill dir): `uv run skills/note/scripts/<script>.py …`.
**Language.** This skill body is English; always reply to the user in the user's own
language.
## `/note <text>` — capture and file NOW (default)
The default: file the note into the knowledge base immediately, in this turn.
1. Read `Channel` / `Chat ID` from the runtime context if present.
2. Capture:
`uv run skills/note/scripts/note_capture.py --text "<raw input>" [--channel <ch>] [--chat-id <id>]`
Pass the input **as-is** — do not reformulate or strip URLs here.
3. Run the **Compile workflow** (below) inline: acquire the lock, process `notes/inbox/`,
file into `notes/notes.md`, move the source to `notes/done/` (or `notes/hard/`).
4. **Commit** the change (see *Versioning* below): via `exec` run
`git add notes/ && git commit -m "note: <short summary of what landed>"`.
5. Confirm to the user **which section** it landed under, **and quote the exact
text that was filed** (the reformulated fact(s), verbatim as written into
`notes.md` — not a re-summary of it), in their language.
This blocks the turn for a while (reads the whole doc; a URL/article adds a fetch). If
the user is firing off many notes quickly, suggest `/note cron`.
## `/note cron <text>` — deferred capture
Capture only; let the background cron file it later. Fast, non-blocking.
1. Read `Channel` / `Chat ID` from the runtime context if present.
2. `uv run skills/note/scripts/note_capture.py --text "<raw input>" [--channel <ch>] [--chat-id <id>]`
3. Confirm in **one short line** (e.g. "captured — I'll file it in the background") and **STOP the
turn**. Forbidden here: reformulating, reading `notes/notes.md`, running any compile
step, taking the lock. If you catch yourself about to read the doc, you are compiling
inline — stop and just capture.
## Compile workflow (shared: inline immediate mode + cron drain)
The cron (`note_compile.py`) invokes this via a drain goal; immediate mode runs it inline.
Either way:
1. **Take the lock.** Create `notes/.compile.lock` (skip if a live one exists — another
compile is running; try again later). The cron script handles this itself; inline mode
must respect it so an inline merge and a cron drain never edit `notes.md` at once.
2. **For each file in `notes/inbox/`:**
- **Reformulate** the body into a terse fact (or a few). One concept per entry; drop
filler; **preserve the input language** — never translate. Split if too complex.
If the input is Czech typed without diacritics (e.g. "kdyz uz to psal bez hacku"),
restore correct diacritics as part of reformulation. Leave already-accented text
and non-Czech text untouched — never add diacritics where none belong.
- **URLs:** extract **every** URL from the body (0..N). Fetch each with the `web` tool
(Jina Reader — returns clean markdown, handles JS and soft paywalls). If a URL is
**paywalled / login-gated / truncated / unreadable** (login/subscribe/metered
content, very short output, HTTP 401/403): **do not fabricate a summary** — write
just the URL + any available title + a `⚠ paywall/incomplete` marker. Whole articles:
summarize the key points.
- **File it** under the right thematic `##` section of `notes/notes.md`. Create a new
section if none fits. Use a surgical `str_replace`/append — never rewrite the whole
document.
- **Move the source out of `inbox/` immediately:** to `notes/done/` if anything usable
was filed (paywall markers count as filed — they are the breadcrumb); to `notes/hard/`
if nothing usable could be extracted. Move right after each file so a crash mid-batch
re-processes at most one.
3. **Release the lock** (the cron script does this in `finally`).
Never assert content you could not read. When unsure, hedge or mark it.
## `/note search <query>` / `/note find <query>` — query
Also triggered by "what do I have on …?", "find in my notes …".
1. Read the whole `notes/notes.md` — that single file **is** the knowledge base. Do
not read `inbox/`, `done/`, or `hard/` (those are the raw pipeline, not the KB).
2. Answer from it. If the topic is not covered, say so plainly — do not confabulate.
3. Read-only: never modify the document in a search turn.
## `/note delete <query>` / `/note edit <query>` — remove or change a note
Also triggered by "delete/remove the note about X", "forget X", "edit/update the note
about X". Notes are prose in `notes/notes.md`**no IDs, no DB.** The user
names a note by describing it; you find it by reading the document.
**This is a hard-gated TWO-TURN flow. NEVER delete or edit in the same turn as the
request — showing is not doing.**
**Turn 1 — locate and confirm (absolutely NO mutation):**
1. Read `notes/notes.md` to find the matching note(s). That file is the only place a note
lives — **do not search `db/`, do not run `sqlite3`, do not read `inbox/`/`done/`/
`hard/`.** If nothing matches, say so. If the target is ambiguous or several entries
match, list the candidates and ask which one.
2. Show the user the **exact verbatim line(s)/section** you would remove (for an edit: the
`before``after`), and ask them to confirm in plain words. Then **STOP the turn.**
Forbidden this turn: `str_replace`/`edit_file`, `rm`, `git`, or any other mutation.
**Turn 2 — only after the user explicitly confirms ("yes", "confirmed", "delete it"…):**
1. Remove/change it in `notes/notes.md` with a surgical `str_replace` (never rewrite the
whole document). **Touch `notes/notes.md` only** — do NOT delete or move anything in
`notes/done/`; those raw-capture breadcrumbs are internal plumbing, not a second copy
of the note.
2. Commit (see *Versioning*): via `exec` run
`git add notes/ && git commit -m "note: delete <short desc>"` (edit → `note: edit …`).
**Exception:** a capture still **pending** (not yet compiled, a file in `notes/inbox/`)
never reached the KB — you may cancel it directly with `exec: rm notes/inbox/<file>`
(commit only if it was already tracked).
## Versioning (git)
`notes/` lives inside the workspace git repo. The Dream processor never touches it, so
**this skill is the only thing that commits `notes/`** — do it after every change to
`notes/notes.md`:
- **Inline `/note <text>` and delete/edit:** commit in the same turn via `exec`
(`git add notes/ && git commit -m "note: …"`). One commit per operation.
- **Cron drain:** `note_compile.py` commits deterministically after the batch — you do
not commit inside the cron `DRAIN_GOAL` run.
- Never `git add -A` (Dream owns the rest of the workspace); stage only `notes/`. The
`.compile.lock` is gitignored, so `git add notes/` never stages it.
## Edge cases
- `/note` with no content → ask what to note.
- Empty / whitespace-only input → `note_capture.py` exits non-zero; ask for real content.
- The compile step, not capture, decides sections and does all fetching. If you ever find
yourself reformulating or reading `notes.md` during a `cron` capture, stop.