Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9c8ca11
Lock the local API to the app
claude Oct 10, 2026
8493a06
Commit only what each change wrote
claude Oct 10, 2026
0c95869
Make ripple reverts all-or-nothing and retry failed proposals
claude Oct 10, 2026
987f6c4
Retry transient model failures and log every call's cost
claude Oct 10, 2026
ccc7dc7
Keep prompts bounded as the book grows
claude Oct 10, 2026
66a8bc7
Add a UI unit-test project and test the pure helpers
claude Oct 10, 2026
1ad7657
Make UI saves trustworthy; report errors; fix Escape, focus and labels
claude Oct 10, 2026
8c0fadb
Let QUILLGON_FIXTURE_DIR point open-sample at the sample novella
claude Oct 10, 2026
c94630f
Package the desktop app with esbuild and electron-builder
claude Oct 10, 2026
1b11a72
Harden the Electron shell and run the bundled server when packaged
claude Oct 10, 2026
3f921cb
Add a release workflow that attaches installers to a draft release
claude Oct 10, 2026
3fce4d9
Document building installers and the desktop packaging
claude Oct 10, 2026
f37db59
Run the browser e2e in CI, queue server jobs, and fix the CLI's prompts
claude Oct 10, 2026
542b3f6
Merge UI save safety, error reporting and accessibility work
claude Oct 10, 2026
25e67ec
Merge desktop packaging, release workflow and Electron hardening
claude Oct 10, 2026
67f3789
Honour `serve --port 0`
claude Oct 10, 2026
8e72370
Ask before closing with unsaved edits; let installed apps find Codex
claude Oct 10, 2026
46cb5f0
Add an eval harness for prompt and model changes
claude Oct 10, 2026
5698060
Lint for correctness in CI
claude Oct 10, 2026
952ee54
Fix double prompt when closing Electron with unsaved edits
cursoragent Oct 10, 2026
35d3ccd
Fix the issues a final review found
claude Oct 10, 2026
dfb6b0d
Merge remote-tracking branch 'origin/claude/repo-review-improvements-…
claude Oct 10, 2026
6c65a72
Keep the in-app close prompt now that the native one is gone
claude Oct 10, 2026
1620e7d
Scope a deletion ripple to the state files the deletion rewrote
claude Oct 10, 2026
6fd0656
Merge the base branch: covers, the README redesign and the thinking p…
claude Oct 10, 2026
2abdb4c
Save pending prose before the window closes
claude Oct 10, 2026
f4699a0
Fix close dropping unsaved prose
cursoragent Oct 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,36 @@ jobs:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm test
- run: pnpm typecheck
# Prompts must grow with the scene, not the book (see scripts/scale-check.ts).
- run: pnpm scale-check --max-prompt 8000
- run: pnpm site:build
- run: pnpm site:check

e2e:
# The real UI in Chromium against the real server, with offline models.
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10.28.0
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm exec playwright-core install --with-deps chromium
- run: pnpm e2e
- run: pnpm tsx scripts/ui-refresh.ts
- run: pnpm tsx scripts/e2e-covers.ts
- name: Keep screenshots of a failed run
if: failure()
uses: actions/upload-artifact@v4
with:
name: e2e-screenshots
path: docs/screenshots
retention-days: 7
93 changes: 93 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
name: Release

# Pushing a tag like v0.2.0 builds the desktop app on macOS (Apple silicon), Windows (x64) and
# Linux (x64) and attaches the installers to a draft GitHub release for that tag. Review the
# draft, then publish it. Running the workflow by hand builds the same installers as workflow
# artifacts without creating a release.
#
# Signing is optional. Without these repository secrets the installers are unsigned (macOS builds
# are ad-hoc signed): MAC_CSC_LINK, MAC_CSC_KEY_PASSWORD, APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD,
# APPLE_TEAM_ID (sign and notarize for macOS); WIN_CSC_LINK, WIN_CSC_KEY_PASSWORD (Windows).

on:
push:
tags: ['v*']
workflow_dispatch:

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: read

jobs:
build:
name: build (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [macos-latest, windows-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10.28.0
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm test
if: runner.os == 'Linux'
# Builds the UI, bundles the server, rebuilds better-sqlite3 for Electron, then runs
# electron-builder for this platform. Output: apps/desktop/release/.
- run: pnpm dist
env:
QUILLGON_VERSION: ${{ github.ref_type == 'tag' && github.ref_name || '' }}
CSC_LINK: ${{ runner.os == 'macOS' && secrets.MAC_CSC_LINK || '' }}
CSC_KEY_PASSWORD: ${{ runner.os == 'macOS' && secrets.MAC_CSC_KEY_PASSWORD || '' }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
- uses: actions/upload-artifact@v4
with:
name: quillgon-${{ runner.os }}
if-no-files-found: error
path: |
apps/desktop/release/*.dmg
apps/desktop/release/*.zip
apps/desktop/release/*.exe
apps/desktop/release/*.AppImage
apps/desktop/release/*.deb

release:
needs: build
if: github.ref_type == 'tag'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
path: installers
merge-multiple: true
- name: Attach the installers to a draft release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.ref_name }}
run: |
ls -l installers
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release upload "$TAG" installers/* --clobber --repo "$GITHUB_REPOSITORY"
else
prerelease=""
case "$TAG" in *-*) prerelease="--prerelease" ;; esac
gh release create "$TAG" installers/* --draft $prerelease --verify-tag --generate-notes \
--title "Quillgon $TAG" --repo "$GITHUB_REPOSITORY"
fi
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ out/
coverage/
.vite/
.vercel/
release/
evals/runs/
23 changes: 22 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,37 @@ Open <http://127.0.0.1:4317> and choose **Open the sample novella**. To explore

For UI development, run `pnpm dev`. The local Vite server uses <http://localhost:5173>.

## Desktop app and packaging

`pnpm desktop` runs the Electron shell against the source tree (plain Node and tsx; needs `node` on your `PATH`). Installers are built with:

```sh
pnpm dist # UI + server bundle + electron-builder, for this platform
pnpm -C apps/desktop dist:dir # the same, but only the unpacked app (faster)
pnpm -C apps/desktop bundle # only stage apps/desktop/out/ (needs `pnpm build` first)
```

The pieces:

- `apps/desktop/electron/main.cjs` and `preload.cjs`: the shell. It starts the server, waits up to 30 seconds for its URL (`QUILLGON_STARTUP_TIMEOUT_MS`), and keeps the window on the server's origin.
- `apps/desktop/electron/server-entry.mjs`: the server's entry point in a packaged app.
- `apps/desktop/scripts/prepare-package.mjs`: bundles that entry and the workspace packages into `out/server/server.mjs` with esbuild, copies the UI, the sample novella and the agent prompts next to it, and installs `better-sqlite3` rebuilt for Electron (a prebuilt binary if one exists, otherwise compiled, which needs a C/C++ toolchain, Python 3 and access to electronjs.org).
- `apps/desktop/electron-builder.config.cjs`: targets, and optional signing.
- `.github/workflows/release.yml`: on a `v*` tag, builds macOS, Windows and Linux and attaches the installers to a draft release.

To run the bundled server without packaging, stage it and point the shell at it: `pnpm -C apps/desktop bundle`, then `QUILLGON_SERVER_BUNDLE="$PWD/apps/desktop/out/server/server.mjs" pnpm desktop` from the repository root. If you add a file that the server reads at runtime relative to its own source file, stage it in `prepare-package.mjs` too, or the installed app will not find it. Build output (`apps/desktop/out/`, `apps/desktop/release/`) is ignored by Git; do not commit it.

## Before a pull request

```sh
pnpm lint
pnpm test
pnpm typecheck
pnpm site:build
pnpm site:check
```

If you change the desktop UI, include a screenshot and describe the user flow you checked. If you change story data or an agent, describe the fixture or offline scenario you ran. Live-model tests require a provider key and are not part of the normal contribution gate.
If you change the desktop UI, include a screenshot and describe the user flow you checked. If you change the Electron shell or packaging, say which platforms you built and ran (`pnpm -C apps/desktop dist:dir` is enough for most changes). If you change story data or an agent, describe the fixture or offline scenario you ran. Live-model tests require a provider key and are not part of the normal contribution gate.

Keep pull requests focused. Explain the user-facing problem, the change, and how you verified it. Do not commit API keys, private manuscripts, generated books, or `.engine` databases. The project stores books as local Markdown and YAML; sample data belongs in `fixtures/` only when it is safe to publish.

Expand Down
58 changes: 53 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,12 @@ pnpm serve # http://127.0.0.1:4317, then "Open the sample novella"
> [!TIP]
> **No API key? No problem.** Choose **Offline** models when you create a book (or set `QUILLGON_OFFLINE=1`). Offline output is placeholder text, but every screen and workflow works, so you can explore the whole app for free.

For the Electron shell, stop the server and run `pnpm desktop` (it also needs Node.js on your `PATH`). For development, `pnpm dev` runs the API and the Vite dev server with hot reload on http://localhost:5173.
For the Electron shell, stop the server and run `pnpm desktop` (it also needs Node.js on your `PATH`). To build an installer instead, see [Desktop app and installers](#desktop-app-and-installers). For development, `pnpm dev` runs the API and the Vite dev server with hot reload on http://localhost:5173.

Books are stored in local Markdown and YAML files. Connected AI providers receive relevant story context when you use generation or analysis.

> [!NOTE]
> **Quillgon is an early-stage source preview.** The sample novella and offline workflows are tested end to end. A complete 80,000-word novel with live models has not yet been validated, and there are no packaged installers yet. [Tell us what breaks](https://github.com/JeremySNR/quillgon/issues).
> **Quillgon is an early-stage source preview.** The sample novella and offline workflows are tested end to end. A complete 80,000-word novel with live models has not yet been validated. No installers have been published yet; you can build one with `pnpm dist` (see [Desktop app and installers](#desktop-app-and-installers)). [Tell us what breaks](https://github.com/JeremySNR/quillgon/issues).

### 🔌 Connect a model

Expand Down Expand Up @@ -195,6 +195,35 @@ Verification: a short synthetic request through the real Codex SDK and local Cha

</details>

## Desktop app and installers

`pnpm desktop` runs the Electron shell from your checkout. To build an installer for the computer you are on:

```bash
pnpm install
pnpm dist # installers in apps/desktop/release/
```

| Platform | Output |
| --- | --- |
| macOS (Apple silicon) | `.dmg` and `.zip` |
| Windows (x64) | NSIS installer (`.exe`) |
| Linux (x64) | `.AppImage` and `.deb` |

`pnpm dist` builds the UI, bundles the server into a single file with esbuild, rebuilds `better-sqlite3` for Electron, and runs electron-builder (see `apps/desktop/scripts/prepare-package.mjs` and `apps/desktop/electron-builder.config.cjs`). The installed app runs that bundled server with its own Electron runtime, so it needs neither Node.js nor the source tree. The `better-sqlite3` rebuild needs a C/C++ toolchain, Python 3 and network access to electronjs.org for the Electron headers. `pnpm -C apps/desktop dist:dir` skips the installer and leaves the unpacked app in `apps/desktop/release/`.

Pushing a `v*` tag runs [`.github/workflows/release.yml`](.github/workflows/release.yml), which builds all three platforms and attaches the installers to a draft GitHub release.

Before you install one:

- **Installers are unsigned unless you configure signing.** macOS builds are only ad-hoc signed, so macOS reports an unidentified developer: Control-click the app and choose **Open** the first time. Windows SmartScreen warns about an unknown publisher. To sign, set electron-builder's variables (`CSC_LINK` and `CSC_KEY_PASSWORD`, plus `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD` and `APPLE_TEAM_ID` to notarize); the release workflow reads them from the repository secrets listed at its top.
- **Git must be installed.** Every book is a Git repository. The `.deb` depends on `git`.
- **API keys come from the environment.** An app started from the Dock, the Start menu or a launcher does not see variables set in your shell profile. Start it from a terminal where `OPENAI_API_KEY` is set (on macOS, `/Applications/Quillgon.app/Contents/MacOS/Quillgon`), or use Offline models.
- **Codex subscription in an installed app needs your own Codex binary.** The Codex SDK runs the binary from its own npm package, which the installer does not include. Install the Codex CLI and set `QUILLGON_CODEX_PATH` to its path (the output of `which codex`) in the environment that launches Quillgon.
- The installers use Electron's default icon.

Verified so far: on Linux x64, electron-builder produces the AppImage, the `.deb` and the unpacked app, and the packaged app starts its bundled server and shows the UI. Not yet verified: opening a book in an installed build, which needs `better-sqlite3` rebuilt for Electron (the release workflow does this; it could not be done where the packaging was developed), and the macOS and Windows installers, which so far are built only by the release workflow.

## 🛠️ How it works

```mermaid
Expand Down Expand Up @@ -260,11 +289,13 @@ quillgon read [--act act-1] [--outline] | check [--mechanical] | voice | estimat
quillgon export [--format md|docx|epub] (docx/epub need Pandoc)
quillgon branch [name | switch <b> | merge <b> | delete <b>]
quillgon serve [--dir book] [--port 4317]
quillgon eval [--offline|--codex] [--limit N] [--only a,b] [--all-scenes] score the frozen premise set
quillgon eval compare <runA> <runB> judge run B's key scenes against run A's, both ways
```

</details>

Pass `--yes` to accept the defaults at every gate (useful for scripting), or `--record <dir>` / `--replay <dir>` to record model responses as test fixtures or replay them.
Pass `--yes` to accept the defaults at every gate (useful for scripting). Without a terminal and without `--yes`, any choice that changes the book stops with an error rather than answering itself. Use `--record <dir>` / `--replay <dir>` to record model responses as test fixtures or replay them.

## 🏗️ Architecture

Expand All @@ -284,6 +315,7 @@ fixtures/novella/ "The Tidekeeper's Ledger": 12 scenes, 4 characters, 3 threa

<br>

- **The local API is locked to the app.** The server binds to 127.0.0.1 and requires a per-launch token on every API request; the UI reads it from the page the server serves. Requests with a non-loopback `Host` (DNS rebinding) or a foreign `Origin` are refused, request bodies must be JSON, and git refs from the UI are validated before they reach git. Set `QUILLGON_TOKEN` to choose the token yourself, for example to script against `pnpm serve`. A book's `api_key_env` must name a variable ending in `_API_KEY`, so a shared book file cannot send another secret to a model endpoint.
- **Only the graph store writes to the manuscript.** Agents return structured output, validated with Zod, and the store writes files, updates the index and commits.
- **A book is a git repo of Markdown and YAML** (`book.yaml`, `intent.md`, `plot-points/`, `threads/`, `characters/<id>/{bio.md,voice.md,state.yaml}`, `scenes/<id>/{card.yaml,prose.md,facts.yaml}`, `obligations/`, `idea.yaml` (the idea board and its conversation), `structure.yaml`, `timeline.yaml`, `style/` (samples, banned patterns, `style.yaml`), `reports/`). You can edit any of it in another editor; the app reloads.
- **The index is disposable.** `.engine/index.sqlite` holds entities, references, facts and scene embeddings. Delete it and it rebuilds. Embeddings are local and deterministic (hashed term vectors), so impact analysis needs no network.
Expand All @@ -300,13 +332,29 @@ The UI design follows the research brief in [docs/design.md](docs/design.md); th
## 🧪 Tests and roadmap

```bash
pnpm test # 94 unit/integration tests, no model calls
pnpm test # unit, integration, CLI and UI tests, no model calls
pnpm e2e # drives the real UI in Chromium (offline models)
pnpm scale-check # prompt sizes and write latency on a 96-scene book
pnpm quillgon eval --offline --limit 2 # the eval harness, offline (see below)
pnpm screenshots # regenerates docs/screenshots
pnpm typecheck
pnpm test:live # live-model tests; needs QUILLGON_LIVE=1 and an API key
```

### Evaluating prompt and model changes

`quillgon eval` runs the frozen set in `evals/premises.yaml` (20 premises, each with a fixed intent) through every gate: plot points, structure, scene cards, then the opening, the turn and the ending (or every scene with `--all-scenes`), and the simulated reader. It scores each run with deterministic measures:

| Measure | What it catches |
| --- | --- |
| Slop per 1,000 words | Banned patterns and stock machine phrasing |
| Style violations per 1,000 words | Breaks of the code-checked style sheet |
| Continuity errors, structural tells | Knowledge errors; single-track plots, sagging middles, tidy endings |
| Twists guessed early, engagement | The simulated reader's predictions and interest |
| Shared names, trigram overlap, plan diversity | Books in one run converging on the same story |

Results and a report go to `evals/runs/<id>/`, with the generated books. To test a change, run the set before and after it, then `quillgon eval compare <before> <after>`: every key scene is judged against its counterpart in both orders, a split verdict counts as a tie, and the result is a win rate and the Elo difference it implies. A live run calls the model for every gate, so start with `--limit`. Pass `--record <dir>` to keep the model responses and `--replay <dir>` to re-score them without new calls.

| Phase | Status |
| --- | --- |
| 1. Schemas and store | ✅ Fixture validates; 12 kinds of corruption reported with exact file and field; index rebuild matches its snapshot |
Expand All @@ -326,7 +374,7 @@ pnpm test:live # live-model tests; needs QUILLGON_LIVE=1 and an API key
<br>

- **OpenAI Responses API and Codex SDK.** Quillgon owns the story graph and review workflow; each writing agent requests a structured result through the official OpenAI SDK. A hosted Agents API execution environment is unnecessary for these calls. The local Codex SDK provides a separate path for users signing in with ChatGPT.
- **Electron runs the core as a plain Node process.** This lets `better-sqlite3` load without an Electron rebuild. It needs `node` on your `PATH` (or set `QUILLGON_NODE`).
- **Electron runs the core as a separate process.** From a checkout (`pnpm desktop`) it is a plain Node process running the CLI through tsx, so `better-sqlite3` needs no Electron rebuild; it needs `node` on your `PATH` (or set `QUILLGON_NODE`). An installed app runs the bundled server with Electron's own Node (`ELECTRON_RUN_AS_NODE`) and a `better-sqlite3` rebuilt for it. Either way the window only shows the server's own pages: other navigation is blocked, web and mail links open in your browser, permission requests are denied, and if the server stops the app offers to restart it.
- **Reader prediction matching is mechanical** (embedding similarity, synonym folding, and the prediction must name the right person). A cheap model judge would be more accurate and is an easy swap.
- **Two small additions to the data model:** `scenes/<id>/facts.yaml` holds extracted facts, so the index can rebuild from files, and an optional `locations.yaml` / `facts.yaml` glossary gives ids human-readable names.

Expand Down
Loading
Loading