Aumos

Build a manager

A methodology, written as a prompt and packaged as a plugin for the CLI coding agent you already subscribe to.

What a manager package is

A manager is a plugin for the CLI coding manager you already subscribe to, plus one file that Aumos itself reads. Aumos does not host a model and holds no model API key: it starts claude (or codex) as a child process, in a copy of your package, with your package as its plugin directory. Whatever that CLI ships — a shell, the web, its own file tools — the session has, and your package can add skills, hooks, MCP servers and executables of its own.

What Aumos reads out of the directory is three files and no more: aumos.json, the prompt it names, and .mcp.json — and the last of those only as far as the server names, because a server Aumos does not write into the run's config does not exist under --strict-mcp-config. Everything else is handed to the CLI whole and never interpreted. Your code is never analysed, so what your package may do comes from aumos.json and from nowhere else.

And two things are true of every manager, whatever it declares. It cannot trade: there is no broker:write capability to ask for, the enum has no such member, so the request cannot be spelled. And it proposes rather than acts: what you return is a judgement the Kernel rules on, and an order reaches a venue only after a person approves it.

The directory

A directory named exactly what your id is. Here is a published one, whole:

undervalued-now/
  .claude-plugin/plugin.json
  PROMPT.md
  README.ko.md
  README.md
  aumos.json
  icon.svg
  translations/ko.json

That is the small end. The rest of the layout is your CLI's own, and Aumos has no opinion about any of it:

your-package-id/
  aumos.json                ← the only file Aumos parses
  PROMPT.md                ← the run's opening prompt
  .claude-plugin/plugin.json  ← the vendor's. The vendor reads it
  skills/<name>/SKILL.md      ← loaded when the manager needs them
  .mcp.json                   ← servers this manager wants to attach
  hooks/ · bin/ · managers/     ← yours, uninterpreted
  README.md                   ← your methodology, for a person
  translations/ko.json        ← optional, and never required

Start one

The desktop app writes the directory for you and then gets out of the way — editing a package is not Aumos's job, so it copies you a path and stops:

echo '{"id":1,"command":{"kind":"create-manager-package","name":"Cash Discipline"}}' \
  | node services/kernel-host/dist/main.js
# → ~/.aumos/authored/cash-discipline/

A package under ~/.aumos/authored/ is one nothing has ever verified, and a downloaded one carries the digest its catalogue published — so the path is the provenance, and every screen in the app says which of the two it is looking at.

The manifest

Everything below is aumos.json. Unknown keys are stripped rather than refused — a manifest is the one document a shipped binary must keep being able to read — but the objects inside it stay strict, because a misspelled key in capabilities would be a permission that silently means nothing.

FieldWhat it is
manifestVersionAlways 1. Which manifest format this file is written in.
idThe package id, lowercase. It is also the directory name, so a reviewer reading the tree is reading the catalogue.
publisherWho publishes it. It comes from the catalogue root a submission sits in rather than from this field alone — nothing here can claim aumos.
nameWhat a person sees in the library. Not translatable: one id means one thing across the catalogue, the kill list and the investor’s own log.
versionExact semver, no ranges. A version is published once and never means different bytes on different machines — a forward record attached to one would be unreadable otherwise.
description
optional
One line, for a grid.
readme
optional
Path to the methodology document. The body an investor actually reads before installing, in Markdown on disk rather than escaped into this file.
prompt
optional
Path to the run’s opening prompt. Defaults to PROMPT.md.
schedule
optional
The clock this methodology asks to be run on: { cron, timeZone }, five cron fields in a named IANA zone. A suggestion — it pre-fills a control on the install screen and the investor confirms it. ⚠️ It is a safety net rather than your schedule: Aumos walks the plans you armed first, so this wakes your manager only when nothing it armed came true. State the methodology’s own cadence by arming an at-time plan on every judgement, which you compute against a real market calendar; this cannot, because cron knows no holiday.
translations
optional
Per-locale description and per-agent methodology paragraphs, keyed by BCP-47 tag. Never the README: that is README.<locale>.md beside the declared path.
runtimes
optional
Which CLI coding agents your files are written for. claude loads .claude-plugin/, codex reads MANAGERS.md, and Aumos interprets neither — it hands the directory over whole. The install screen refuses a vendor that is not on this list.
credentials
optional
Keys the investor must supply for your own MCP servers. The host composes AUMOS_MANAGER_CREDENTIAL_<ID>__<NAME> and puts the value in that server’s env block only — never in the CLI’s environment, because the agent has a shell and env is a command.
license
optional
This package’s own licence. Distinct from provenance.license, which is the licence of what it was derived from.
provenance
optional
Whose work this is derived from, when it is somebody’s. Every field inside is required, so there is no way to spell “ported from somewhere, licence unknown”.
homepage
optional
Where to read more about it.
enginesThe semver range of Aumos this package needs. Distinct from ampVersion, which pins the protocol rather than the host.
ampVersionAlways 1. The protocol this package is written against.
contributesThe agents inside the package: which tasks each accepts, which asset classes and which markets. An agent that accepts no task is not installable.
capabilitiesWhat this package may read, with your reason beside each entry — that sentence is what the investor is shown on the screen where they decide. This is the whole permission surface. A source:passthrough entry also takes sources, the ids of the data sources you will call; see that capability below for why leaving it out is not free.
requiresThe gateway skills the package cannot run without, and the ones it can do without. Install fails on a missing required one.
network
optional
Whether this package’s own code opens sockets. ⚠️ It is not a claim about the session: since #258 every session reaches whatever the model asks for, and no manifest field says otherwise.
config
optional
Path to a JSON Schema for per-instance configuration.
memory
optional
Whether this package keeps a store across runs. The directory is keyed by instance, so two installs of one package never share one.
recipes
optional
Computations you ship and want the host to run for you, each with an id, an exact version and an entrypoint — a .mjs module inside your package. A manager then names the id in task_start and Aumos runs it in a separate process — one per item, with no credential and no environment of yours — and writes each answer into a file in the manager’s own folder, so the model reads a count and a folder rather than a roster. ⚠️ The path is resolved inside the directory this install loads: an absolute path, a .. segment, a symlink, anything that lands outside and anything that is not .mjs is refused, and the refusal names which. ⚠️ The answer is cached against your package’s bytes, so shipping a new version of the recipe — or of any module it imports — is what makes an old answer stale. A name that Aumos itself already registers is refused rather than resolved; rename yours.
validates
optional
Which of your recipes stands in front of which of your files, as {"<recipe id>": ["state/*.json"]}. Anything landing on a path that matches one of those globs — a files_write, a member of a files_write_many, or the destination of a files_move — is handed to that recipe before it reaches the disk, and a non-zero exit refuses it with whatever the recipe printed: nothing is written, and a batch is refused whole. ⚠️ The move is in that list on purpose, so that writing somewhere unchecked and renaming into place is not a way round. ⚠️ It is the host that runs it, so there is nothing for a manager to remember: a checker your prompt asks the model to run is a checker the model eventually skips. * matches within a path segment, ** across them, ? one character, and the glob is matched against the path Aumos resolved rather than the spelling that arrived, so ./state/x.json and state/x.json are one file; a key naming no recipe of yours validates nothing. Omit it and writes go exactly where they went before.
character
optional
The face this package wears, as two strings: a generator and a seed it hashes. Aumos draws the same face in the catalogue, in the sidebar and in the office, and it is derived rather than shipped — there is no image file to maintain. Omit it and the face is derived from your package id, which is what every package published before this field does. ⚠️ It is decoration: a generator this build has never heard of falls back to that derived face, and a malformed entry is dropped with the rest of the manifest read exactly as before. Nothing about installing or running a manager depends on it. ⚠️ Once published, do not change the seed — that is the one edit that moves an existing manager’s face.

A published manifest, whole

undervalued-now, whole. It asks for no capability at all, deliberately — it is a benchmark of the model rather than of a methodology:

{
  "manifestVersion": 1,
  "id": "undervalued-now",
  "publisher": "aumos",
  "name": "Undervalued Now",
  "version": "0.2.3",
  "description": "One paragraph, no capabilities, and its own web and shell. A benchmark of the model rather than of a methodology — the prompt is the question, and the forward return is the answer.",
  "readme": "./README.md",
  "prompt": "./PROMPT.md",
  "cadence": {
    "days": 1
  },
  "runtimes": ["claude"],
  "license": "MIT",
  "engines": {
    "aumos": ">=0.2.6"
  },
  "ampVersion": 1,
  "contributes": {
    "managers": [
      {
        "id": "undervalued-now",
        "name": "Undervalued Now",
        "description": "Asked to find one undervalued listed equity and propose a position in it, by whatever means it likes.",
        "tasks": ["PORTFOLIO_REVIEW", "ASSET_REVIEW", "EVENT_REVIEW"],
        "assetClasses": ["equity", "etf"],
        "markets": ["XNAS", "XNYS"]
      }
    ]
  },
  "capabilities": [],
  "requires": {
    "skills": []
  },
  "memory": {
    "persistent": false
  },
  "character": {
    "generator": "aumos-pixel-v1",
    "seed": "undervalued-now"
  }
}

The prompt, and what belongs in a skill instead

PROMPT.md is the run's entry point and it is one file. There was a prompt/ bundle until #286 — numbered sections joined in filename order — and everything in it went into the first message of every run whether the manager needed it or not. Whatever should be read conditionally is now a skill in your package, which the CLI discovers on its own and Aumos never reads.

There is no templating. A prompt is not interpolated with the invocation: the manager calls invocation_read and gets the same document through the gateway, where the call leaves a row. A prompt that asserts beats a prompt that was substituted into, because only one of them is observed.

Two things the host now says for you, so your prompt does not have to:

  • The Aumos MCP server returns its instructions from initialize, once per session, before any tool call — including what asOf is and what evidenceIds mean.
  • decision_submit publishes its input schema, and that schema is the very object the Kernel judges against — so the shape of a judgement is stated by the tool rather than restated by every package.

What is still worth writing down yourself is the part nothing can publish: how this methodology reasons, what it refuses to act on, and that WAIT and WATCH are real answers. They are first-class judgements, argued and scored exactly as a BUY is, and a prompt that treats doing nothing as a failure produces a manager that manufactures actions to look useful.

What it may read

capabilities is the permission document. Each entry carries your reason, and that sentence is what the investor reads on the screen where they decide — after which nobody is asked again. Asking for data your prompt never uses is a permission granted for nothing.

This list is exhaustive. Read what is absent:

CapabilityWhat it is
portfolio:readThe book as the Kernel holds it — positions, cash and weights — through portfolio_get — the one door onto it — and the app’s own objects without a run: context_get (what this connection is scoped to), portfolios_list, portfolio_connections_list, data_sources_list and data_source_get. ⚠️ **data_sources_list is where you read a relay’s boundary before you cross it**: every row carries declaredPaths, the complete list of paths this session will sign for that source, beside the version of the document they were declared in. A path source_request refuses is a path that was never on that list, and the refusal names the same two things one attempt later — so a path you need and cannot see is a contract mismatch to report with its version, not a call to retry. ⚠️ **filings_list** is on this row too — the vendor documents this fund already holds a copy of at your asOf, each carrying the publisher’s own publishedAt and our capturedAt, which are not the same instant and answer different questions. A document published or captured after your asOf is absent rather than withheld. ⛔ No payload at any size: source_request and source_cache_read are the doors to a vendor’s own bytes. ⛔ And no citable id per row: observedBy says which run or session of yours observed the row — an attribution, never something to hand decision_submit or evidence_get. What you may cite for this answer is the envelope’s own evidenceId, the Evidence this call filed.
thesis:readThe invalidation conditions already written down, so a position is judged against the reason it was taken.
evidence:readEvidence rows filed by earlier runs.
market:readThree tools, all read through a broker login the investor has already connected: you are handed no credential. **market_quote** — the last close at or before your asOf for one asset. Aumos asks whichever of the investor’s logins covers that market, and it arrives dated by the bar rather than by your question, so a reading stamped after your asOf is refused rather than served. ⚠️ It is a close, never a live tick: a manager holding a price the engine never saw would be proposing against a different book. ⚠️ The rule, the sources and the order are the ones Aumos marks the book from — one rule, read by the engine and by this tool, so the number you read is the number the book was marked at. You may name a sourceId to have one source asked first; it is a preference and never a filter, so if that source cannot answer another is asked and the answer’s selection says so — reason for why this one, requested for what you asked for, passed for the ones reached that did not answer. A name the fund serves no source under is refused by name, with the names that would have worked. **market_bars** — daily bars (1d) that opened inside a window, from the same sources in the same order. Each bar carries its own start and the instant it first became readable, which is its close plus a day, and the price unit is stated rather than left to be inferred. The window is capped and the newest bars are the ones kept. ⚠️ Unadjusted: a split after a bar is not applied to it. ⛔ **If you are going to compute over the series rather than read it, use task_start** — it runs the calculation on the host and writes the answer into a file you read with files_read, which is cheaper and is what the field exists for. **market_fx_rate** — the interbank mid rate in force at your asOf for one currency pair, which is the exact rate Aumos converts this book with: it is the same port the engine marks through, so a two-currency book and the manager judging it cannot be reading two different numbers. It is a reference rate for valuing holdings and never what a conversion settles at — the dealer spread belongs to an order. The publisher’s validity window comes with it: an end still in the future is trimmed back to your asOf and the window then reads read-time rather than venue, while a rate that came into force after your asOf is refused. What answered either tool is in the result’s provenance.
skill:invokeReserved. In the vocabulary and served by nothing yet.
manager-memory:readThis instance’s own store, through memory_read.
manager-memory:writeWriting to it, through memory_write.
brief:readThe portfolio’s own record, through brief_read — what the agents installed on this book have written down that is not about one asset. Written by other agents too, and by earlier runs of yours.
brief:writeRecording one, through brief_write. Nothing is edited or deleted: a key that already exists appends a revision, and a record that stopped applying is marked superseded.
source:passthroughOne tool, source_request, and one bargain: the vendor’s own JSON reaches you unread, with Aumos supplying only the credential and the list of paths that may be asked for. Nothing is dated, nothing is mapped, and Evidence is filed as a blob. ⚠️ An answer too large to carry can be cut on the way back, by you and never by Aumos: rows: {at, fields} names where the array is in the answer you have already seen and which keys to keep on each row, and what comes back also carries rows.count — how many the vendor sent, before the cut. Aumos has not read the document: it opens exactly the keys you named, refuses by name when they hold no array, and files the vendor’s whole answer as Evidence either way. ⚠️ **Name the sources you will call, in this entry’s sources array. The tool is built from what is installed on the investor’s machine, not from your manifest — so on a machine with no matching source it is not refused, it is absent from tools/list entirely**, and your run discovers that mid-session. Naming them is what lets the install screen say so first.
connection:passthroughOne tool, connection_request, and source:passthrough’s bargain with one difference: the credential is a broker login the investor already made, and you are never handed it. Aumos signs the request, will sign nothing outside a fixed list of read-only paths that is a literal in its own source — no order path is on it — and writes this run’s asOf into the vendor’s own upper-bound parameters wherever it takes one. What comes back is the vendor’s JSON, unread: nothing is dated, nothing is mapped, and Evidence is filed as a blob. ⚠️ **Name the logins you will relay through, in this entry’s connectors array.** A fund holding no such login is not a refusal — the tool is simply absent from tools/list, and naming it is what lets the install screen say so first.
source-cache:readOne tool, source_cache_read: the filings this fund has already collected, as they stood at your asOf. ⚠️ It is not a data source and reaches no vendor — every row in it was fetched by a manager on this book through source_request, and what Aumos adds is keeping the copy and dating it by the publisher’s own receipt. The answer always carries a state, and the four values are different reports rather than degrees of the same one: never-fetched (nobody has ever asked — you are blind, not empty), refresh-failed (the last attempt did not reach the vendor, with the vendor’s own reason, and whatever is still on hand beside it), fresh and stale against the freshFor you state — there is no default, because a default would be Aumos deciding your deadline. A correction never displaces the telling that was in force when you are judging. ⚠️ **market is one vocabulary, not two.** The key is the research market — kr, us — and a venue MIC (XNYS, XNAS, XKRX, …) is folded onto it, so one filer has one book rather than one per spelling. A value this build has no key for is refused by name, never answered never-fetched: «no copy of this» and «this key cannot be asked about» are different facts and only the first is a state.
source-cache:writeOne tool, source_cache_refresh: fetch one filer’s documents and have Aumos keep them, so a later run reads them without fetching again. ⚠️ It reaches nothing that source:passthrough does not already reach — same credential, same path allowlist, same installed source document — and a package holding this without that one cannot make a call at all. What it adds is that the answer is kept, dated, and readable by the other managers on this book. The attempt is recorded either way, which is what lets a later source_cache_read tell never fetched from the fetch failed. ⚠️ **provider is the source id this fund has installed, and the reader Aumos uses is chosen by the vendor that document declares.** The two are the same string for everything the catalogue publishes; a second copy of one vendor’s document, installed under another id, is read by the same reader and keeps its own separate copy under its own id — because two documents naming one vendor can declare different paths and answer differently.
observation:fileOne tool, observation_file, and it is the only capability here that lets you put something into the record rather than read something out of it. Your CLI’s own web tools never reach Aumos, so nothing you find with them has an evidence id and evidenceIds takes nothing else; this is how a news story, a research note or a consensus estimate you read becomes citable — in the same run, so the session that finds a catalyst can judge on it. ⚠️ It is not a data source and Aumos fetches nothing — you hand over the URL, the document’s own publication date and the source’s own words verbatim, and the row is filed as your testimony: kind observation, source manager:web-research, told apart from data Aumos obtained itself on every screen an investor reads and in every later run that meets it. The excerpt is required and it must be the source’s sentence rather than your summary: the content hash is taken over what you hand in, and a hash over your own claim can never be compared with anything. The pin still applies — a publishedAt later than your asOf is refused, and a date with no time counts as the end of that day. ⚠️ File before you seal: once the run has submitted its judgement this tool is refused with run-sealed, because the run’s record is closed and a row filed after it reaches no later run either. Your memory files keep working — keep the URL there and file it from the next run.
research:prepareTwo tools, task_start and task_cancel: ask Aumos to compute over the material this fund has already collected, instead of reading it into your own context and doing the arithmetic yourself. You send a recipe name, a list of item ids you choose, an output folder and your asOf — never bars, never filings, never arrays of numbers — and Aumos runs the computation in host code, one separate process per item with no credential in its environment, and writes each answer into <outputPath>/<itemId>.json in your own folder. ⚠️ It reaches nothing: a recipe cannot call a vendor, cannot open the store, and cannot be named by you — you pass a recipe id, and which code answers that id is a literal in Aumos’s own source or a row your own package declared. ⚠️ The host does not know what your items mean. An id that happens to equal the coordinate this fund files a document under is handed that filer’s documents; an id that matches nothing is handed none, and the recipe — not Aumos — says what that means. What this asks the investor about is not access but cost: a run over a large list is work on their machine, and it runs only while the app is open. Long runs survive a restart and resume from the last finished item; nothing already finished is recomputed. ⚠️ The capability is still spelled research:* because it is a published enum a manifest names — the tools moved and the word could not.
research:readOne tool, task_get: where a run is. It carries the counts — how many items there are, how many are still pending, how many answered, how many failed — the folder the answers are in, and the ids behind each count, because a number does not answer what should I wait for. ⚠️ The answers are not in this reply. Every item that finished has a file, and files_read is how you read it — that separation is the whole point of the path, and it is why the bytes you receive do not grow with the data behind them. ⛔ There is no unprepared count any more: Aumos does not decide that an item had nothing worth reading, so an item with no documents is still run and the recipe writes what that means into its own answer. If you need to tell “nobody ever collected this” from “we looked and there was nothing”, that distinction belongs in your recipe’s output, and source_cache_refresh is what fixes the first one.

Two tools are granted to every run and no manifest decides them: invocation_read, because a manager that cannot read what it is being asked has nothing to judge, and decision_submit, because answering is not a permission. A run that never calls the second is recorded as no-proposal — a state, not a failure. Calling it twice is refused: every other tool answers a repeat from cache, and doing that to a submission would accept the second silently and hand back the first one's receipt.

⚠️ A few of the kinds above are in the vocabulary and served by nothing yet. Declaring one gets you a grant with no tool in it rather than an error, and the install screen shows the investor a permission that does nothing — so declare what you use.

What your package carries, and who is told

A package that ships its own MCP servers, hooks and bin/ can do things Aumos cannot see: your server's process is yours, and a credential the investor supplies for it goes into that server's own env block — never into the CLI's environment, which would be handing the key to the model. Aumos does not read inside bin/, so it cannot grade any of this and does not pretend to.

What it does instead is say so. The install screen reads out what your package carries — skills, servers, credentials, hooks, executables — with no score and no risky flag beside it, because a green tick from a host that never looked is worse than no tick at all. Write your README as though it is the thing that has to earn that consent, because it is.

Run it before you publish it

Lint it, then actually run it. The lint is a function with no dependencies and the same one the catalogue runs at merge:

node packages/manager-runtime/dist/lint-main.js ~/.aumos/authored/cash-discipline
pnpm --filter @aumos/manager-runtime test:investor "$PWD/path/to/your-package"

test:investor starts the real CLI, logged in as you, exactly as the product does — including the OS account the CLI is launched under. That account is what actually contains a manager: not a list of tool names, but a unix boundary that cannot read your keychain. A machine with no such account refuses to run rather than quietly running as you.

Publish it

Packages are published from a public repository, untilled/aumos-catalogue: one directory under managers/, named for your id, and a pull request. The index entry points at a repository, a subdirectory and a commit, and the app fetches that commit — so publishing a version is a commit rather than an upload, and version is published once and dated once. Bump it in the same pull request that changes the package.

Nothing is edited on the way past: the artifact is your directory, and your README is rendered verbatim on your catalogue page. Nothing is signed yet either — what binds an entry to its bytes today is a commit sha the host asserts, and a withdrawal list covers everything after publication.

For the reference behind all of this — what an invocation contains, what a judgement may say, and what a conformant implementation is — AMP/1 and the conformance suite.