From 275ec8531d899cff020abbf451503fed548bb1fc Mon Sep 17 00:00:00 2001 From: hypercross Date: Sun, 13 Sep 2026 16:41:26 +0800 Subject: [PATCH] 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. --- AGENTS.md | 185 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 54 +++++++++++++++ docs/games.md | 115 ++++++++++++++++++++++++++++++ docs/oracle.md | 81 ++++++++++++++++++++++ 4 files changed, 435 insertions(+) create mode 100644 AGENTS.md create mode 100644 README.md create mode 100644 docs/games.md create mode 100644 docs/oracle.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9a1b1af --- /dev/null +++ b/AGENTS.md @@ -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// 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// 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/.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 ` 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 ` 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//characters/.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 [--campaign ] [--game ] [--tag ] [--count ] +journal oracle [] [--complicate] [--campaign ] [--game ] [--table ] +journal dice +journal tables [--campaign ] [--game ] +journal check [--campaign ] [--game ] +journal new-campaign [--game ] +journal games +journal game [] +journal [...] provided by the campaign's game package +``` + +`journal games` lists the installed games; `journal game ` 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//` following + `docs/games.md` (copy `games/generic/` as a starting point). +2. `journal new-campaign --game ` +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/`. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e9041e0 --- /dev/null +++ b/README.md @@ -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// a game: rules, tables, characters, scripts +campaigns// 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. diff --git a/docs/games.md b/docs/games.md new file mode 100644 index 0000000..3655a47 --- /dev/null +++ b/docs/games.md @@ -0,0 +1,115 @@ +# Game packages + +Each game is a self-contained package under `games//`. 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// + package.json @solo/game-, 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 ` 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: `. + +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 ` | +| Character backgrounds | `characters/` | game commands, or the agent | +| Game tooling | `index.js`, `scripts/` | `journal `, `node` | + +## Table lookup + +`journal roll ` 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 ` 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-`, 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
--game ` and each command with +`journal --game `. + +``` +journal check --game my-game +journal game my-game +``` + +## Creating a game + +1. Copy `games/generic/` to `games//`. +2. Rename the package to `@solo/game-` and set `id` in `index.js`. +3. Replace `game.md`, the tables, and `characters/` with the game's content. +4. Run `journal check --game `, `journal games`, and `journal game `. +5. Start a campaign with `journal new-campaign --game `. diff --git a/docs/oracle.md b/docs/oracle.md new file mode 100644 index 0000000..d89ae49 --- /dev/null +++ b/docs/oracle.md @@ -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 ` searches: + +1. `campaigns//tables/.csv` +2. `games//tables/.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 +```