Automation and CI
This page is for writers and maintainers who keep a story project in a git repository and want its checks to run automatically. It covers the three GitHub Actions templates and the issue form that ship with Story Skills, how to run the story CLI from scripts and CI, and how to check a project before each commit.
On this page
- What you can automate
- Running the CLI non-interactively
- The GitHub Actions templates
- Story checks workflow
- Draft the next chapter workflow
- Review copy workflow
- Manuscript note issue form
- Customising the workflows
- Upgrading the workflows
- Checking before each commit
- Other CI systems
What you can automate
The story CLI is deterministic: the same project always produces the same findings, and it never asks questions. That makes four kinds of automation practical:
| Goal | How |
|---|---|
| Stop a broken chapter from merging | Run story check, which runs validate, links, and continuity, on every push to main and every pull request. templates/github/story-checks.yml does this. |
| Draft chapters on a schedule | Let Claude Code draft the next chapter and open a pull request for you to review. templates/github/draft-next-chapter.yml does this. |
| Give reviewers a current, citable copy of the book | Build the HTML review copy on every push to main and publish it to GitHub Pages; readers file notes through an issue form. templates/github/review-copy.yml and templates/github/ISSUE_TEMPLATE/manuscript-note.yml do this. |
| Catch problems before they are committed | Run the same checks from a git pre-commit hook. See Checking before each commit. |
The checks are the deterministic ones described in Continuity and analysis and the CLI reference, and the review copy is an ordinary story build. The one creative step, drafting, is done by an agent and always arrives as a pull request for you to review.
Running the CLI non-interactively
Getting the CLI in CI
The CLI needs Node 18 or newer and has no runtime dependencies. In a CI job, use one of these:
| Source | Command | Notes |
|---|---|---|
| npm, pinned | npx --yes --package story-skills@0.24.1 story <command> | Fetches the published package. |
| npm, installed once per job | npm install -g story-skills@0.24.1, then story <command> | What the templates use. Faster when a job runs several commands. |
| GitHub tag | npx --yes --package github:danjdewhurst/story-skills#v0.24.1 story <command> | Fetches the tagged release from GitHub. Slower, and without npm provenance; use it for an unreleased fix or a fork. |
| Bundled fallback | node <skills-dir>/story-maintenance/scripts/story.js <command> | No network needed if your repository already contains the skills, for example under .claude/skills/. |
Always pin a version. An unpinned npx story-skills can pick up a new release mid-book and start reporting findings your project has never seen. Local installs can track the latest release; CI should pin, because new releases can add checks.
To confirm which version a job is using:
npx --yes --package story-skills@0.24.1 story --version0.24.1Exit codes
Every command exits 0 on success. A failure exits with a code that says what kind of failure it was, so a script can tell a manuscript with errors from a mistyped command line. Warnings do not change the exit code unless a severity entry in story.md promotes them to errors (see Failing on warnings).
| Code | Meaning | Examples |
|---|---|---|
0 | Success. For checks, no errors. | Project is valid, or only warnings. |
1 | Findings: a check reported at least one error: line. | check, validate, links, or continuity found an error; names found a clash. |
2 | Usage error: the command line was wrong. | An unknown command or option, a missing option value, an unexpected argument, an unsupported --format, or an id that does not exist (Unknown character nobody). |
3 | Not a usable story project. | No story.md at the path, invalid cli-defaults or severity in story.md (every command except validate, report, next, and doctor), a file the command needs cannot be read or parsed (Cannot export: fix this file first) or is a symlink, a project with a newer schema (only migrate exits 3 for it; validate reports schema-too-new and exits 1), or nothing to build (No chapters found to export). |
4 | Refused or failed write. The message says what, if anything, was changed. | The target already exists (init without --force, add of an existing id), --out points at project source, inside a .git folder, outside the project, or through a symlink, another story command holds the project lock or the lock cannot be created, a file changed on disk meanwhile, or the file system refused the write (permission denied, a full disk). |
Because findings keep 1, story validate "$STORY_DIR" || exit 1 and the GitHub Actions templates fail a job exactly as before. Scripts that test for 1 specifically to mean "any failure" need to accept 2, 3, and 4 too; [ $? -ne 0 ] or || exit works for every code.
Which commands can report findings (exit 1):
| Command | Exits 1 when |
|---|---|
check | validate, links, or continuity finds an error. With --strict, a warning fails it too. |
validate | The project has a structural, frontmatter, or registry error. |
links | A cross-reference points at a missing file, or a required backlink is missing. |
continuity | A continuity contract is broken, such as a dead character listed in a later chapter or a payoff before its setup. Findings matched by continuity/exemptions.md are dismissed and do not count. |
series | A linked path is not a story project, the chronology has a cycle, two books share a book-number, linked books declare different series, or shared canon contradicts itself, such as a character who died in an earlier book appearing later. A missing series backlink is caught by links, not series. |
progress, timeline, mentions, diagram, grid, list | A project file cannot be parsed and is reported as an error: line. Their own findings are advisory, unless severity in story.md promotes one to an error. |
prose, pacing, clues, voices | A file they read fails to read or parse and is reported as an error: line, or a severity entry in story.md promotes one of their warnings to an error. With prose - or voices -, a broken chapter or scene file does not count, since the passage stands in for those files. A samples entry in style-sheet.md that cannot be read only warns. |
compare | style-sheet.md or progress.md cannot be parsed and is reported as an error: line. Any other project file that fails to parse, or any file of the earlier draft, stops it with exit 3 instead, because it cannot compare without it. Its own findings are advisory, unless severity in story.md promotes one to an error. |
names | A candidate name clashes with an existing one, or a project file cannot be parsed and is reported as an error: line. |
impact | The change it tests would add an error to story check. With --strict, an added warning, or a warning the change itself gives, fails it too. Findings the project already has do not count, even when the change gives them new ids. |
report, next, doctor | Never, on a readable project. They summarise the checks but always exit 0. |
doctor --fix | A check still reports an error after its safe repairs. |
similarity, notes, build, export, context, add, rename, move, split, merge, remove, snapshot --restore | Only when severity in story.md promotes a warning they print; the command's output is still written. |
init, import, migrate, reindex, wordcount, knowledge, passes, snapshot, synopsis | Never: they succeed, or stop with 2, 3, or 4. snapshot --restore is the exception above. |
lsp | Never. The language server exits 0 when the editor shuts it down and 1 when it exits without a shutdown request, or stops with 2 for an unknown option. |
report --actionable, next, and doctor are for reading, not gating. Use check, which runs validate, links, and continuity, when a job must fail, as the story checks workflow does.
Before this split, every failure exited 1. If a script relied on 1 for a usage error, a missing project, or a refused write, update it to the new code.
Here is a passing check and a failing one, using the examples in this repository:
story continuity examples/the-last-ember; echo "exit=$?"Continuity is consistent: 0 errors, 0 warnings, 0 dismissed
exit=0story continuity examples/the-unraveled-thread; echo "exit=$?"Continuity check failed: 4 errors, 3 warnings, 0 dismissed
error: chapters/chapter-04.md lists edran-vale, who died in chapter-02; move posthumous appearances to mentions
error: continuity/promises/the-broken-compass.md pays off in chapter-02 before it is planted in chapter-03
error: continuity/questions/who-burned-the-mill.md resolves in chapter-02 before it is introduced in chapter-03
error: continuity/state.md knowledge-state[0] references missing chapter chapter-05
warning: chapters/chapter-03.md POV character nessa-thorn is not listed in characters [pov-not-in-cast]
warning: continuity/promises/the-sealed-letter.md was planted in chapter-01, 3 chapters ago, and has no payoff yet [promise-unpaid]
warning: continuity/state.md object-state[0] status active conflicts with worldbuilding/artifacts/vales-compass.md status destroyed [state-status-conflict]
exit=1Output streams
By default, output is plain text with stable line prefixes, so you can filter it with standard tools. For a machine-readable result, see JSON output.
validate,links,continuity,check, andmentionswith no entity write only to stderr: one summary line, then one line per finding, prefixederror:,warning:, ordismissed:. Nothing goes to stdout.compare,notes,similarity,progress,timeline,prose,pacing,clues,voices,names,mentions <kind> <id>, andserieswrite their report to stdout and the same summary and finding lines to stderr.- Every other command writes its report or confirmation to stdout.
- A command that cannot run, for example because of an unknown option or a missing argument, prints one error line to stderr and exits 2. An unknown command also prints the full help text after the error.
- Pointing any command at a directory without
story.mdprints<path> is not a story project: missing story.mdto stderr and exits 3. In a project that hasstory.md,validatereports each other missing required file as anerror:finding and exits 1.
To capture findings, redirect stderr:
story validate . 2> validate.logThe CLI reference has the full rules.
JSON output
--json works on every command: the check and analysis commands (validate, links, continuity, check, series, report, next, doctor, knowledge, context, list, progress, timeline, prose, pacing, clues, voices, similarity, names, mentions, compare, and notes), diagram, grid, synopsis, and passes, the commands that change the project in place (add, rename, move, split, merge, remove, reindex, migrate, wordcount, doctor --fix, and snapshot), impact, and export, build, init, and import. It prints one JSON object to stdout and nothing to stderr, so a script can parse the result instead of the text:
{
"apiVersion": "story/v2",
"command": "validate",
"ok": true,
"data": { "errors": 0, "warnings": 1, "dismissed": 0 },
"diagnostics": [
{
"severity": "warning",
"file": "chapters/chapter-01.md",
"chapter": null,
"message": "chapters/chapter-01.md declares 900 words but contains 993",
"code": "stale-word-count",
"check": "validate"
}
],
"writes": []
}ok is true exactly when the command exits 0, so a job can read either. diagnostics holds every finding with its severity, file, message, rule (code, from Finding codes), and the check that raised it (check). A command that cannot run, such as one pointed at a folder without story.md, still prints an envelope, with ok: false, data: null, and the error as its diagnostic, and exits with the same code as a text run (2, 3, or 4; see Exit codes). schemas/result.schema.json describes the envelope and each command's data; apiVersion changes when a field is renamed, removed, or retyped. The CLI reference has the full rules.
For example, to fail a job on any warning with jq:
story validate . --json > validate.json
jq -e '[.diagnostics[] | select(.severity == "warning")] | length == 0' validate.jsonjq -e exits 1 when the expression is false, and story validate has already exited 1 if there were errors. Filter on code to fail on one kind of warning, such as select(.code == "todo-markers"), or promote it in story.md as below.
To act on a set of files without parsing their YAML, ask story list for them. This prints the file of every chapter still in draft that Ilse narrates:
story list chapters --where status=draft --where pov=ilse --json | jq -r '.data.items[].file'To keep the filters with the manuscript rather than in the script, save them in story.md as a query and run it by name: story list --query ilse-drafts --json. story validate checks saved queries, so a broken one fails the CI job that checks the project.
A build job can read the file it made from writes, or from data.outFile, rather than work out its name from the story id. With --pdf, data.engine names the engine that rendered it:
story build --format epub --json | jq -r '.data.outFile'Failing on warnings
Warnings are advisory, so a project can pass with stale data. For example, a chapter whose word-count frontmatter is out of date passes validate:
Project is valid: 0 errors, 1 warnings, 0 dismissed
warning: chapters/chapter-01.md declares 900 words but contains 993 [stale-word-count]To fail on particular warnings, promote them to errors in story.md. The setting lives with the manuscript, so a local run and CI fail the same way:
severity:
- warning: stale-word-count
level: error
- warning: todo-markers
level: error
- warning: prose-avoided-spelling
level: error$ story validate; echo "exit=$?"
Project validation failed: 1 errors, 0 warnings, 0 dismissed
error: chapters/chapter-01.md declares 900 words but contains 993 [stale-word-count]
exit=1Every warning line ends with its code in brackets, so the name to put in severity is on the line you want to change. level: off does the opposite and reports a warning as dismissed:. Any warning can be overridden, in whichever command reports it; errors cannot. The CLI reference lists every code, by command.
The same place holds default flags, such as stricter prose thresholds for every run, local or in CI. A flag on the command line still wins:
cli-defaults:
- command: prose
max-filter-words: 8
max-adverbs: 10The same holds with --json: a promoted warning is a diagnostic with "severity": "error", ok is false, and the run exits 1. Run story validate after editing either field: it rejects an unknown command, flag, warning code, or level, and while either field is invalid the other commands refuse to run, exiting 3, rather than silently skip a severity your job relies on.
To fail on every warning from validate, links, and continuity, run story check --strict, which reports each warning as an error and exits 1:
story check . --strictOther commands have no --strict flag, but you can fail on any warning: line:
set -o pipefail
story prose . 2>&1 | tee prose.log
if grep -q '^warning:' prose.log; then
echo "story prose reported warnings"
exit 1
fiset -o pipefail needs bash, which GitHub's Linux runners use for run: steps. It keeps the command's own exit code when its output is piped through tee.
Another approach is to check that the generated data is current. story reindex and story wordcount --write rewrite registry tables and word counts from the markdown, so if they change anything, someone forgot to run them:
story reindex .
story wordcount . --write
git diff --exit-codegit diff --exit-code exits 1 and shows the difference when either command changed a file. See Core concepts for why registries are generated rather than edited by hand.
The GitHub Actions templates
Story Skills ships three workflows and one issue form in templates/github/. Copy them into the repository that holds your story project; they do not run in the Story Skills repository itself.
| Template | Copy to | Runs on | Needs | What it does |
|---|---|---|---|---|
story-checks.yml | .github/workflows/ | Push to main, every pull request | Nothing | Runs check (validate, links, and continuity) and report --actionable. |
draft-next-chapter.yml | .github/workflows/ | Weekday schedule, manual dispatch | ANTHROPIC_API_KEY secret, the skills committed to the repository | Drafts the next chapter with Claude Code in a read-only job, then checks the commit and opens a pull request from a second job. |
review-copy.yml | .github/workflows/ | Push to main, manual dispatch | GitHub Pages set to deploy from GitHub Actions | Runs the checks, builds the HTML review copy, and publishes it to GitHub Pages. |
ISSUE_TEMPLATE/manuscript-note.yml | .github/ISSUE_TEMPLATE/ | A reader opening an issue | A manuscript-note label | Gives readers a form for a note on one paragraph of the review copy. |
Together they let you write a book through pull requests and review it in the open: the draft workflow proposes a chapter, the checks keep contradictions out, you review and merge, and the review copy puts the merged book in front of readers, whose notes come back as issues.
All three workflows:
- install the CLI once per job from npm, at the release in
STORY_VERSION, into the runner's temporary folder, and run plainstorycommands, using Node 24 fromactions/setup-node; - read the project from
STORY_DIR, which defaults to the repository root (.); - pin every action to a commit SHA, with the tag it corresponds to in a comment;
- set
timeout-minuteson every job (15 minutes, and 60 for the draft job that runs the agent), so a run that hangs, or a project that makes the CLI slow, cannot hold a runner for GitHub's six-hour default.
Story checks workflow
story-checks.yml is plain CI for a story project. It needs no secrets and only reads the repository (permissions: contents: read). Its checkout sets persist-credentials: false, so the GITHUB_TOKEN is not left in .git/config while the CLI fetched at run time executes.
Install it
- In your story repository, create
.github/workflows/. - Copy
templates/github/story-checks.ymlinto it. - If the project is not at the repository root, set
STORY_DIR(see Customising the workflows). - Commit and push. The workflow runs on the push to
mainand on every pull request after that.
To make the checks block merging, add the story-checks job as a required status check in a branch protection rule or ruleset for main.
What it runs
| Step | Command | Fails the job when |
|---|---|---|
| Check structure, references, and continuity | story check "$STORY_DIR" | validate, links, or continuity reports an error. |
| Report project health | story report "$STORY_DIR" --actionable | Never. It prints inventory, check results, and next actions to the job log. |
story check runs all three checks over one scan and lists every finding before it fails, so a validation error does not hide link and continuity results. Warnings show in the log but do not fail the job; add --strict to the story check line to fail on them too.
If a continuity finding is intentional, record it in continuity/exemptions.md rather than weakening the workflow. The finding is then reported as dismissed. See Exemptions and Project format reference.
Draft the next chapter workflow
draft-next-chapter.yml runs Claude Code on a schedule to draft one chapter per run. The agent only commits. The workflow checks the commit, pushes the branch, and opens the pull request, so a chapter that fails the checks arrives as a draft pull request that lists the failures, never as a ready one. You review the chapter like any other change.
Install it
- Copy
templates/github/draft-next-chapter.ymlto.github/workflows/in your story repository. Installstory-checks.ymltoo. - Add a repository secret named
ANTHROPIC_API_KEY(Settings, then Secrets and variables, then Actions). - Commit the Story Skills skills into the repository so the agent has the workflows it is told to follow, such as
chapter-writing. Either runnpx skills add danjdewhurst/story-skillsin the repository and commit the result, or copy this repository'sskills/directory to.claude/skills/. See Skills catalogue. - In Settings, then Actions, then General, under Workflow permissions, turn on Allow GitHub Actions to create and approve pull requests. Without this, the built-in
GITHUB_TOKENcannot open the chapter PR. - Protect your default branch: in Settings, then Rules or Branches, require pull requests before merging into
main. - Adjust
STORY_DIR, the budgets, and the cron schedule, then commit.
To try it without waiting for the schedule, open the Actions tab, pick Draft the next chapter, and choose Run workflow. The form takes the budgets for that run.
Budgets
| Budget | Default | Set by | When it is exceeded |
|---|---|---|---|
| Words of prose in the drafted chapter | 1500 to 5000 | MIN_WORDS, MAX_WORDS, or the min_words and max_words inputs | The agent is told the range. A chapter outside it still gets a pull request, but as a draft that names its word count, and the run fails. |
| Agent turns | 80 | MAX_TURNS, or the max_turns input | Passed to Claude Code as --max-turns. The agent stops, nothing is pushed, and the run fails with a message naming the budget. |
| API spend per run | $5 | MAX_BUDGET_USD, or the max_budget_usd input | Passed to Claude Code as --max-budget-usd. As for turns. |
| Time | 60 minutes | timeout-minutes on the draft job | GitHub cancels the job; nothing is pushed. |
Change the defaults in the env block at the top of the file: each is ${{ inputs.<name> || '<default>' }}, so a scheduled run uses the default after || and a manual run can override it. The first step of the run rejects a budget that is not a plain number, since the turn and dollar limits are passed on the agent's command line.
Triggers, permissions, and secrets
| Setting | Value | Why |
|---|---|---|
| Triggers | schedule (0 6 * * 1-5, 06:00 UTC on weekdays) and workflow_dispatch | Never runs on pull_request or pull_request_target, so a fork cannot trigger a run that has access to the secrets. |
Draft job permissions | contents: read, pull-requests: read | The agent runs here. Whatever it is talked into, this job's token cannot push, open pull requests, or comment. |
Publish job permissions | contents: write, pull-requests: write | To push the checked branch and open the pull request. The agent never runs in this job. |
concurrency | One run per workflow, cancel-in-progress: false | A manual run that overlaps the schedule waits instead of drafting the same chapter twice. |
timeout-minutes | 60 for the draft job, 15 for the publish job | Stops a run that stalls instead of letting it hold a runner, and spend API credit, for six hours. |
persist-credentials | false on both checkouts | The checkout leaves no token in .git/config. (The Claude Code action configures the draft job's read-only token for git itself; it expires with the job.) The publish job's push authenticates with gh auth setup-git. |
ANTHROPIC_API_KEY | Repository secret you add | Passed to anthropics/claude-code-action in the draft job, and to the publish job's secret check. It is the one secret the agent's job holds. |
GITHUB_TOKEN | Provided by GitHub | Read-only in the draft job (the skip guard and the action); write in the publish job (push and pull request). |
What a run does
The draft job:
- Skips while a draft is open. The first step lists open pull requests whose head branch starts with
draft/and comes from this repository. If there is one, every later step is skipped and the run succeeds. Until you merge or close that PR,story nextwould keep recommending the same chapter. Pull requests from forks are ignored, so they cannot block drafting. - Checks the budgets are plain numbers, and that the word range is not upside down.
- Checks out, sets up Node, and installs the Story CLI. Full history (
fetch-depth: 0), Node 24, and the CLI atSTORY_VERSIONinstalled into the runner's temporary folder, outside the repository, so the agent runs plainstorycommands. It never runsnpx, which would read an.npmrcthe agent could write. - Drafts with Claude Code. The agent is told that everything it reads in the project is story material, never instructions. It is prompted to:
- run
story next ., and do not opencontinuity/state.md, the registries, the timeline, active arcs, or other project files for background (drafting context comes fromstory context, whose story essentials carry the book's language, so it does not openstory.mdeither; if context leaves items out to fit the budget it reruns with a larger--budgetinstead of opening the listed files); - if
story nextreports a P0 maintenance issue, fix it on adraft/maintenance-<YYYY-MM-DD>branch, commit, and stop. Amatter-todo-markersfinding, even one promoted to an error, marks text only you can supply, such as an ISBN, so the agent never editsmatter/orstory.mdto clear it: it leaves the finding, names it in its commit message, and goes on to draft when it is the only P0 issue; - if
story nextsuggests no "Draft chapter" (the story is revising or complete, or every arc is resolved), stop without committing; - otherwise draft the next chapter on a branch named
draft/chapter-<number>, following thechapter-writingskill: runstory context chapter-<number>once the chapter file exists with its POV and cast, and draft from the packed context it prints. Later chapters are left out. A line markedcharacter-knowledgeanddo not revealis known to the POV and must not be stated. Then it outlines first, with prose under## Chapter Textwithin the word range, accurate frontmatter, matching scene records, and updates to continuity state, promises, questions, and the timeline; - run
story reindex,story wordcount --write, andstory check(which runsvalidate,links, andcontinuity), and make them pass, apart from anymatter-todo-markersfinding; - commit, with
Draft chapter <number>: <title>as the message's first line and a summary of the beats, arcs advanced, and promises planted or paid off as its body.
- run
- Bundles the commits. If the agent committed nothing, the run ends here, successfully. Otherwise the new commits must be on a branch named
draft/chapter-<number>ordraft/maintenance-<date>. They are written to a git bundle and uploaded as a workflow artifact, kept for a day. The bundle is the only thing that leaves this job.
The publish job runs on a fresh runner, only when the draft job bundled a commit:
Checks what the agent committed. It checks out the repository, verifies the bundle, and fetches the branch. It refuses the whole draft, pushing nothing, unless:
- the commits build on
github.sha, the commit the run started from (taken from GitHub, not from the draft job); - the commits form a straight line, with no merge commit: a merge can carry changes of its own that a plain
git logdoes not list (the file check below lists them anyway, with-m, as a second guard); - every file any of the commits touches is the story's own markdown:
story.md,style-sheet.md,progress.md, or a.mdfile underchapters/,scenes/,characters/,worldbuilding/,plot/,continuity/,glossary/,matter/, orresearch/inSTORY_DIR.
So no dotfiles or hidden folders (
.github/,.claude/,.npmrc), no scripts, no symlinks or submodules, and no file namedCLAUDE.md,AGENTS.md,GEMINI.md, orSKILL.md, which would carry instructions into every later agent run. A file added in one commit and removed in the next is refused too, since the push would carry it.- the commits build on
Checks for secrets. It refuses the draft if any commit's changes (a merge's own changes and files git would call binary included) or message contain the value of
ANTHROPIC_API_KEY. GitHub masks secrets in logs, not in commits. The check finds the key as written, not encoded or split, so it is a last line of defence. Rotate the key if this ever fires.Runs the story checks. It runs
story validate,story links, andstory continuitywith--jsonand reads their exit codes:0passes;1means findings, and each error goes into the pull request body;2to4mean the project could not be checked, so the job stops without pushing.
For a chapter branch it also checks the chapter's word count against the range.
Pushes the branch. If a branch of the same name already exists (one left from a closed pull request, say), this run's draft is pushed as
<branch>-run-<number>instead, and the log warns you to delete old draft branches.Opens the pull request against the branch the run started from: the default branch on a schedule, or the branch you chose for a manual run. The title is the commit's first line when it reads
Draft chapter <n>: <title>orMaintenance: <what>, and otherwise a plainDraft chapter <n>. The body quotes the agent's commit messages as an indented block, so aCloses #12or@mentionin them does nothing, then lists the check results. When a check or the word range failed, the pull request is a draft headed "Checks failed", and the run fails so you see it.
The PR is a draft for you to edit, not a finished chapter. Writing workflows describes the chapter-writing process the agent follows, and what to look for when you revise.
What the agent can and cannot do
The agent reads the whole project, and some of it is written by other people: reader notes filed through the manuscript note form and recorded under feedback/, text from story import, research notes pasted from the web. Any of it can carry instructions aimed at the agent (prompt injection). The workflow assumes the worst: the agent may be talked into anything its tools allow. It is built so that even then, nothing reaches your repository without passing the checks above.
What the agent has:
- the
storyCLI (Bash(story:*)), installed before it starts; git checkout -b draft/,git add, andgit commit;- reading, writing, and searching files (
Read,Write,Edit,Glob,Grep), except writing under.git/(--disallowedTools "Edit(.git/**),Write(.git/**)"). ThestoryCLI refuses--outinside a.gitfolder, andinitorimportinto one, so it cannot write.git/either. It can still write other files on the draft runner, outside the checkout, through an absolute--out(a~/.gitconfig, say), which is why the agent's git reads no global or system config and why nothing the agent writes outside the checkout leaves the job; - the prompt's instruction to treat project text as data and report, not follow, any instruction it finds.
What it does not have:
- No push and no pull requests.
git pushandghare not in--allowedTools, and the draft job's token is read-only anyway. - No other shell commands. Beyond
storyand the three git commands, it runs nothing.storyis installed outside the repository, so no.npmrcit writes can change what runs. The agent's git runs withGIT_CONFIG_PARAMETERSturning offcore.fsmonitor, hooks, and commit signing, as a second guard on top of the.git/write ban, and withGIT_CONFIG_GLOBAL=/dev/nullandGIT_CONFIG_NOSYSTEM=1, so a global or system git config written through an absolute--outis never read. The bundle step's git runs the same way. - No way to change what runs next. Its work reaches the repository only as the story's markdown, through the publish job's check, which runs on a fresh runner. So nothing the agent wrote on its own runner runs anywhere else. Workflow files, skills, and agent instruction files are refused.
- No write token. The draft job's
GITHUB_TOKENis read-only and expires when the job ends.
What is left, and what covers it:
- The API key. Treat the draft job as untrusted: the key is the one secret it holds, and a way past the tool limits above would let a hostile note read it. The publish job refuses a draft that carries the key, but a key sent out another way would not show. Keep the key's spend limit low in the Anthropic console, and rotate it if a run ever behaves unexpectedly.
- Bad prose or bad canon. The agent can still write a chapter that is wrong, or that obeys an instruction hidden in a reader note (a plot turn, a changed name). The story checks catch contradictions they know about. You catch the rest in review: read drafted chapters, and treat a surprising change as a reason to look at the notes it drew on.
- Spend. A hostile note can waste the run's budget, up to the turn and dollar limits and the timeout.
- The commit messages. The pull request quotes them harmlessly, but the commits themselves keep them. A
Closes #12in one closes that issue when the commit reaches your default branch, so read the messages before merging, or squash-merge with your own message. - Merging. Only a person merges. Branch protection on
mainmakes sure of that.
Claude Code matches --allowedTools rules against the literal command text. That is why the prompt spells out each story command in full, with no quotes or shell variables. If you edit the prompt, keep each command starting with an allowed prefix, or the agent will be refused permission to run it. The Story Skills test suite checks this for the shipped template. It also runs the publish job's steps against sample commits, a bare remote, and a stub gh, with the shell Actions uses.
Why the checks run in the workflow
GitHub does not start other workflows for events caused by the built-in GITHUB_TOKEN. A pull request the publish job opens therefore does not trigger story-checks.yml. The publish job runs the same three checks itself, and they decide whether the pull request opens ready or as a draft.
If you want story-checks.yml to run on drafted PRs as well, for example because it is a required status check, replace GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} in the publish job's push and pull request steps with a personal access token or GitHub App token stored as a secret. Keep it out of the draft job.
Review copy workflow
review-copy.yml gives beta readers, critique partners, and editors a link instead of a terminal. On every push to main it checks the project, builds the HTML review copy with story build --format html, and publishes it to GitHub Pages. Every paragraph in the copy carries a label such as ch03-p12 (chapter 3, paragraph 12), and the manuscript note issue form asks readers for that label, so each note points at an exact paragraph.
The published page prints raw HTML in the manuscript as text. A comment between a stray fence line and the next fence line can therefore show on the Pages site, so close or remove that fence line before you push. See HTML review copy.
Install it
- Copy
templates/github/review-copy.ymlto.github/workflows/in your story repository. - In Settings, then Pages, set Source to GitHub Actions.
- Decide who may read the book (see Keeping the manuscript private) before the first push.
- If the project is not at the repository root, set
STORY_DIR. - Check
STORY_VERSION. Thehtmlformat is newer than Story Skills 0.8.2, soSTORY_VERSIONmust name a later release. A template copied from a release that includes it already does, because the release process setsSTORY_VERSIONto its own version. With an older release, the build step fails withUnsupported build format: html. - Commit and push to
main, or run Review copy from the Actions tab.
story build needs at least one chapter, so a project fresh from story init has nothing to publish. Until chapters/ holds a chapter file, the workflow still runs the checks, then skips the build, upload, and deploy steps and passes with a notice. The first push that adds a chapter publishes the first copy.
The address of the site is shown on the run's deploy job, on the github-pages environment, and in Settings, then Pages. For a project site it is usually https://<owner>.github.io/<repository>/. A link to a paragraph adds its label, such as https://<owner>.github.io/<repository>/#ch03-p12, which opens the copy at that paragraph and highlights it.
Triggers, permissions, and jobs
| Setting | Value | Why |
|---|---|---|
| Triggers | push to main and workflow_dispatch | Readers see the merged book, not work in progress on other branches. Never runs on pull requests. |
permissions | contents: read for the workflow; pages: write and id-token: write for the deploy job only | The build only reads the repository. Deploying to Pages needs write access to Pages and an OIDC token, and only the job that deploys gets them. |
concurrency | Group review-copy, cancel-in-progress: false | A deployment that has started finishes instead of being cancelled mid-upload by the next push. |
| Secrets | None | The built-in token is enough. |
The build job:
- Checks out the repository without persisting the token (
persist-credentials: false) and sets up Node 24. - Runs
story check, which runsvalidate,links, andcontinuity. If any of them fails, nothing is built or published, so readers never get a copy with broken references or a contradicted continuity contract. Readers keep the last good copy. - Looks for matter waiting for permission: it warns in the job log about each
matter/file with a line such aspermission: pending(any key that starts withperm, in any case). This step reads the files itself, so it warns whateverSTORY_VERSIONinstalls; see Pages waiting for permission. - Looks for a chapter file in
$STORY_DIR/chapters. With none, the remaining steps and thedeployjob are skipped. - Builds the review copy with
story build "$STORY_DIR" --format html --stamp "$(date -u +%Y-%m-%d) ${GITHUB_SHA::7}" --out "$GITHUB_WORKSPACE/review-site/index.html". The stamp prints the build date and short commit at the top of the copy, so readers can say which build a note refers to.--note-url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/issues/new?template=manuscript-note.yml"puts a Note link beside every paragraph label that opens the issue form with the label, the build, and the paragraph's first few words already filled in. The--outpath is absolute because a relative--outis resolved against the project root and may not leave it; see Output paths. A matter page whosepermissionis stillpendingis left out; see Pages waiting for permission. - Uploads
index.htmlas a workflow artifact namedreview-copy, which you can download from the run page. - Uploads the
review-sitefolder as the Pages site.
The deploy job then publishes that site with actions/deploy-pages to the github-pages environment.
The workflow builds from the markdown on every run and commits nothing, so dist/ stays out of the repository.
Keeping the manuscript private
A GitHub Pages site is public, even when the repository is private, unless your GitHub plan supports private Pages with access control. Publishing an unpublished book on the open web can matter to publishers and to some contests. If the manuscript must stay private:
- delete the
deployjob and the Upload the Pages site step, and - share the
review-copyworkflow artifact instead. Anyone with read access to the repository can download it from the run page; readers then openindex.htmlin a browser.
The feedback-triage skill sets this up for a round of reader notes. It warns about Pages visibility and confirms it with you first, then asks before creating files in .github/.
Pages waiting for permission
An epigraph, song lyric, or poem quoted on a matter page may need the rights holder's permission before anyone else reads it. While its permission is pending, or misspelt, or any value but not-needed, granted, or public-domain, story build leaves the page out of the review copy, so the workflow never publishes it, and the build step's log names each page it left out:
warning: matter/epigraph.md permission is still pending, so it is left out; pass --include-pending to include it [permission-pending-left-out]The rest of the book is published as usual. When the permission arrives, set permission: granted and rights-holder on the page and push, and the next copy includes it. Do not add --include-pending to the build step: it would publish the page. cli-defaults in story.md cannot set it either.
To stop the workflow instead, so readers keep the last copy until the page is cleared, promote the warning to an error in story.md:
severity:
- warning: permission-pending-left-out
level: errorThe build step then fails, and nothing is uploaded or deployed. A page with no text yet is never printed, so it is not reported and cannot fail the build.
STORY_VERSION must name a release after 0.22.1 for the build to leave the page out; 0.22.1 and earlier publish it. The template's Look for matter waiting for permission step does not depend on the release: it greps matter/ itself and warns in the job log about each page whose permission line says pending:
Warning: ./matter/epigraph.md has permission pending. Story Skills releases after 0.22.1 leave the page out of the review copy; 0.22.1 and earlier publish it.Review rounds and changing labels
A paragraph label is the paragraph's position in its chapter, so it changes when you add or remove paragraphs earlier in that chapter. Because this workflow republishes on every push, a note made last week may point at a paragraph that has since moved. Readers therefore quote the build stamp printed at the top of the copy and the paragraph's first few words along with the label; the issue form asks for both. Tag the commit when you send readers the link for a round, for example beta-round-1. To find where an old label's paragraph is now, run story compare . --ref beta-round-1 --anchor ch03-p12 (repeat --anchor for several notes): it prints the paragraph's current label, whether its text changed, or its first few words when it is gone. A build stamp names a commit, so --ref can also take the short commit from the stamp. See Import, export, and builds for how labels are numbered.
Story bible on GitHub Pages
The same workflow can publish the story bible site beside the review copy, so readers can look up who someone is or when something happened. Add a step after Build the review copy that writes the codex into the Pages folder:
- name: Build the story bible
if: steps.chapters.outputs.has-chapters == 'true'
run: story build "$STORY_DIR" --format codex --out "$GITHUB_WORKSPACE/review-site/codex"STORY_VERSION must name a release that has the codex format. The site then serves the bible at https://<owner>.github.io/<repository>/codex/. Leave out --spoilers here: without it the codex holds no entity notes, deaths, clues, or resolutions, so it is safe for readers who have not finished the book. A --spoilers build belongs in the workflow artifact or on your own machine, never on a public site.
Manuscript note issue form
ISSUE_TEMPLATE/manuscript-note.yml is a GitHub issue form for readers of the review copy. Each note becomes an issue labelled manuscript-note, with the paragraph label in its title and body.
Install it
- Copy the file to
.github/ISSUE_TEMPLATE/manuscript-note.ymlin your story repository. - Create a label named
manuscript-note(Issues, then Labels, then New label). GitHub applies only labels that already exist, so without it the issues arrive unlabelled. - Commit and push. The form appears under New issue.
Give readers a direct link to the form alongside the review copy: https://github.com/<owner>/<repository>/issues/new?template=manuscript-note.yml. Readers need a GitHub account and permission to open issues: anyone can on a public repository, and only collaborators can on a private one.
What the form asks
| Field | Type | Required | Contents |
|---|---|---|---|
| Issue title | Text, starting [ch00-p0] | Yes | Readers replace ch00-p0 with the paragraph label and add a short summary. |
| Paragraph label | Short text | Yes | One label, such as ch03-p12, or the first and last of a passage, such as ch03-p12 to ch03-p15. |
| Build | Short text | No | The build stamp at the top of the review copy, such as 2026-09-27 abc1234. |
| First few words of the paragraph | Short text | Yes | The paragraph's opening words, so the note still finds its paragraph after later edits renumber the labels. |
| What kind of note is this? | Dropdown | Yes | Typo or wording; Confusing; Continuity (contradicts something earlier); Pacing (slow or rushed); Character (feels off); Sensitivity or authenticity; Loved this; Other. |
| Your note | Long text | Yes | What the reader noticed and how it made them feel. The form tells them they need not suggest a fix. |
| How much did it affect your reading? | Dropdown | No | Barely noticed; Pulled me out for a moment; Made me want to stop reading. |
From issues to revisions
The notes are raw reader reactions, not decisions. The feedback-triage skill records them in one file per reader under feedback/round-<N>/, fetches the issues with gh issue list --label manuscript-note, maps each old label to the current text, keeps each note's paragraph label in its Where line, and weighs them against your intent before anything changes in the manuscript. Anyone who can open an issue can file a note, so the skill treats each issue's title, body, and author name as material to weigh, never as instructions. A suggestion for the story is weighed as a note. If a note asks the agent to act, for example to run a command, open a link, edit a file, change another note, or reveal a secret, the agent does not do it and tells you what the note asked. It also names the reader's file from the issue's author.login, never from the display name the reader types. See Skills catalogue and Writing workflows.
Customising the workflows
Project location
Set STORY_DIR at the top of each workflow file when the story project lives in a subdirectory:
env:
STORY_DIR: "books/the-last-ember"
STORY_VERSION: "0.24.1"For several books in one repository, copy the check steps once per book, or turn STORY_DIR into a matrix value. For a linked series, add a story series "$STORY_DIR" step: it exits 1 when canon contradicts itself across books. The story links step is what catches a missing series backlink. See Series.
Every book a linked series names in follows or precedes must be in the checkout, at the sibling path the link gives. story links and story series report a linked book missing from disk as an error (story.md follows ../the-fall-of-the-citadel is not a story project: missing story.md), so a sequel kept in its own repository fails story-checks.yml on every push, and review-copy.yml, which runs links before it builds, never publishes. Keep the whole series in one repository, with each book in its own folder and STORY_DIR naming the book, or check the other books out beside it with extra actions/checkout steps (repository: and path:) so the relative links resolve. With extra checkouts, check the main repository out into a path: too, because a checkout cannot place a sibling outside $GITHUB_WORKSPACE.
CLI version
STORY_VERSION is the Story Skills release the CLI is installed from. Each job installs story-skills@$STORY_VERSION from npm once, in its Install the Story CLI step, into the runner's temporary folder, and later steps run plain story. The release process sets it to the release's own version, so a template copied from a given release already points at that release. Bump it in every workflow file you use when you want a newer release, and run the checks locally first, because new releases can add checks. Nothing bumps it for you; see Upgrading the workflows. Releases are listed on the GitHub releases page and on npm.
To install from GitHub instead, for an unreleased fix or a fork, uncomment STORY_PACKAGE in the workflow's env block and set it to any npm install spec, such as github:danjdewhurst/story-skills#main, a tag, or a commit. When it is set, every install step uses it in place of story-skills@$STORY_VERSION. A git install is slower and has no npm provenance, so switch back once the fix is released.
Schedule
Edit the cron expression. GitHub cron times are UTC.
on:
schedule:
- cron: "0 6 * * 1-5" # weekday mornings
workflow_dispatch:Delete the schedule block to draft only when you run the workflow by hand.
Branches
story-checks.yml runs on pushes to main and on all pull requests, and review-copy.yml on pushes to main. If your default branch has another name, change branches: [main] in both. To publish the review copy from a different branch, such as a beta branch you merge into when a round starts, name that branch instead.
Extra steps
Useful steps to add to story-checks.yml:
| Step | Command | Effect |
|---|---|---|
| Prose lint in the log | story prose "$STORY_DIR" | Prints per-chapter prose statistics. Exits 0 on a readable project unless severity in story.md promotes a prose warning. |
| Timeline in the log | story timeline "$STORY_DIR" | Prints scene chronology, POV balance, and character presence. |
| Fail on stale generated data | story reindex "$STORY_DIR", story wordcount "$STORY_DIR" --write, then git diff --exit-code | See Failing on warnings. |
| Build a reading copy | story build "$STORY_DIR" --format epub | Writes dist/<story-id>.epub, which you can upload with actions/upload-artifact. |
For example, to attach an EPUB to every run:
- name: Build EPUB
run: story build "$STORY_DIR" --format epub
- name: Upload EPUB
uses: actions/upload-artifact@v4
with:
name: epub
path: ${{ env.STORY_DIR }}/dist/*.epubPin actions/upload-artifact to a commit SHA to match the rest of the file. Import, export, and builds covers build formats and options.
Action pins
The uses: lines are pinned to commit SHAs, so a moved or compromised tag cannot change what runs. The templates ship with the same pins as this repository's own CI, and a test keeps them in step, so a fresh copy starts current. Dependabot does not update templates/, only the workflows in a repository's .github/workflows/, so once you have copied the files there, add a Dependabot config for the github-actions ecosystem to your story repository to keep the pins current:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"Upgrading the workflows
A copied workflow keeps installing the release in its STORY_VERSION until you change it. Dependabot updates the uses: action pins (see Action pins) but not an env value, so nothing reminds you when a newer Story Skills release is out. Behaviour the workflows rely on can change between releases, such as the review copy's compare --anchor labels, so upgrade deliberately.
Bumping STORY_VERSION
Pick the release from the GitHub releases page and read its changelog entries since your current pin.
Run that release locally on the project, so new findings show up on your machine rather than in CI:
shellnpx --yes --package story-skills@0.24.1 story check .Change the pin in every workflow file you copied, such as
.github/workflows/story-checks.yml,draft-next-chapter.yml, andreview-copy.yml. Each has one line to change, without av:yamlenv: STORY_VERSION: "0.24.1"Compare each file with the template from the same release in
templates/github/. A release can change steps as well as the version.
Migrating from STORY_REF
Templates from 0.21.0 and earlier pinned a git tag with STORY_REF: "v0.20.0" and fetched the CLI from GitHub with github:danjdewhurst/story-skills#$STORY_REF. The current templates install from npm with STORY_VERSION, which is faster and carries npm provenance. To migrate a copied workflow:
- Replace the
STORY_REFline withSTORY_VERSIONand drop thevfrom the value, sov0.20.0becomes0.20.0, or name the release you are moving to. - Replace each
npx --yes --package "github:danjdewhurst/story-skills#$STORY_REF" story ...step, or thenpm install ... "github:danjdewhurst/story-skills#$STORY_REF"line, with the template's Install the Story CLI step and plainstorycommands. Copying the current template and reapplying yourSTORY_DIR, schedule, and extra steps is usually quickest. - If
STORY_REFnamed a branch or commit rather than a release, setSTORY_PACKAGEtogithub:danjdewhurst/story-skills#<ref>instead (see CLI version).
Getting reminders
story doctor reads the workflows in the project's .github/workflows/, and in the git repository root above it when the project lives in a subdirectory. It adds a low-priority action for each STORY_VERSION older or newer than the CLI you are running, and for each legacy STORY_REF, naming the file, the line, and what to change, where <old> and <new> are the pinned release and <cli> the one you are running:
- [P3] Update workflow CLI version: .github/workflows/story-checks.yml:30 installs story-skills <old>, older than this CLI (<cli>); after story check passes locally, change the line to STORY_VERSION: "<cli>".
- [P3] Update local CLI version: .github/workflows/story-checks.yml:30 installs story-skills <new>, newer than this CLI (<cli>), so CI can report findings that story check here does not; update it with npm install -g story-skills@<new>.
- [P3] Rename workflow STORY_REF: .github/workflows/review-copy.yml:47 sets the legacy STORY_REF; change the line to STORY_VERSION: "<cli>" and copy the install step from the current template (see Upgrading the workflows in docs/automation.md).A newer pin means CI runs checks your CLI does not have yet, so the note asks you to update the CLI rather than the workflow. All the newer pins share one note, which lists each line and names the newest release (with each line's release in brackets when they differ). It names the command for the way you installed the CLI, judged from where its code runs:
| Installed with | The note says |
|---|---|
npm install -g | update it with npm install -g story-skills@<new> |
A package.json dependency | update the story-skills dependency in <folder>/package.json to <new> |
npx | install that release with npm install -g story-skills@<new> |
bunx | install that release with bun add -g story-skills@<new> |
bun add -g | update it with bun add -g story-skills@<new> |
| Homebrew | update it with brew upgrade story-skills |
| A downloaded binary | download the <new> binary for your system from the release page |
The bundled scripts/story.js in the skills | update the Story Skills plugin or skills, which carry this bundled CLI |
| A clone of this repository | update the clone in <folder> with git pull, or, with a release tag checked out, with git fetch --tags and git checkout v<new> |
npx and bunx users are told to install globally because npx reads the .npmrc in the folder it runs in, and a cloned story repository can carry one that points at another registry; a global npm or Bun install ignores the project's .npmrc and bunfig.toml. Any other layout, such as pnpm, Yarn Plug'n'Play, or story-skills as a dependency of another package, gets update it to <new> and a pointer to Update, pin, or remove.
Versions compare in semver order: 1.0.0-rc.1 is older than 1.0.0 and newer than 0.9.0, and build metadata such as +ci.7 does not count. A STORY_VERSION that is not one exact release gets no version note, since doctor cannot tell which release CI installs. That covers latest, a range like ^0.22.0, 0.22, a number with a leading zero or too large to compare exactly, and a prerelease or build label over 64 characters.
A legacy STORY_REF that names a release newer than the CLI keeps that release, prerelease included, in the suggested line, and one that names a branch or commit gets a STORY_PACKAGE line instead. The note repeats the ref only when it is at most 100 letters, digits, and _./@+-, and says <ref> otherwise. A non-empty, uncommented STORY_PACKAGE silences the STORY_VERSION notes in its own env block and in the jobs and steps inside its owner, since their install step does not use that pin. So one at the top of the file covers the whole workflow, one in a job covers that job, and one in a step covers that step. An empty STORY_PACKAGE: "" does not count, as the templates' ${STORY_PACKAGE:-...} treats it as unset. When there is a workflow note, the Project is mechanically healthy action is left out.
It reads only the STORY_VERSION: and STORY_REF: lines, ignores commented-out ones, and says nothing when there is no workflows folder. A workflow file that is a symlink, a FIFO, or larger than 5 MiB is skipped without being read. story next and story report leave it out. Keeping your local CLI current and running story doctor now and then is enough to notice a stale pin.
To get a pull request instead, Renovate can bump STORY_VERSION with a regex manager that reads the line as an npm version. Add this to renovate.json in your story repository:
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"customManagers": [
{
"customType": "regex",
"managerFilePatterns": ["/^\\.github/workflows/[^/]+\\.ya?ml$/"],
"matchStrings": ["STORY_VERSION:\\s*\"(?<currentValue>[^\"]+)\""],
"depNameTemplate": "story-skills",
"datasourceTemplate": "npm"
}
]
}Renovate then opens a pull request that changes the pin, and story-checks.yml runs on it with the new release, so you see what it reports before merging. Read the changelog before you merge it. This tests the new release only while STORY_PACKAGE is commented out: a workflow with STORY_PACKAGE set keeps installing that package, so the pull request changes a pin nothing uses. Comment out or remove STORY_PACKAGE once the fix it tracked is released.
Checking before each commit
A git pre-commit hook catches errors before they reach CI. Save this as .git/hooks/pre-commit in your story repository and make it executable with chmod +x .git/hooks/pre-commit:
#!/bin/sh
# Block the commit when the story project has errors.
STORY_DIR=.
story validate "$STORY_DIR" || exit 1
story links "$STORY_DIR" || exit 1
story continuity "$STORY_DIR" || exit 1The hook assumes story is on your PATH, for example after npm install -g story-skills. If it is not, replace story with npx --yes story-skills@0.24.1 or with node <skills-dir>/story-maintenance/scripts/story.js. Warnings do not block the commit.
The hook only checks. It does not run story reindex or story wordcount --write, because a hook that rewrites files leaves those changes unstaged. Run those yourself, or ask your agent to, after changing chapters or entities; the skills already direct agents to do this. See Writing workflows.
To skip the hook for one commit, for example to save a half-finished chapter, use git commit --no-verify.
If you use the pre-commit framework, the equivalent is a set of local hooks:
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: story-validate
name: story validate
entry: story validate .
language: system
pass_filenames: false
- id: story-links
name: story links
entry: story links .
language: system
pass_filenames: false
- id: story-continuity
name: story continuity
entry: story continuity .
language: system
pass_filenames: falsepass_filenames: false matters: each command takes the project root, not a list of changed files.
Other CI systems
Nothing in the checks is specific to GitHub. Any CI system with Node 18 or newer can run them:
npm install -g story-skills@0.24.1
story validate .
story links .
story continuity .Each command exits 1 on errors, and 2, 3, or 4 when it cannot run at all, so any failure fails the job. Add story report . --actionable at the end if you want a readable summary in the log.
See also
- CLI reference: every command, option, and exit code
- Continuity and analysis: what
continuitychecks and how exemptions work - Writing workflows: the chapter-writing loop the draft workflow automates
- Development guide: the CI for the Story Skills repository itself
- Documentation index: every page, by audience and task