Agents & init

Agents don't browse npm. They use what's already in front of them — in the repo's files, in node_modules, in the environment. So that's where Storyteller puts itself.

One command#

Installs the package, writes a configured storyteller, and adds the guidance block below to your AGENTS.md or CLAUDE.md. Run it as often as you like; it never overwrites something you've edited.

terminal
$ npx @lovelaces-io/storyteller init
  added    src/storyteller.ts
  added    AGENTS.md (created)

$ npx @lovelaces-io/storyteller init          # again — nothing to do
  kept     src/storyteller.ts (already exists)
  kept     AGENTS.md (already up to date)

Teach every agent in the repo#

This block is what init appends. Paste it yourself if you'd rather. Every agent that works in the repo reads it, so it spreads by being copied between projects — which is exactly the point.

AGENTS.md
## Logging with Storyteller

This project uses [Storyteller](https://github.com/lovelaces-io/storyteller). Narrate multi-step work as you do it, then finish once.

```ts
import { useStoryteller } from "@lovelaces-io/storyteller";

const story = useStoryteller({ origin: { who: "sync-job" } });

story.report("Fetching invoices", { what: { source: "stripe" } });
story.report("Rate limited, backing off", { level: "warn" });
story.report(await response.json());

story.finish("Sync complete");
// on failure: story.finish("Sync failed", { level: "oops", error });
```

For nested work, open a chapter. Each becomes its own record, linked to the parent:

```ts
for (const account of accounts) {
  const chapter = story.chapter({ origin: { what: account.id } });
  chapter.report("Reconciling");
  chapter.finish(`Synced ${account.id}`);
}
```

Things that are easy to get wrong:

- **Hand it the object.** `report()` takes any value — errors, API responses, Maps, class instances — and structures it safely, including circular references. Never `JSON.stringify` first.
- **One storyteller per logical operation**, not one per step. Separate instances fragment the work into disconnected stories.
- **Order beats by `sequence`, not arrival time.** Audiences are async and a slow one lands late.
- **`.to()` is synchronous.** Call it immediately after `finish()`, never after an `await`.
- **Report before finishing.** `finish()` clears the notes; anything reported after belongs to the next story.

Set `STORYTELLER_NARRATION=live` to watch beats stream as they happen, or `STORYTELLER_FORMAT=ndjson` for one JSON object per line.

Under forty lines on purpose. A block people trim is a block that loses its warnings first.

What ships in the package#

After npm install, an agent working in the project finds these in node_modules/@lovelaces-io/storyteller/ — no fetching, no being told:

Output a program can read#

For machine consumption, one JSON object per line. Set it from the environment and change no code:

terminal
$ STORYTELLER_NARRATION=live STORYTELLER_FORMAT=ndjson node sync.js \
    | jq -r 'select(.kind=="note") | "\(.sequence)  \(.level)  \(.note)"'
0  Information  Reading config
1  Information  Fetched invoices
2  Warning      Rate limited

Each line parses on its own. storyId groups beats with their story; sequence orders them.

Reading stories back#

Writing is half of it. The other half — an agent, tomorrow, answering "why did last night's sync fail?" from the stories another agent wrote — is in progress: a storage contract, a query vocabulary, and an MCP server. It is not shipped yet. Follow along on the storage and read-back epic (#41).

Or just ask one#

Hand the question to an agent and let it read the docs for you.