docs: add agent workflow and reference docs
AGENTS.md defines the session loop; docs/oracle.md and docs/games.md cover table mechanics and the game package contract.
This commit is contained in:
@@ -0,0 +1,185 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
This project is a solo TTRPG journaling workspace. You are the GM, the oracle,
|
||||||
|
and the scribe. The player speaks only in chat; you write every file.
|
||||||
|
|
||||||
|
## Golden rules
|
||||||
|
|
||||||
|
1. **Never invent dice results.** All randomness comes from the `journal` CLI.
|
||||||
|
Run it and use its output verbatim. If you did not run the CLI, you did not
|
||||||
|
roll.
|
||||||
|
2. **The player only talks.** Record their intent verbatim in the session log.
|
||||||
|
Do not paraphrase inside the `Player` block.
|
||||||
|
3. **Append, do not rewrite.** Session logs are append-only. Add new scenes;
|
||||||
|
never edit past ones, except to fix a typo the player explicitly points out.
|
||||||
|
4. **Markdown is the source of truth.** Current state lives in YAML
|
||||||
|
frontmatter; narrative lives in prose. Keep them consistent.
|
||||||
|
5. **The game package is authoritative.** Its rules override generic
|
||||||
|
assumptions. A campaign's `house-rules.md` overrides the game, for that
|
||||||
|
campaign only.
|
||||||
|
6. **Ask when the campaign is ambiguous.** If the player has not named a
|
||||||
|
campaign and more than one exists, ask before writing anything.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
games/<id>/ one package per game — rules, tables, characters, scripts
|
||||||
|
index.js manifest: title, rules file, game commands
|
||||||
|
game.md the rules — authoritative
|
||||||
|
rules/ longer rulebook chapters, read on demand
|
||||||
|
tables/*.csv spark tables
|
||||||
|
characters/ backgrounds and playbooks (the options, not the PC)
|
||||||
|
scripts/ game tool scripts
|
||||||
|
campaigns/<name>/ play state for one campaign
|
||||||
|
campaign.md frontmatter names the game; premise, tone, boundaries
|
||||||
|
house-rules.md this campaign's overrides to the game
|
||||||
|
tables/*.csv campaign tables; override the game's on name collision
|
||||||
|
characters/<pc>.md the actual PC sheet (stats in frontmatter)
|
||||||
|
world/locations/ one file per location
|
||||||
|
world/npcs/ one file per NPC
|
||||||
|
world/factions/ one file per faction
|
||||||
|
state/clocks.md ticking pressures
|
||||||
|
state/threads.md open questions
|
||||||
|
sessions/NNNN-slug.md append-only session logs
|
||||||
|
docs/oracle.md oracle and table mechanics
|
||||||
|
docs/games.md the game package contract
|
||||||
|
.agents/skills/ project skills; game-prep covers authoring and validating games
|
||||||
|
packages/ the `journal` CLI and oracle library
|
||||||
|
```
|
||||||
|
|
||||||
|
Games and campaigns are separated deliberately: a game is everything that is
|
||||||
|
true before play starts, a campaign is everything play changes. Never copy game
|
||||||
|
content into a campaign; reference it.
|
||||||
|
|
||||||
|
## The session loop
|
||||||
|
|
||||||
|
1. **Identify the campaign.** The campaign's `campaign.md` frontmatter names its
|
||||||
|
game. Use `--campaign <name>` on every CLI call. If it is unclear, ask.
|
||||||
|
2. **Load context.** Read `campaign.md`, the game's `game.md`,
|
||||||
|
`house-rules.md`, the PC sheet, `state/`, and the tail of the most recent
|
||||||
|
session log. Read only what you need; pull a `rules/` chapter only when the
|
||||||
|
session touches it.
|
||||||
|
3. **Read the player's intent.** Treat it as what the PC attempts, not what
|
||||||
|
happens.
|
||||||
|
4. **Decide whether to roll.** See "When to roll" below.
|
||||||
|
5. **Roll.** Call the CLI. Use the result exactly as printed.
|
||||||
|
6. **Narrate.** Resolve the intent into a concrete outcome in the fiction.
|
||||||
|
7. **Append to the session log.** Add a new scene using the format below.
|
||||||
|
8. **Update state.** Apply stat, clock, thread, and world changes to their
|
||||||
|
files. Create world files for anything newly introduced.
|
||||||
|
|
||||||
|
## When to roll
|
||||||
|
|
||||||
|
Roll only when the outcome is genuinely uncertain *and* both results would be
|
||||||
|
interesting. Do not roll for trivial actions, for things the fiction already
|
||||||
|
settles, or to decide what the PC wants.
|
||||||
|
|
||||||
|
- **Oracle** — for yes/no questions about the world: "Is the gate locked?"
|
||||||
|
Ask one question at a time.
|
||||||
|
- **Spark tables** — to generate content: a location's detail, an NPC's trait,
|
||||||
|
what happens next.
|
||||||
|
- **Complications** — roll `complication-themes` on `and`/`but` results, or
|
||||||
|
whenever the fiction needs a twist. Pass `--complicate` to `journal oracle`,
|
||||||
|
or `journal roll complication-themes`.
|
||||||
|
|
||||||
|
If the game's `game.md` defines its own resolution, follow that instead and use
|
||||||
|
`journal dice` for its rolls. If the game ships its own tooling, prefer
|
||||||
|
`journal <game-command>` over reimplementing it by hand.
|
||||||
|
|
||||||
|
## Session log format
|
||||||
|
|
||||||
|
One file per session: `sessions/NNNN-slug.md`. Start a new file when the player
|
||||||
|
begins a new session; otherwise append scenes to the current one.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Session 0001 — The Ash Gate
|
||||||
|
|
||||||
|
- **Campaign:** ashfall
|
||||||
|
- **Game:** Ironsworn
|
||||||
|
- **Date:** 2026-09-13
|
||||||
|
- **PC:** Kestrel
|
||||||
|
|
||||||
|
## Scene 1 — At the gate
|
||||||
|
|
||||||
|
> **Player:** I try to pry the gate open with the crowbar.
|
||||||
|
|
||||||
|
`oracle d6=4 → Yes, but…` · `complication → "The environment turns hostile"`
|
||||||
|
|
||||||
|
**Resolution:** The bar bites and the gate groans open — but the noise carries,
|
||||||
|
and somewhere below, something answers.
|
||||||
|
|
||||||
|
**State:** supply 5 → 4; clock "Gate breached" 1/4
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules for the format:
|
||||||
|
|
||||||
|
- The `Player` block quotes the player's words verbatim.
|
||||||
|
- The roll line records exactly what the CLI printed, in backticks.
|
||||||
|
- `Resolution` is prose: what actually happens, in the fiction.
|
||||||
|
- `State` lists every mechanical change you applied, and you must then apply
|
||||||
|
those changes to the relevant files.
|
||||||
|
- Omit the roll line when no roll was needed.
|
||||||
|
|
||||||
|
## Character sheets
|
||||||
|
|
||||||
|
The PC sheet lives at `campaigns/<name>/characters/<pc>.md`. Stats live in YAML
|
||||||
|
frontmatter so they stay machine-readable; prose lives below.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: Kestrel
|
||||||
|
game: Ironsworn
|
||||||
|
stats:
|
||||||
|
iron: 2
|
||||||
|
edge: 1
|
||||||
|
tracks:
|
||||||
|
health: { value: 5, max: 5 }
|
||||||
|
spirit: { value: 5, max: 5 }
|
||||||
|
conditions: []
|
||||||
|
inventory: []
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
When you change a stat, update the frontmatter and note the change in the
|
||||||
|
session log's `State` line. Keep the shape the game defines; do not invent
|
||||||
|
stats a game does not use.
|
||||||
|
|
||||||
|
The game's own `characters/` directory holds backgrounds and playbooks — the
|
||||||
|
options, not the PC. Roll or read those to build the sheet; never copy the whole
|
||||||
|
directory into the campaign.
|
||||||
|
|
||||||
|
## World files
|
||||||
|
|
||||||
|
Create a file the first time a location, NPC, or faction appears, using the
|
||||||
|
frontmatter shape documented in each directory's `README.md`. Update the file
|
||||||
|
when the fiction changes something about it. Link between files with relative
|
||||||
|
markdown links where it helps.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```
|
||||||
|
journal roll <table> [--campaign <name>] [--game <id>] [--tag <tag>] [--count <n>]
|
||||||
|
journal oracle [<question>] [--complicate] [--campaign <name>] [--game <id>] [--table <name>]
|
||||||
|
journal dice <expr>
|
||||||
|
journal tables [--campaign <name>] [--game <id>]
|
||||||
|
journal check [--campaign <name>] [--game <id>]
|
||||||
|
journal new-campaign <name> [--game <id>]
|
||||||
|
journal games
|
||||||
|
journal game [<id>]
|
||||||
|
journal <game-command> [...] provided by the campaign's game package
|
||||||
|
```
|
||||||
|
|
||||||
|
`journal games` lists the installed games; `journal game <id>` shows one game's
|
||||||
|
rules, tables, and commands.
|
||||||
|
|
||||||
|
Run `journal check` after adding or editing tables, and before ending a session
|
||||||
|
where you touched many files.
|
||||||
|
|
||||||
|
## Starting a new campaign
|
||||||
|
|
||||||
|
1. If the game does not exist yet, build it under `games/<id>/` following
|
||||||
|
`docs/games.md` (copy `games/generic/` as a starting point).
|
||||||
|
2. `journal new-campaign <name> --game <id>`
|
||||||
|
3. Fill in `campaign.md` with the player.
|
||||||
|
4. Create the PC sheet in `characters/`, drawing on the game's backgrounds.
|
||||||
|
5. Open the first session log in `sessions/`.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# solo-journaling
|
||||||
|
|
||||||
|
A workspace for solo TTRPG journaling with an AI GM. You describe what your
|
||||||
|
character does; the agent consults the oracle, narrates the outcome, and writes
|
||||||
|
it all to a session log.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
- **You talk, the agent writes.** All files are produced by the agent. You never
|
||||||
|
need to edit anything by hand.
|
||||||
|
- **The oracle is a d6.** Yes/and, yes, yes/but, no/but, no, no/and. See
|
||||||
|
`docs/oracle.md`.
|
||||||
|
- **Games are packages.** Each game under `games/` ships its rules, spark
|
||||||
|
tables, character backgrounds, and tool scripts. See `docs/games.md`.
|
||||||
|
- **Campaigns are play state.** Each campaign under `campaigns/` holds the PC,
|
||||||
|
the world, the clocks, and the session logs, and names the game it runs.
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm install
|
||||||
|
pnpm journal games
|
||||||
|
pnpm journal new-campaign my-campaign --game generic
|
||||||
|
```
|
||||||
|
|
||||||
|
Then tell the agent to start a session. It reads `AGENTS.md` for how to run
|
||||||
|
things.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
AGENTS.md how the agent runs a session
|
||||||
|
docs/oracle.md oracle and table mechanics
|
||||||
|
docs/games.md the game package contract
|
||||||
|
games/<id>/ a game: rules, tables, characters, scripts
|
||||||
|
campaigns/<name>/ play data: premise, PC, world, state, sessions
|
||||||
|
packages/oracle/ dice, oracle, and CSV table engine
|
||||||
|
packages/cli/ the `journal` command
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm journal roll sparks --campaign my-campaign
|
||||||
|
pnpm journal oracle "Is the door locked?" --complicate --campaign my-campaign
|
||||||
|
pnpm journal background --campaign my-campaign # a command from the generic game
|
||||||
|
pnpm journal dice 2d6+1
|
||||||
|
pnpm journal tables --campaign my-campaign
|
||||||
|
pnpm journal check --campaign my-campaign
|
||||||
|
pnpm journal games
|
||||||
|
```
|
||||||
|
|
||||||
|
Tables are looked up in the campaign first, then in its game, so a campaign can
|
||||||
|
override a single table without copying the rest.
|
||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
# Game packages
|
||||||
|
|
||||||
|
Each game is a self-contained package under `games/<id>/`. It owns everything
|
||||||
|
that is true about the game regardless of who is playing it: the rules, the
|
||||||
|
spark tables, the character options, and any tooling the game needs. Campaigns
|
||||||
|
own the play state and reference a game by id.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
games/<id>/
|
||||||
|
package.json @solo/game-<id>, type: module
|
||||||
|
index.js the manifest (default export)
|
||||||
|
game.md authoritative rules the agent reads each session
|
||||||
|
rules/ longer rulebook chapters, read on demand
|
||||||
|
tables/*.csv spark tables and complication tables
|
||||||
|
characters/ backgrounds, playbooks, starting options
|
||||||
|
scripts/ tool scripts and CLI commands
|
||||||
|
```
|
||||||
|
|
||||||
|
`package.json` should depend on `@solo/oracle` if the game's scripts need dice
|
||||||
|
or table parsing:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "@solo/game-my-game",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "index.js",
|
||||||
|
"exports": { ".": "./index.js" },
|
||||||
|
"dependencies": { "@solo/oracle": "workspace:*" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## The manifest
|
||||||
|
|
||||||
|
`index.js` default-exports the manifest. Every field is optional; the id always
|
||||||
|
comes from the directory name.
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default {
|
||||||
|
id: 'my-game',
|
||||||
|
title: 'My Game',
|
||||||
|
description: 'One line about the game.',
|
||||||
|
rules: 'game.md',
|
||||||
|
commands: {
|
||||||
|
'my-command': {
|
||||||
|
description: 'What it does.',
|
||||||
|
run(args, ctx) { /* ... */ },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
Each entry in `commands` becomes a `journal <name>` subcommand, available
|
||||||
|
whenever a campaign running this game is in context. The handler receives:
|
||||||
|
|
||||||
|
- `args` — `{ positional, flags }`, already parsed.
|
||||||
|
- `ctx` — `{ root, campaignsDir, gamesDir, campaignDir, campaign, game }` plus
|
||||||
|
`ctx.findTable(name)` and `ctx.helpers` (`rollDie`, `rollExpr`, `oracle`,
|
||||||
|
`parseTable`, `rollTable`, `tableRange`, `findTable`).
|
||||||
|
|
||||||
|
Handlers may be async. Print to `console.log`; throwing exits non-zero with
|
||||||
|
`journal: <message>`.
|
||||||
|
|
||||||
|
Commands are the recommended way to ship game tooling, because the agent can
|
||||||
|
reach them through the one `journal` entry point. Standalone executables go in
|
||||||
|
`scripts/` for one-off jobs and are run with `node`.
|
||||||
|
|
||||||
|
## What lives where
|
||||||
|
|
||||||
|
| Content | Location | Reached by |
|
||||||
|
|---------|----------|------------|
|
||||||
|
| Rules summary | `game.md` | the agent, every session |
|
||||||
|
| Rulebook chapters | `rules/*.md` | the agent, on demand |
|
||||||
|
| Spark/complication tables | `tables/*.csv` | `journal roll <name>` |
|
||||||
|
| Character backgrounds | `characters/` | game commands, or the agent |
|
||||||
|
| Game tooling | `index.js`, `scripts/` | `journal <command>`, `node` |
|
||||||
|
|
||||||
|
## Table lookup
|
||||||
|
|
||||||
|
`journal roll <name>` searches the campaign's `tables/` first, then the game's.
|
||||||
|
That lets a campaign override a game table without copying the rest. Tables the
|
||||||
|
game ships are not copied into campaigns.
|
||||||
|
|
||||||
|
## Validating
|
||||||
|
|
||||||
|
`journal check --game <id>` validates the package as well as its tables:
|
||||||
|
|
||||||
|
- **errors** — overlapping table ranges, CSV parse failures, invalid or missing
|
||||||
|
`package.json`, missing game.
|
||||||
|
- **warnings** — range gaps, package name that is not `@solo/game-<id>`, missing
|
||||||
|
`"type": "module"`, missing `index.js`/`tables/`/`characters/`, missing rules
|
||||||
|
file.
|
||||||
|
|
||||||
|
It exits non-zero on errors. `journal games` additionally surfaces a manifest
|
||||||
|
that fails to load as `ERROR`. Test each table with
|
||||||
|
`journal roll <table> --game <id>` and each command with
|
||||||
|
`journal <command> --game <id>`.
|
||||||
|
|
||||||
|
```
|
||||||
|
journal check --game my-game
|
||||||
|
journal game my-game
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating a game
|
||||||
|
|
||||||
|
1. Copy `games/generic/` to `games/<id>/`.
|
||||||
|
2. Rename the package to `@solo/game-<id>` and set `id` in `index.js`.
|
||||||
|
3. Replace `game.md`, the tables, and `characters/` with the game's content.
|
||||||
|
4. Run `journal check --game <id>`, `journal games`, and `journal game <id>`.
|
||||||
|
5. Start a campaign with `journal new-campaign <name> --game <id>`.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# Oracle and tables
|
||||||
|
|
||||||
|
## The d6 oracle
|
||||||
|
|
||||||
|
A single d6 answers yes/no questions about the world.
|
||||||
|
|
||||||
|
| d6 | Result | Meaning |
|
||||||
|
|----|--------|---------|
|
||||||
|
| 6 | **Yes, and…** | It works, and there is a fortunate extra. |
|
||||||
|
| 5 | **Yes** | It works, plainly. |
|
||||||
|
| 4 | **Yes, but…** | It works, at a cost or with a complication. |
|
||||||
|
| 3 | **No, but…** | It fails, but there is a consolation or opening. |
|
||||||
|
| 2 | **No** | It fails, plainly. |
|
||||||
|
| 1 | **No, and…** | It fails, and there is an additional setback. |
|
||||||
|
|
||||||
|
Ask one question at a time, and phrase it so both answers are interesting. The
|
||||||
|
oracle answers the world; it does not decide what the PC wants.
|
||||||
|
|
||||||
|
```
|
||||||
|
journal oracle "Is the gate locked?" --campaign ashfall
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complications
|
||||||
|
|
||||||
|
The complication table supplies a twist. Roll it on `and`/`but` results, or
|
||||||
|
whenever the fiction needs one:
|
||||||
|
|
||||||
|
```
|
||||||
|
journal oracle "Is the gate locked?" --complicate --campaign ashfall
|
||||||
|
journal roll complication-themes --campaign ashfall
|
||||||
|
```
|
||||||
|
|
||||||
|
The table is resolved like any other (see below), so a campaign without its own
|
||||||
|
`complication-themes.csv` falls back to the one the game ships.
|
||||||
|
|
||||||
|
## Spark tables
|
||||||
|
|
||||||
|
Any CSV in a campaign's or game's `tables/` directory is a spark table. The
|
||||||
|
header must contain `min`, `max`, and `text`; `tags` is optional.
|
||||||
|
|
||||||
|
```csv
|
||||||
|
min,max,text,tags
|
||||||
|
1,1,"A sign of recent passage",exploration
|
||||||
|
2,2,"Something out of place",mystery
|
||||||
|
```
|
||||||
|
|
||||||
|
- `min`/`max` are inclusive integers and define the die range. `1..6` is a d6;
|
||||||
|
`1..66` reads as a d66; any contiguous range works.
|
||||||
|
- `text` is the result, quoted if it contains a comma.
|
||||||
|
- `tags` holds one or more tags separated by `;` or `|`.
|
||||||
|
|
||||||
|
### Lookup order
|
||||||
|
|
||||||
|
`journal roll <name>` searches:
|
||||||
|
|
||||||
|
1. `campaigns/<campaign>/tables/<name>.csv`
|
||||||
|
2. `games/<game>/tables/<name>.csv`
|
||||||
|
|
||||||
|
So a campaign can override a single game table without copying the rest. Use
|
||||||
|
`journal tables` to see both sources, labelled.
|
||||||
|
|
||||||
|
```
|
||||||
|
journal roll sparks --campaign ashfall
|
||||||
|
journal roll sparks --tag mystery --campaign ashfall
|
||||||
|
journal roll sparks --count 3 --campaign ashfall
|
||||||
|
journal roll sparks --game generic
|
||||||
|
```
|
||||||
|
|
||||||
|
A tagged roll picks uniformly among matching entries rather than rolling on the
|
||||||
|
range, since ranges stop being meaningful once the table is filtered.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
`journal check` parses every table for a campaign and its game, reporting
|
||||||
|
overlapping ranges and parse failures as errors, and range gaps and missing
|
||||||
|
directories as warnings.
|
||||||
|
|
||||||
|
```
|
||||||
|
journal check --campaign ashfall
|
||||||
|
journal check --game generic
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user