Prompts & agent self-modification
The project's prompts/ directory holds markdown files that shape how the agent thinks. Anything you drop here that parses against the prompt schema is treated like a first-class prompt — there are no hard-coded special filenames. The CLI and the agent itself both have full CRUD access (list, read, create, edit, delete).
What ships in a fresh project
botholomew init seeds three files. None of them are special — init just writes them so a new project has something to start from. You can rename, delete, or replace any of them; the loader doesn't care about filenames.
| File | Purpose |
|---|---|
goals.md | The agent's identity (the wise-owl prose) plus the current goal list. Updated as goals complete. |
beliefs.md | Priors the agent has learned about the world / project. |
capabilities.md | LLM-summarized, thematic inventory of what the agent can do. Auto-regenerated by botholomew capabilities. |
Every prompt requires three frontmatter fields — exactly these three, no extras:
---
title: Beliefs
loading: always # or "contextual"
agent-modification: true # or false
---
# Beliefs
- I should be concise and clear in my work products.
- I should ask for help when I'm stuck rather than guessing.Strict validation
Every prompt file is validated on load. The schema requires title (non-empty string), loading (always | contextual), and agent-modification (boolean). Unknown keys are rejected.
If any file under prompts/ fails validation, the loader throws and the worker fails the task (or the chat turn refuses to start) with a message naming the offending path and reason. There is no quarantine — bad prompts must be fixed.
Run botholomew prompts validate at any time to check every file without starting a worker. The command exits non-zero if anything is wrong.
Upgrading from an older project? Older
initruns wrotesoul.md, and none of the seed files had atitle:field. After upgrading you must either deletesoul.md(its content has been merged intogoals.mdfor new projects) or add atitle:line, and addtitle:to the other prompt files inprompts/. The validator will name any file it can't load.
Loading modes
loading: always — the file is concatenated into every system prompt, verbatim. Use sparingly.
loading: contextual — the file is included only if its content shares keywords with the caller's current intent. The worker derives keywords from the running task's name and description; the chat agent derives them from your most recent message. Good for topic-specific notes ("Everything I know about our invoicing system") that shouldn't pollute the prompt on unrelated tasks.
See loadPersistentContext() and extractKeywords() in src/worker/prompt.ts.
The hardcoded ## Style block
After the prompt files (and after the optional MCP section), every system prompt — worker and chat alike — appends a short ## Style block defined as STYLE_RULES in src/worker/prompt.ts. It tells the model to skip sycophantic preambles ("You're absolutely right!", "Great question!"), push back when the user is wrong, and report failures and uncertainty directly. This is hardcoded so it applies to every install without needing to re-run botholomew init. Anything you put in your own prompt files still loads above it and can layer on top.
CRUD: CLI and agent tools
Both surfaces validate on every write. They reject names containing slashes, .., or characters outside [a-zA-Z0-9._-], and they reject files whose round-tripped content wouldn't load back.
CLI: botholomew prompts …
| Command | Description |
|---|---|
prompts list [-l <n>] [-o <n>] | Tabular list with name, title, loading, editable, size, status |
prompts show <name> | Print raw file (frontmatter + body) |
prompts create <name> [--title <s>] [--loading always|contextual] [--no-agent-modification] [--from-file <path>] [--force] | Create a new prompt; body from file (- for stdin) or a default # Title skeleton |
prompts edit <name> | Open $EDITOR on the file; refuse to keep invalid output (writes a .tmp.invalid sibling so you can recover) |
prompts delete <name> [--force] | Delete a prompt; respects agent-modification: false unless --force |
prompts validate | Validate every file under prompts/; exits non-zero on any failure |
Agent tools
The chat and worker agents have the same CRUD surface:
prompt_list— list all prompts with metadata and per-filevalidflag.prompt_read— read a prompt; returns parsedtitle/loading/agent_modification.prompt_create— create a new prompt (frontmatter assembled from arguments, validated before commit).prompt_edit— apply git-style line-range patches via the shared hunk patch format. Refuses files markedagent-modification: falseand rolls back any patch that would clear that flag. Atomic-write-via-rename with mtime guard.prompt_delete— remove a prompt. Files markedagent-modification: falseare protected. A malformed prompt can still be deleted (so the agent can clean up after itself).
A context_update interaction is logged to the current thread on every successful edit / create / delete, so you can audit every change the agent makes to its own priors.
capabilities.md — auto-generated tool inventory
capabilities.md is otherwise just a normal prompt — same schema, same loader. The only thing special about it is that botholomew capabilities and the capabilities_refresh agent tool know how to regenerate the body by scanning the built-in tool registry and any configured MCPX servers.
The body is a thematic summary of what the agent can do — built-in capabilities grouped into coarse themes (task management, files, search, threads, …) and one theme per external service reachable through MCPX. Specific tool names are intentionally omitted from the rendered file; the agent uses mcp_list_tools, mcp_search, or mcp_info to look up exact names when it actually needs to invoke a tool. This keeps the always-loaded context small.
Summarization uses Claude (the chunker_model from config) on every refresh. When no Anthropic API key is configured, a static fallback listing is rendered with internal themes + MCPX server names and tool counts.
init seeds it with the built-in tools already populated. Regenerate any time via:
botholomew capabilities— CLI refresh (honors--no-mcp)capabilities_refresh— agent tool, useful when the agent suspects the inventory has drifted (new MCPX servers added, tools renamed, file deleted)/capabilities— the matching slash command in chat
Frontmatter (including title: Capabilities) is preserved on regeneration.
What this actually looks like
A typical beliefs.md after a few weeks of use:
---
title: Beliefs
loading: always
agent-modification: true
---
# Beliefs
- Evan prefers bullet-point summaries over paragraphs.
- The "Q4 planning" doc in /notes is the canonical source for revenue targets.
- The worker should escalate to a "waiting" status if a task needs access
to a tool that isn't configured, instead of failing outright.
- When summarizing email, strip quoted replies — they add tokens without
value.None of those bullets were in the seed template — they accumulated as the agent worked tasks and the chat user confirmed them. That's the whole point: the agent gets smarter about your workflow over time, and you can read (and edit) exactly what it believes.
Why not put this all in a vector store?
Prompt files are high-priority, always-loaded text — they're what the agent uses to decide what to do, not raw reference material. Burying them in a vector index means the agent might not retrieve them when it matters. Keeping them as flat markdown with a hard always flag makes them impossible to miss.
Long-form reference material (ingested PDFs, web pages, meeting notes) lives in the context & embeddings system instead. The two are complementary:
- Prompts (
prompts/) = how the agent thinks. context/+ the search index = what the agent knows.
Adding your own
The CLI is the easiest path:
botholomew prompts create deploy-checklist \
--title "Deploy checklist" \
--loading contextual \
--from-file ./checklist.mdOr write the file directly with any editor. Tasks mentioning "deploy", "release", or "version" — and chat messages mentioning the same — will now include this file in the system prompt automatically. You didn't have to register it anywhere. On every tick the worker reads every .md file in prompts/, extracts words longer than three characters from the current task's name and description, and includes any loading: contextual file whose content contains at least one of those words. The chat agent does the same on every turn, using your most recent message as the keyword source. See loadPersistentContext() in src/worker/prompt.ts for the exact logic.