Vibecode Ulysses
track this build5 phases, 15 steps, beginner friendly0%The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Ulysses, write in a focused Markdown library, organize sheets, set goals, and export polished documents. The hard boundary is native apple polish, icloud sync, mobile apps, and publishing integrations, plus polish, integrations, and proprietary language models.
You are building a lean indie version of Ulysses. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Ulysses · indie build A writing environment over a folder of Markdown files: a sheet library with groups and filters, a typewriter-style editor, word and character goals with honest progress, optional editing passes shown as a diff, and export to formatted documents with the source always readable elsewhere. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri with a TypeScript front end | real file access and a small binary, on every desktop platform | | Files | Plain Markdown, one file per sheet, in a folder you choose | the library is a directory; the application is a way of looking at it | | Index | SQLite for search, groups and goals, treated as disposable | the files are the truth and the index rebuilds from them | | Editor | A text editor built on a code editing component with Markdown syntax | reliable text editing is harder than it looks and is not where your effort belongs | | Export | Markdown to formatted output through a document toolchain | one source, several destinations, none of which change the source | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the front end and the tooling. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **The Rust toolchain** · free - Why: Tauri compiles the desktop shell and the file watcher. - Get it: rustup from rustup.rs, plus the platform packages Tauri lists. - Verify: A Tauri hello world builds and runs - [ ] **A document conversion toolchain** · free - Why: Phase 5 turns Markdown into the formats people actually accept. Writing a converter is not the project. - Get it: Install pandoc or an equivalent, plus a typesetting engine if you want PDF output. - Verify: A test Markdown file converts to your target format - [ ] **A real writing library of at least fifty sheets** · free - Why: Ordering, search and goals are all trivial at five sheets and revealing at fifty, and compile order in particular fails only at scale. - Get it: Use your own writing, or split a long public domain text into sheets. - Verify: The library loads and its sheet count is reported - [ ] **A second Markdown editor** · free - Why: Phase 6 verifies the library reads without this application, which is the ownership claim and cannot be tested with the application that wrote it. - Get it: Any general Markdown editor you already have. - Verify: The library folder opens and sheets read correctly ## Quick start ```sh npm create tauri-app@latest ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The native polish of the commercial application. - Built-in sync and mobile applications. - Publishing integrations. - A decade of library management refinements. - native Apple polish, iCloud sync, mobile apps, and publishing integrations - cross-device sync - large proprietary style models - browser and office-suite extensions - team style guides If one of those is essential to you, that is the reason to keep paying for Ulysses, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Ulysses The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a distraction-light Markdown writing library with sheets, goals and export that stays out of the way to replace Ulysses. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Shell: Tauri with a TypeScript front end · real file access and a small binary, on every desktop platform - Files: Plain Markdown, one file per sheet, in a folder you choose · the library is a directory; the application is a way of looking at it - Index: SQLite for search, groups and goals, treated as disposable · the files are the truth and the index rebuilds from them - Editor: A text editor built on a code editing component with Markdown syntax · reliable text editing is harder than it looks and is not where your effort belongs - Export: Markdown to formatted output through a document toolchain · one source, several destinations, none of which change the source ### Data model (create this before Phase 1) - sheets(id, path, title, group_id, position, word_count, char_count, mtime, content_hash) · one Markdown file each - groups(id, parent_id, name, position) · directories on disk, so the structure is visible outside the application - goals(id, target_kind, target_value, scope_kind, scope_id, deadline) · scope is a sheet or a group - sessions(id, started_at, ended_at, words_added, sheet_id) · writing sessions, which is what progress is actually made of - keywords(id, name), sheet_keywords(sheet_id, keyword_id) · stored in front matter so they travel with the file - Groups are directories and keywords are front matter, so the organisation survives without this application. ### Phase 1 · A library over a folder Build: Open a folder as a library. Directories become groups, files become sheets, front matter supplies the title and keywords. Watch for external changes. Never move a file without the user asking. Trap: Deriving a sheet's title from its filename and then letting the user rename the title in the interface creates an ambiguity: does the file move? Decide explicitly, put the title in front matter, and leave filenames alone unless the user renames them deliberately. Done when: A folder of Markdown files opens as a library with directories as groups. Editing a file in another editor updates the view. Renaming a sheet's title changes front matter and leaves the filename alone. Moving a file on disk moves it in the library after a refresh. Do not build yet: No editor features, goals or export. ### Phase 2 · An editor that stays out of the way Build: A Markdown editor with syntax highlighting rather than a rich text surface. Typewriter scrolling and a focus mode. Debounced atomic saves. A word count that updates without stuttering on a long sheet. Trap: Writing directly over the file means a crash mid-save can truncate a chapter, which for a writer is the worst possible failure. Write to a temporary file and rename over the target, which is atomic and cannot leave a half-written sheet. Done when: Typing and quitting immediately leaves the text on disk. Killing the process during a save leaves either the old file or the new one. A ten thousand word sheet types without input lag and its word count stays current. Focus mode dims everything but the current paragraph. Do not build yet: No goals or export yet. ### Phase 3 · Goals that are honest Build: Goals by word or character count, on a sheet or a group, with an optional deadline. Track writing sessions and words added per session. Show progress without streaks, grades or any language that treats a missed day as a failure. Trap: Counting net words means a day spent cutting shows as negative progress, which is exactly backwards: editing is writing. Track words added and words removed separately, and show both rather than a single number that punishes revision. Done when: A word goal on a group sums its sheets correctly. A session that removed words shows both added and removed rather than a negative total. Progress is shown without a streak counter or a grade. A deadline that passes shows the goal as unmet without any judgement language. Do not build yet: No editing passes or export yet. ### Phase 4 · Editing passes with a visible diff Build: Optional editing passes: readability, repetition and a grammar check through a local service. Present every result as a diff. Accept in whole or in part. Keep the original text so an accepted change stays reversible. Trap: Applying a change directly and relying on undo loses the original as soon as anything else is typed, and a writer who accepts a rewrite and then dislikes it has no way back. Store the original with the suggestion, so acceptance remains reversible beyond the undo stack. Done when: Every suggestion is shown as a diff before it can be applied. Accepting part of a suggestion applies only that part. The original is recoverable after acceptance and further editing. Disabling all passes leaves the editor completely unchanged, with no network activity. Do not build yet: No export yet. ### Phase 5 · Export Build: Export a sheet or a group to formatted output with a chosen style. Compile a group in the library's order with a separator you configure. Include or exclude sheets by keyword. Never modify the source files. Trap: Compiling in filesystem order rather than the library's explicit order will reorder a book, and the mistake is invisible until someone reads the export. Store an explicit position and use it, since alphabetical filenames stop working at chapter ten. Done when: A group exports as one document in the library's order, verified against a group where alphabetical and intended order differ. Excluding a keyword removes those sheets from the export. Source files are unmodified after export, verified by checksum. Exporting twice produces identical output. Do not build yet: Sync and distribution come last. ### Phase 6 · Sync, backup and leaving Build: Sync the library folder with a file sync tool or version control, and surface conflicts rather than hiding them. Keep the index outside the synced folder. Confirm the library is fully usable in another editor. Trap: Putting the search index inside the synced folder means two machines fight over the same database file and corrupt it. Keep it outside, and treat it as disposable so a corruption is a rebuild rather than a loss. Done when: A simultaneous edit on two machines surfaces as a visible conflict rather than a silent loss. The index lives outside the synced folder. Opening the library folder in a plain Markdown editor shows readable sheets with front matter. Deleting the index and relaunching rebuilds it with identical search results. Do not build yet: Nothing after this. ### Out of scope (and why) - The native polish of the commercial application, which is a decade of attention to how writing feels rather than a feature list. - Sync built into the application, and the mobile applications that make it work everywhere. - Publishing integrations that post directly to blogging platforms. - The library management depth that comes from years of writers asking for specific things. ### README must contain - That the library is a folder of Markdown files and the application is not required to read them - That groups are directories and keywords are front matter, so organisation survives without this - That saves are atomic and a crash cannot truncate a sheet - That goals count words added and removed separately, because editing is writing - That the index lives outside the library folder and is disposable ===== AGENTS.md ===== # Agent instructions · Ulysses indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri with a TypeScript front end, Plain Markdown, one file per sheet, in a folder you choose, SQLite for search, groups and goals, treated as disposable, A text editor built on a code editing component with Markdown syntax, Markdown to formatted output through a document toolchain. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Deriving a sheet's title from its filename and then letting the user rename the title in the interface creates an ambiguity: does the file move? Decide explicitly, put the title in front matter, and leave filenames alone unless the user renames them deliberately. - Writing directly over the file means a crash mid-save can truncate a chapter, which for a writer is the worst possible failure. Write to a temporary file and rename over the target, which is atomic and cannot leave a half-written sheet. - Counting net words means a day spent cutting shows as negative progress, which is exactly backwards: editing is writing. Track words added and words removed separately, and show both rather than a single number that punishes revision. - Applying a change directly and relying on undo loses the original as soon as anything else is typed, and a writer who accepts a rewrite and then dislikes it has no way back. Store the original with the suggestion, so acceptance remains reversible beyond the undo stack. - Compiling in filesystem order rather than the library's explicit order will reorder a book, and the mistake is invisible until someone reads the export. Store an explicit position and use it, since alphabetical filenames stop working at chapter ten. ===== BUILD_PLAN.md ===== # Build plan · Ulysses A writing environment over a folder of Markdown files: a sheet library with groups and filters, a typewriter-style editor, word and character goals with honest progress, optional editing passes shown as a diff, and export to formatted documents with the source always readable elsewhere. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A library over a folder Your Markdown files appear as a browsable library, and the structure on disk matches what you see. ### Steps 1. Map directories and files to groups and sheets The structure on disk is the structure in the application. No hidden database-only organisation. Files: `src/library.ts` ```sh npm create tauri-app@latest ``` 2. Read front matter for title and keywords So they travel with the file and are visible in any editor. Files: `src/frontmatter.ts` 3. Watch without fighting yourself Hash what you write and ignore your own events, or the watcher will reload under the cursor. Files: `src/watcher.ts` ### Done when - [ ] A folder opens as a library with directories as groups - [ ] External edits appear in the view - [ ] Renaming a title changes front matter, not the filename - [ ] Moving a file on disk moves it in the library ### Watch out - Deriving a sheet's title from its filename and then letting the user rename the title in the interface creates an ambiguity: does the file move? Decide explicitly, put the title in front matter, and leave filenames alone unless the user renames them deliberately. ## Phase 2 · An editor that stays out of the way Writing feels quiet, and nothing is ever lost. ### Steps 1. Use a code editing component with Markdown syntax Highlighting rather than rendering. A rich text surface fights Markdown and loses. Files: `src/editor.ts` 2. Save atomically with debounce Temporary file plus rename. Test by killing the process mid-save. Files: `src/save.ts` 3. Count words without stuttering Incrementally or off the main thread. Recounting a long sheet on every keystroke is felt immediately. Files: `src/count.ts` ### Done when - [ ] Text typed and immediately quit is on disk - [ ] A killed save leaves a complete file - [ ] A ten thousand word sheet types without lag - [ ] Focus mode dims everything but the current paragraph ### Watch out - Writing directly over the file means a crash mid-save can truncate a chapter, which for a writer is the worst possible failure. Write to a temporary file and rename over the target, which is atomic and cannot leave a half-written sheet. ## Phase 3 · Goals that are honest Progress is measured from what you actually wrote, including the days you did not. ### Steps 1. Track sessions with added and removed Both numbers. A day of cutting is progress and the interface should not disagree. Files: `src/sessions.ts` 2. Sum goals across a scope Sheet or group, computed from current counts rather than a running total that can drift. Files: `src/goals.ts` 3. Keep the language neutral No streaks, no grades, no red. A missed deadline is a fact, not a verdict. Files: `src/ui/Goal.tsx` ### Done when - [ ] A group goal sums its sheets correctly - [ ] A cutting session shows added and removed separately - [ ] No streak counter or grade appears anywhere - [ ] A passed deadline is stated without judgement ### Watch out - Counting net words means a day spent cutting shows as negative progress, which is exactly backwards: editing is writing. Track words added and words removed separately, and show both rather than a single number that punishes revision. ## Phase 4 · Editing passes with a visible diff Suggested changes are shown before they are applied, and the original survives. ### Steps 1. Run passes locally where possible A local grammar service keeps the manuscript on the machine. Say clearly if any pass does not. Files: `src/passes/` 2. Diff at word level Sentence-level diffs on prose are unreadable. Word level with sentence grouping is what a writer can judge. Files: `src/diff.ts` 3. Preserve originals On the suggestion record, so acceptance is reversible beyond undo. Files: `src/suggestions.ts` ### Done when - [ ] Every suggestion is shown as a diff first - [ ] Partial acceptance applies only the selected part - [ ] The original is recoverable after acceptance and further edits - [ ] Disabling passes produces no network activity ### Watch out - Applying a change directly and relying on undo loses the original as soon as anything else is typed, and a writer who accepts a rewrite and then dislikes it has no way back. Store the original with the suggestion, so acceptance remains reversible beyond the undo stack. ## Phase 5 · Export A group of sheets becomes one formatted document, and the source is untouched. ### Steps 1. Compile in explicit order The stored position, not the filename. Test with a group where the two disagree. Files: `src/export.ts` 2. Filter by keyword Include or exclude, so drafts and notes can live in the same group as the manuscript. Files: `src/export.ts` 3. Never touch the source Checksum before and after as a test. Files: `src/export.test.ts` ### Done when - [ ] A group exports in the library's order, not alphabetically - [ ] Keyword filtering excludes the right sheets - [ ] Source checksums are unchanged after export - [ ] Two exports of the same content are identical ### Watch out - Compiling in filesystem order rather than the library's explicit order will reorder a book, and the mistake is invisible until someone reads the export. Store an explicit position and use it, since alphabetical filenames stop working at chapter ten. ## Not in this build - The native polish of the commercial application. - Built-in sync and mobile applications. - Publishing integrations. - A decade of library management refinements. ## After v1, if you want it - Split view for writing against notes, which is the most missed feature in a single-pane editor - Revision history from version control surfaced in the interface - Custom export styles as files, so a manuscript format is version controlled ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The folder of Markdown files. Your data, in your backup. LIBRARY_PATH=~/Writing/Library # Required. Search and goals. Deliberately outside the library so syncing cannot corrupt it. INDEX_PATH=~/.local/share/write/index.db # Required. How long after typing before writing. Lower loses less on a crash. SAVE_DEBOUNCE_MS=400 # Required. Where compiled documents are written. Never the library folder. EXPORT_DIR=~/Writing/Exports # Optional. Optional local grammar service. Leave unset and the editing passes are disabled entirely. GRAMMAR_ENDPOINT=http://localhost:8081
You are building a lean indie version of Ulysses. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Ulysses · indie build A writing environment over a folder of Markdown files: a sheet library with groups and filters, a typewriter-style editor, word and character goals with honest progress, optional editing passes shown as a diff, and export to formatted documents with the source always readable elsewhere. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri with a TypeScript front end | real file access and a small binary, on every desktop platform | | Files | Plain Markdown, one file per sheet, in a folder you choose | the library is a directory; the application is a way of looking at it | | Index | SQLite for search, groups and goals, treated as disposable | the files are the truth and the index rebuilds from them | | Editor | A text editor built on a code editing component with Markdown syntax | reliable text editing is harder than it looks and is not where your effort belongs | | Export | Markdown to formatted output through a document toolchain | one source, several destinations, none of which change the source | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the front end and the tooling. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **The Rust toolchain** · free - Why: Tauri compiles the desktop shell and the file watcher. - Get it: rustup from rustup.rs, plus the platform packages Tauri lists. - Verify: A Tauri hello world builds and runs - [ ] **A document conversion toolchain** · free - Why: Phase 5 turns Markdown into the formats people actually accept. Writing a converter is not the project. - Get it: Install pandoc or an equivalent, plus a typesetting engine if you want PDF output. - Verify: A test Markdown file converts to your target format - [ ] **A real writing library of at least fifty sheets** · free - Why: Ordering, search and goals are all trivial at five sheets and revealing at fifty, and compile order in particular fails only at scale. - Get it: Use your own writing, or split a long public domain text into sheets. - Verify: The library loads and its sheet count is reported - [ ] **A second Markdown editor** · free - Why: Phase 6 verifies the library reads without this application, which is the ownership claim and cannot be tested with the application that wrote it. - Get it: Any general Markdown editor you already have. - Verify: The library folder opens and sheets read correctly ## Quick start ```sh npm create tauri-app@latest ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The native polish of the commercial application. - Built-in sync and mobile applications. - Publishing integrations. - A decade of library management refinements. - native Apple polish, iCloud sync, mobile apps, and publishing integrations - cross-device sync - large proprietary style models - browser and office-suite extensions - team style guides If one of those is essential to you, that is the reason to keep paying for Ulysses, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Ulysses The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a distraction-light Markdown writing library with sheets, goals and export that stays out of the way to replace Ulysses. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Shell: Tauri with a TypeScript front end · real file access and a small binary, on every desktop platform - Files: Plain Markdown, one file per sheet, in a folder you choose · the library is a directory; the application is a way of looking at it - Index: SQLite for search, groups and goals, treated as disposable · the files are the truth and the index rebuilds from them - Editor: A text editor built on a code editing component with Markdown syntax · reliable text editing is harder than it looks and is not where your effort belongs - Export: Markdown to formatted output through a document toolchain · one source, several destinations, none of which change the source ### Data model (create this before Phase 1) - sheets(id, path, title, group_id, position, word_count, char_count, mtime, content_hash) · one Markdown file each - groups(id, parent_id, name, position) · directories on disk, so the structure is visible outside the application - goals(id, target_kind, target_value, scope_kind, scope_id, deadline) · scope is a sheet or a group - sessions(id, started_at, ended_at, words_added, sheet_id) · writing sessions, which is what progress is actually made of - keywords(id, name), sheet_keywords(sheet_id, keyword_id) · stored in front matter so they travel with the file - Groups are directories and keywords are front matter, so the organisation survives without this application. ### Phase 1 · A library over a folder Build: Open a folder as a library. Directories become groups, files become sheets, front matter supplies the title and keywords. Watch for external changes. Never move a file without the user asking. Trap: Deriving a sheet's title from its filename and then letting the user rename the title in the interface creates an ambiguity: does the file move? Decide explicitly, put the title in front matter, and leave filenames alone unless the user renames them deliberately. Done when: A folder of Markdown files opens as a library with directories as groups. Editing a file in another editor updates the view. Renaming a sheet's title changes front matter and leaves the filename alone. Moving a file on disk moves it in the library after a refresh. Do not build yet: No editor features, goals or export. ### Phase 2 · An editor that stays out of the way Build: A Markdown editor with syntax highlighting rather than a rich text surface. Typewriter scrolling and a focus mode. Debounced atomic saves. A word count that updates without stuttering on a long sheet. Trap: Writing directly over the file means a crash mid-save can truncate a chapter, which for a writer is the worst possible failure. Write to a temporary file and rename over the target, which is atomic and cannot leave a half-written sheet. Done when: Typing and quitting immediately leaves the text on disk. Killing the process during a save leaves either the old file or the new one. A ten thousand word sheet types without input lag and its word count stays current. Focus mode dims everything but the current paragraph. Do not build yet: No goals or export yet. ### Phase 3 · Goals that are honest Build: Goals by word or character count, on a sheet or a group, with an optional deadline. Track writing sessions and words added per session. Show progress without streaks, grades or any language that treats a missed day as a failure. Trap: Counting net words means a day spent cutting shows as negative progress, which is exactly backwards: editing is writing. Track words added and words removed separately, and show both rather than a single number that punishes revision. Done when: A word goal on a group sums its sheets correctly. A session that removed words shows both added and removed rather than a negative total. Progress is shown without a streak counter or a grade. A deadline that passes shows the goal as unmet without any judgement language. Do not build yet: No editing passes or export yet. ### Phase 4 · Editing passes with a visible diff Build: Optional editing passes: readability, repetition and a grammar check through a local service. Present every result as a diff. Accept in whole or in part. Keep the original text so an accepted change stays reversible. Trap: Applying a change directly and relying on undo loses the original as soon as anything else is typed, and a writer who accepts a rewrite and then dislikes it has no way back. Store the original with the suggestion, so acceptance remains reversible beyond the undo stack. Done when: Every suggestion is shown as a diff before it can be applied. Accepting part of a suggestion applies only that part. The original is recoverable after acceptance and further editing. Disabling all passes leaves the editor completely unchanged, with no network activity. Do not build yet: No export yet. ### Phase 5 · Export Build: Export a sheet or a group to formatted output with a chosen style. Compile a group in the library's order with a separator you configure. Include or exclude sheets by keyword. Never modify the source files. Trap: Compiling in filesystem order rather than the library's explicit order will reorder a book, and the mistake is invisible until someone reads the export. Store an explicit position and use it, since alphabetical filenames stop working at chapter ten. Done when: A group exports as one document in the library's order, verified against a group where alphabetical and intended order differ. Excluding a keyword removes those sheets from the export. Source files are unmodified after export, verified by checksum. Exporting twice produces identical output. Do not build yet: Sync and distribution come last. ### Phase 6 · Sync, backup and leaving Build: Sync the library folder with a file sync tool or version control, and surface conflicts rather than hiding them. Keep the index outside the synced folder. Confirm the library is fully usable in another editor. Trap: Putting the search index inside the synced folder means two machines fight over the same database file and corrupt it. Keep it outside, and treat it as disposable so a corruption is a rebuild rather than a loss. Done when: A simultaneous edit on two machines surfaces as a visible conflict rather than a silent loss. The index lives outside the synced folder. Opening the library folder in a plain Markdown editor shows readable sheets with front matter. Deleting the index and relaunching rebuilds it with identical search results. Do not build yet: Nothing after this. ### Out of scope (and why) - The native polish of the commercial application, which is a decade of attention to how writing feels rather than a feature list. - Sync built into the application, and the mobile applications that make it work everywhere. - Publishing integrations that post directly to blogging platforms. - The library management depth that comes from years of writers asking for specific things. ### README must contain - That the library is a folder of Markdown files and the application is not required to read them - That groups are directories and keywords are front matter, so organisation survives without this - That saves are atomic and a crash cannot truncate a sheet - That goals count words added and removed separately, because editing is writing - That the index lives outside the library folder and is disposable ===== AGENTS.md ===== # Agent instructions · Ulysses indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri with a TypeScript front end, Plain Markdown, one file per sheet, in a folder you choose, SQLite for search, groups and goals, treated as disposable, A text editor built on a code editing component with Markdown syntax, Markdown to formatted output through a document toolchain. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Deriving a sheet's title from its filename and then letting the user rename the title in the interface creates an ambiguity: does the file move? Decide explicitly, put the title in front matter, and leave filenames alone unless the user renames them deliberately. - Writing directly over the file means a crash mid-save can truncate a chapter, which for a writer is the worst possible failure. Write to a temporary file and rename over the target, which is atomic and cannot leave a half-written sheet. - Counting net words means a day spent cutting shows as negative progress, which is exactly backwards: editing is writing. Track words added and words removed separately, and show both rather than a single number that punishes revision. - Applying a change directly and relying on undo loses the original as soon as anything else is typed, and a writer who accepts a rewrite and then dislikes it has no way back. Store the original with the suggestion, so acceptance remains reversible beyond the undo stack. - Compiling in filesystem order rather than the library's explicit order will reorder a book, and the mistake is invisible until someone reads the export. Store an explicit position and use it, since alphabetical filenames stop working at chapter ten. ===== BUILD_PLAN.md ===== # Build plan · Ulysses A writing environment over a folder of Markdown files: a sheet library with groups and filters, a typewriter-style editor, word and character goals with honest progress, optional editing passes shown as a diff, and export to formatted documents with the source always readable elsewhere. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A library over a folder Your Markdown files appear as a browsable library, and the structure on disk matches what you see. ### Steps 1. Map directories and files to groups and sheets The structure on disk is the structure in the application. No hidden database-only organisation. Files: `src/library.ts` ```sh npm create tauri-app@latest ``` 2. Read front matter for title and keywords So they travel with the file and are visible in any editor. Files: `src/frontmatter.ts` 3. Watch without fighting yourself Hash what you write and ignore your own events, or the watcher will reload under the cursor. Files: `src/watcher.ts` ### Done when - [ ] A folder opens as a library with directories as groups - [ ] External edits appear in the view - [ ] Renaming a title changes front matter, not the filename - [ ] Moving a file on disk moves it in the library ### Watch out - Deriving a sheet's title from its filename and then letting the user rename the title in the interface creates an ambiguity: does the file move? Decide explicitly, put the title in front matter, and leave filenames alone unless the user renames them deliberately. ## Phase 2 · An editor that stays out of the way Writing feels quiet, and nothing is ever lost. ### Steps 1. Use a code editing component with Markdown syntax Highlighting rather than rendering. A rich text surface fights Markdown and loses. Files: `src/editor.ts` 2. Save atomically with debounce Temporary file plus rename. Test by killing the process mid-save. Files: `src/save.ts` 3. Count words without stuttering Incrementally or off the main thread. Recounting a long sheet on every keystroke is felt immediately. Files: `src/count.ts` ### Done when - [ ] Text typed and immediately quit is on disk - [ ] A killed save leaves a complete file - [ ] A ten thousand word sheet types without lag - [ ] Focus mode dims everything but the current paragraph ### Watch out - Writing directly over the file means a crash mid-save can truncate a chapter, which for a writer is the worst possible failure. Write to a temporary file and rename over the target, which is atomic and cannot leave a half-written sheet. ## Phase 3 · Goals that are honest Progress is measured from what you actually wrote, including the days you did not. ### Steps 1. Track sessions with added and removed Both numbers. A day of cutting is progress and the interface should not disagree. Files: `src/sessions.ts` 2. Sum goals across a scope Sheet or group, computed from current counts rather than a running total that can drift. Files: `src/goals.ts` 3. Keep the language neutral No streaks, no grades, no red. A missed deadline is a fact, not a verdict. Files: `src/ui/Goal.tsx` ### Done when - [ ] A group goal sums its sheets correctly - [ ] A cutting session shows added and removed separately - [ ] No streak counter or grade appears anywhere - [ ] A passed deadline is stated without judgement ### Watch out - Counting net words means a day spent cutting shows as negative progress, which is exactly backwards: editing is writing. Track words added and words removed separately, and show both rather than a single number that punishes revision. ## Phase 4 · Editing passes with a visible diff Suggested changes are shown before they are applied, and the original survives. ### Steps 1. Run passes locally where possible A local grammar service keeps the manuscript on the machine. Say clearly if any pass does not. Files: `src/passes/` 2. Diff at word level Sentence-level diffs on prose are unreadable. Word level with sentence grouping is what a writer can judge. Files: `src/diff.ts` 3. Preserve originals On the suggestion record, so acceptance is reversible beyond undo. Files: `src/suggestions.ts` ### Done when - [ ] Every suggestion is shown as a diff first - [ ] Partial acceptance applies only the selected part - [ ] The original is recoverable after acceptance and further edits - [ ] Disabling passes produces no network activity ### Watch out - Applying a change directly and relying on undo loses the original as soon as anything else is typed, and a writer who accepts a rewrite and then dislikes it has no way back. Store the original with the suggestion, so acceptance remains reversible beyond the undo stack. ## Phase 5 · Export A group of sheets becomes one formatted document, and the source is untouched. ### Steps 1. Compile in explicit order The stored position, not the filename. Test with a group where the two disagree. Files: `src/export.ts` 2. Filter by keyword Include or exclude, so drafts and notes can live in the same group as the manuscript. Files: `src/export.ts` 3. Never touch the source Checksum before and after as a test. Files: `src/export.test.ts` ### Done when - [ ] A group exports in the library's order, not alphabetically - [ ] Keyword filtering excludes the right sheets - [ ] Source checksums are unchanged after export - [ ] Two exports of the same content are identical ### Watch out - Compiling in filesystem order rather than the library's explicit order will reorder a book, and the mistake is invisible until someone reads the export. Store an explicit position and use it, since alphabetical filenames stop working at chapter ten. ## Not in this build - The native polish of the commercial application. - Built-in sync and mobile applications. - Publishing integrations. - A decade of library management refinements. ## After v1, if you want it - Split view for writing against notes, which is the most missed feature in a single-pane editor - Revision history from version control surfaced in the interface - Custom export styles as files, so a manuscript format is version controlled ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The folder of Markdown files. Your data, in your backup. LIBRARY_PATH=~/Writing/Library # Required. Search and goals. Deliberately outside the library so syncing cannot corrupt it. INDEX_PATH=~/.local/share/write/index.db # Required. How long after typing before writing. Lower loses less on a crash. SAVE_DEBOUNCE_MS=400 # Required. Where compiled documents are written. Never the library folder. EXPORT_DIR=~/Writing/Exports # Optional. Optional local grammar service. Leave unset and the editing passes are disabled entirely. GRAMMAR_ENDPOINT=http://localhost:8081
You are building a production product version of Ulysses. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Ulysses · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Ulysses, write in a focused Markdown library, organize sheets, set goals, and export polished documents. The hard boundary is native apple polish, icloud sync, mobile apps, and publishing integrations, plus polish, integrations, and proprietary language models. ## Product outcome A writing environment where the library is a folder you could open in anything, saves cannot truncate a sheet, goals count editing as work, suggestions are reversible diffs, and compiled exports never touch the source. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - macOS, Windows, or Linux - local files - optional OpenAI or Anthropic API key for semantic rewrites ## Explicit non-goals for v1 - The native polish of the commercial application. - Built-in sync and mobile applications. - Publishing integrations. - A decade of library management refinements. - native Apple polish, iCloud sync, mobile apps, and publishing integrations - cross-device sync - large proprietary style models - browser and office-suite extensions - team style guides ## Success criteria - A killed process during save is confirmed to leave a complete file - Compile order is confirmed correct on a group where alphabetical and intended order differ - Source checksums are unchanged after export - The library is confirmed readable in a second Markdown editor - Index rebuild produces identical search results ===== BRIEF.md ===== # Build brief · Ulysses The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a distraction-light Markdown writing library with sheets, goals and export that stays out of the way to replace Ulysses. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Shell: Tauri with a TypeScript front end · real file access and a small binary, on every desktop platform - Files: Plain Markdown, one file per sheet, in a folder you choose · the library is a directory; the application is a way of looking at it - Index: SQLite for search, groups and goals, treated as disposable · the files are the truth and the index rebuilds from them - Editor: A text editor built on a code editing component with Markdown syntax · reliable text editing is harder than it looks and is not where your effort belongs - Export: Markdown to formatted output through a document toolchain · one source, several destinations, none of which change the source ### Data model (create this before Phase 1) - sheets(id, path, title, group_id, position, word_count, char_count, mtime, content_hash) · one Markdown file each - groups(id, parent_id, name, position) · directories on disk, so the structure is visible outside the application - goals(id, target_kind, target_value, scope_kind, scope_id, deadline) · scope is a sheet or a group - sessions(id, started_at, ended_at, words_added, sheet_id) · writing sessions, which is what progress is actually made of - keywords(id, name), sheet_keywords(sheet_id, keyword_id) · stored in front matter so they travel with the file - Groups are directories and keywords are front matter, so the organisation survives without this application. ### Phase 1 · A library over a folder Build: Open a folder as a library. Directories become groups, files become sheets, front matter supplies the title and keywords. Watch for external changes. Never move a file without the user asking. Trap: Deriving a sheet's title from its filename and then letting the user rename the title in the interface creates an ambiguity: does the file move? Decide explicitly, put the title in front matter, and leave filenames alone unless the user renames them deliberately. Done when: A folder of Markdown files opens as a library with directories as groups. Editing a file in another editor updates the view. Renaming a sheet's title changes front matter and leaves the filename alone. Moving a file on disk moves it in the library after a refresh. Do not build yet: No editor features, goals or export. ### Phase 2 · An editor that stays out of the way Build: A Markdown editor with syntax highlighting rather than a rich text surface. Typewriter scrolling and a focus mode. Debounced atomic saves. A word count that updates without stuttering on a long sheet. Trap: Writing directly over the file means a crash mid-save can truncate a chapter, which for a writer is the worst possible failure. Write to a temporary file and rename over the target, which is atomic and cannot leave a half-written sheet. Done when: Typing and quitting immediately leaves the text on disk. Killing the process during a save leaves either the old file or the new one. A ten thousand word sheet types without input lag and its word count stays current. Focus mode dims everything but the current paragraph. Do not build yet: No goals or export yet. ### Phase 3 · Goals that are honest Build: Goals by word or character count, on a sheet or a group, with an optional deadline. Track writing sessions and words added per session. Show progress without streaks, grades or any language that treats a missed day as a failure. Trap: Counting net words means a day spent cutting shows as negative progress, which is exactly backwards: editing is writing. Track words added and words removed separately, and show both rather than a single number that punishes revision. Done when: A word goal on a group sums its sheets correctly. A session that removed words shows both added and removed rather than a negative total. Progress is shown without a streak counter or a grade. A deadline that passes shows the goal as unmet without any judgement language. Do not build yet: No editing passes or export yet. ### Phase 4 · Editing passes with a visible diff Build: Optional editing passes: readability, repetition and a grammar check through a local service. Present every result as a diff. Accept in whole or in part. Keep the original text so an accepted change stays reversible. Trap: Applying a change directly and relying on undo loses the original as soon as anything else is typed, and a writer who accepts a rewrite and then dislikes it has no way back. Store the original with the suggestion, so acceptance remains reversible beyond the undo stack. Done when: Every suggestion is shown as a diff before it can be applied. Accepting part of a suggestion applies only that part. The original is recoverable after acceptance and further editing. Disabling all passes leaves the editor completely unchanged, with no network activity. Do not build yet: No export yet. ### Phase 5 · Export Build: Export a sheet or a group to formatted output with a chosen style. Compile a group in the library's order with a separator you configure. Include or exclude sheets by keyword. Never modify the source files. Trap: Compiling in filesystem order rather than the library's explicit order will reorder a book, and the mistake is invisible until someone reads the export. Store an explicit position and use it, since alphabetical filenames stop working at chapter ten. Done when: A group exports as one document in the library's order, verified against a group where alphabetical and intended order differ. Excluding a keyword removes those sheets from the export. Source files are unmodified after export, verified by checksum. Exporting twice produces identical output. Do not build yet: Sync and distribution come last. ### Phase 6 · Sync, backup and leaving Build: Sync the library folder with a file sync tool or version control, and surface conflicts rather than hiding them. Keep the index outside the synced folder. Confirm the library is fully usable in another editor. Trap: Putting the search index inside the synced folder means two machines fight over the same database file and corrupt it. Keep it outside, and treat it as disposable so a corruption is a rebuild rather than a loss. Done when: A simultaneous edit on two machines surfaces as a visible conflict rather than a silent loss. The index lives outside the synced folder. Opening the library folder in a plain Markdown editor shows readable sheets with front matter. Deleting the index and relaunching rebuilds it with identical search results. Do not build yet: Nothing after this. ### Out of scope (and why) - The native polish of the commercial application, which is a decade of attention to how writing feels rather than a feature list. - Sync built into the application, and the mobile applications that make it work everywhere. - Publishing integrations that post directly to blogging platforms. - The library management depth that comes from years of writers asking for specific things. ### README must contain - That the library is a folder of Markdown files and the application is not required to read them - That groups are directories and keywords are front matter, so organisation survives without this - That saves are atomic and a crash cannot truncate a sheet - That goals count words added and removed separately, because editing is writing - That the index lives outside the library folder and is disposable ===== ARCHITECTURE.md ===== # Architecture · Ulysses ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri with a TypeScript front end | real file access and a small binary, on every desktop platform | | Files | Plain Markdown, one file per sheet, in a folder you choose | the library is a directory; the application is a way of looking at it | | Index | SQLite for search, groups and goals, treated as disposable | the files are the truth and the index rebuilds from them | | Editor | A text editor built on a code editing component with Markdown syntax | reliable text editing is harder than it looks and is not where your effort belongs | | Export | Markdown to formatted output through a document toolchain | one source, several destinations, none of which change the source | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Library | Mapping the folder to groups and sheets. | Not swappable without giving up the ownership property that justifies the design. | | Editor | Text editing and atomic saves. | Any editing component. The atomic save is a correctness property rather than an implementation detail. | | Goals | Sessions, counts and neutral presentation. | Pure computation over sessions. The added-and-removed distinction should survive any change. | | Passes | Optional editing suggestions. | Entirely optional and off by default. Whatever provides them, the diff and preserved original stay. | | Export | Compiling in explicit order. | Any toolchain, provided compile order comes from stored positions rather than filenames. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `LIBRARY_PATH` · required · The folder of Markdown files. Your data, in your backup. - `INDEX_PATH` · required · Search and goals. Deliberately outside the library so syncing cannot corrupt it. - `SAVE_DEBOUNCE_MS` · required · How long after typing before writing. Lower loses less on a crash. - `EXPORT_DIR` · required · Where compiled documents are written. Never the library folder. - `GRAMMAR_ENDPOINT` · optional · Optional local grammar service. Leave unset and the editing passes are disabled entirely. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Ulysses product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri with a TypeScript front end, Plain Markdown, one file per sheet, in a folder you choose, SQLite for search, groups and goals, treated as disposable, A text editor built on a code editing component with Markdown syntax, Markdown to formatted output through a document toolchain. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Deriving a sheet's title from its filename and then letting the user rename the title in the interface creates an ambiguity: does the file move? Decide explicitly, put the title in front matter, and leave filenames alone unless the user renames them deliberately. - Writing directly over the file means a crash mid-save can truncate a chapter, which for a writer is the worst possible failure. Write to a temporary file and rename over the target, which is atomic and cannot leave a half-written sheet. - Counting net words means a day spent cutting shows as negative progress, which is exactly backwards: editing is writing. Track words added and words removed separately, and show both rather than a single number that punishes revision. - Applying a change directly and relying on undo loses the original as soon as anything else is typed, and a writer who accepts a rewrite and then dislikes it has no way back. Store the original with the suggestion, so acceptance remains reversible beyond the undo stack. - Compiling in filesystem order rather than the library's explicit order will reorder a book, and the mistake is invisible until someone reads the export. Store an explicit position and use it, since alphabetical filenames stop working at chapter ten. - Putting the search index inside the synced folder means two machines fight over the same database file and corrupt it. Keep it outside, and treat it as disposable so a corruption is a rebuild rather than a loss. ===== MILESTONES.md ===== # Delivery milestones · Ulysses Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A library over a folder Your Markdown files appear as a browsable library, and the structure on disk matches what you see. ### Steps 1. Map directories and files to groups and sheets The structure on disk is the structure in the application. No hidden database-only organisation. Files: `src/library.ts` ```sh npm create tauri-app@latest ``` 2. Read front matter for title and keywords So they travel with the file and are visible in any editor. Files: `src/frontmatter.ts` 3. Watch without fighting yourself Hash what you write and ignore your own events, or the watcher will reload under the cursor. Files: `src/watcher.ts` ### Done when - [ ] A folder opens as a library with directories as groups - [ ] External edits appear in the view - [ ] Renaming a title changes front matter, not the filename - [ ] Moving a file on disk moves it in the library ### Watch out - Deriving a sheet's title from its filename and then letting the user rename the title in the interface creates an ambiguity: does the file move? Decide explicitly, put the title in front matter, and leave filenames alone unless the user renames them deliberately. ## M2 · An editor that stays out of the way Writing feels quiet, and nothing is ever lost. ### Steps 1. Use a code editing component with Markdown syntax Highlighting rather than rendering. A rich text surface fights Markdown and loses. Files: `src/editor.ts` 2. Save atomically with debounce Temporary file plus rename. Test by killing the process mid-save. Files: `src/save.ts` 3. Count words without stuttering Incrementally or off the main thread. Recounting a long sheet on every keystroke is felt immediately. Files: `src/count.ts` ### Done when - [ ] Text typed and immediately quit is on disk - [ ] A killed save leaves a complete file - [ ] A ten thousand word sheet types without lag - [ ] Focus mode dims everything but the current paragraph ### Watch out - Writing directly over the file means a crash mid-save can truncate a chapter, which for a writer is the worst possible failure. Write to a temporary file and rename over the target, which is atomic and cannot leave a half-written sheet. ## M3 · Goals that are honest Progress is measured from what you actually wrote, including the days you did not. ### Steps 1. Track sessions with added and removed Both numbers. A day of cutting is progress and the interface should not disagree. Files: `src/sessions.ts` 2. Sum goals across a scope Sheet or group, computed from current counts rather than a running total that can drift. Files: `src/goals.ts` 3. Keep the language neutral No streaks, no grades, no red. A missed deadline is a fact, not a verdict. Files: `src/ui/Goal.tsx` ### Done when - [ ] A group goal sums its sheets correctly - [ ] A cutting session shows added and removed separately - [ ] No streak counter or grade appears anywhere - [ ] A passed deadline is stated without judgement ### Watch out - Counting net words means a day spent cutting shows as negative progress, which is exactly backwards: editing is writing. Track words added and words removed separately, and show both rather than a single number that punishes revision. ## M4 · Editing passes with a visible diff Suggested changes are shown before they are applied, and the original survives. ### Steps 1. Run passes locally where possible A local grammar service keeps the manuscript on the machine. Say clearly if any pass does not. Files: `src/passes/` 2. Diff at word level Sentence-level diffs on prose are unreadable. Word level with sentence grouping is what a writer can judge. Files: `src/diff.ts` 3. Preserve originals On the suggestion record, so acceptance is reversible beyond undo. Files: `src/suggestions.ts` ### Done when - [ ] Every suggestion is shown as a diff first - [ ] Partial acceptance applies only the selected part - [ ] The original is recoverable after acceptance and further edits - [ ] Disabling passes produces no network activity ### Watch out - Applying a change directly and relying on undo loses the original as soon as anything else is typed, and a writer who accepts a rewrite and then dislikes it has no way back. Store the original with the suggestion, so acceptance remains reversible beyond the undo stack. ## M5 · Export A group of sheets becomes one formatted document, and the source is untouched. ### Steps 1. Compile in explicit order The stored position, not the filename. Test with a group where the two disagree. Files: `src/export.ts` 2. Filter by keyword Include or exclude, so drafts and notes can live in the same group as the manuscript. Files: `src/export.ts` 3. Never touch the source Checksum before and after as a test. Files: `src/export.test.ts` ### Done when - [ ] A group exports in the library's order, not alphabetically - [ ] Keyword filtering excludes the right sheets - [ ] Source checksums are unchanged after export - [ ] Two exports of the same content are identical ### Watch out - Compiling in filesystem order rather than the library's explicit order will reorder a book, and the mistake is invisible until someone reads the export. Store an explicit position and use it, since alphabetical filenames stop working at chapter ten. ## M6 · Sync, backup and leaving (production only) The library moves between machines, and leaving costs nothing. ### Steps 1. Choose and document the sync mechanism Version control gives history and explicit conflicts; a sync drive is simpler and hides them. Files: `README.md` 2. Surface conflict files Watch for the naming patterns sync tools produce and list them as sheets needing attention. Files: `src/conflicts.ts` 3. Keep the index out of the folder Outside, disposable, rebuildable. Verify the rebuild produces identical results. Files: `src/index.ts` ### Done when - [ ] A simultaneous edit surfaces as a visible conflict - [ ] The index lives outside the synced folder - [ ] The library reads correctly in a plain Markdown editor - [ ] Index rebuild produces identical search results ### Watch out - Putting the search index inside the synced folder means two machines fight over the same database file and corrupt it. Keep it outside, and treat it as disposable so a corruption is a rebuild rather than a loss. ===== OPERATIONS.md ===== # Operations · Ulysses ## Backup The library folder is everything and is plain files. Version control it if you can, since history for writing is worth more than for most data. The index is disposable except for goal history. ## Restore Restore the folder and open it. The index rebuilds. If anything else is needed, something authoritative has leaked out of the files. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Nothing runs as a service. What is worth tracking during development is index rebuild time and editor input latency on the fifty sheet fixture, since both degrade quietly. ## Incident checklist A sync conflict is the realistic incident and is surfaced rather than resolved automatically. The second is a corrupt index, which the rebuild action handles, which is why rebuild is a visible command rather than a hidden repair. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A killed process during save is confirmed to leave a complete file - [ ] Compile order is confirmed correct on a group where alphabetical and intended order differ - [ ] Source checksums are unchanged after export - [ ] The library is confirmed readable in a second Markdown editor - [ ] Index rebuild produces identical search results ## Launch constraint Do not market omitted Ulysses capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The folder of Markdown files. Your data, in your backup. LIBRARY_PATH=~/Writing/Library # Required. Search and goals. Deliberately outside the library so syncing cannot corrupt it. INDEX_PATH=~/.local/share/write/index.db # Required. How long after typing before writing. Lower loses less on a crash. SAVE_DEBOUNCE_MS=400 # Required. Where compiled documents are written. Never the library folder. EXPORT_DIR=~/Writing/Exports # Optional. Optional local grammar service. Leave unset and the editing passes are disabled entirely. GRAMMAR_ENDPOINT=http://localhost:8081
# Ulysses · indie build A writing environment over a folder of Markdown files: a sheet library with groups and filters, a typewriter-style editor, word and character goals with honest progress, optional editing passes shown as a diff, and export to formatted documents with the source always readable elsewhere. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri with a TypeScript front end | real file access and a small binary, on every desktop platform | | Files | Plain Markdown, one file per sheet, in a folder you choose | the library is a directory; the application is a way of looking at it | | Index | SQLite for search, groups and goals, treated as disposable | the files are the truth and the index rebuilds from them | | Editor | A text editor built on a code editing component with Markdown syntax | reliable text editing is harder than it looks and is not where your effort belongs | | Export | Markdown to formatted output through a document toolchain | one source, several destinations, none of which change the source | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the front end and the tooling. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **The Rust toolchain** · free - Why: Tauri compiles the desktop shell and the file watcher. - Get it: rustup from rustup.rs, plus the platform packages Tauri lists. - Verify: A Tauri hello world builds and runs - [ ] **A document conversion toolchain** · free - Why: Phase 5 turns Markdown into the formats people actually accept. Writing a converter is not the project. - Get it: Install pandoc or an equivalent, plus a typesetting engine if you want PDF output. - Verify: A test Markdown file converts to your target format - [ ] **A real writing library of at least fifty sheets** · free - Why: Ordering, search and goals are all trivial at five sheets and revealing at fifty, and compile order in particular fails only at scale. - Get it: Use your own writing, or split a long public domain text into sheets. - Verify: The library loads and its sheet count is reported - [ ] **A second Markdown editor** · free - Why: Phase 6 verifies the library reads without this application, which is the ownership claim and cannot be tested with the application that wrote it. - Get it: Any general Markdown editor you already have. - Verify: The library folder opens and sheets read correctly ## Quick start ```sh npm create tauri-app@latest ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The native polish of the commercial application. - Built-in sync and mobile applications. - Publishing integrations. - A decade of library management refinements. - native Apple polish, iCloud sync, mobile apps, and publishing integrations - cross-device sync - large proprietary style models - browser and office-suite extensions - team style guides If one of those is essential to you, that is the reason to keep paying for Ulysses, and the README should say so rather than pretend.
# Build brief · Ulysses The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a distraction-light Markdown writing library with sheets, goals and export that stays out of the way to replace Ulysses. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Shell: Tauri with a TypeScript front end · real file access and a small binary, on every desktop platform - Files: Plain Markdown, one file per sheet, in a folder you choose · the library is a directory; the application is a way of looking at it - Index: SQLite for search, groups and goals, treated as disposable · the files are the truth and the index rebuilds from them - Editor: A text editor built on a code editing component with Markdown syntax · reliable text editing is harder than it looks and is not where your effort belongs - Export: Markdown to formatted output through a document toolchain · one source, several destinations, none of which change the source ### Data model (create this before Phase 1) - sheets(id, path, title, group_id, position, word_count, char_count, mtime, content_hash) · one Markdown file each - groups(id, parent_id, name, position) · directories on disk, so the structure is visible outside the application - goals(id, target_kind, target_value, scope_kind, scope_id, deadline) · scope is a sheet or a group - sessions(id, started_at, ended_at, words_added, sheet_id) · writing sessions, which is what progress is actually made of - keywords(id, name), sheet_keywords(sheet_id, keyword_id) · stored in front matter so they travel with the file - Groups are directories and keywords are front matter, so the organisation survives without this application. ### Phase 1 · A library over a folder Build: Open a folder as a library. Directories become groups, files become sheets, front matter supplies the title and keywords. Watch for external changes. Never move a file without the user asking. Trap: Deriving a sheet's title from its filename and then letting the user rename the title in the interface creates an ambiguity: does the file move? Decide explicitly, put the title in front matter, and leave filenames alone unless the user renames them deliberately. Done when: A folder of Markdown files opens as a library with directories as groups. Editing a file in another editor updates the view. Renaming a sheet's title changes front matter and leaves the filename alone. Moving a file on disk moves it in the library after a refresh. Do not build yet: No editor features, goals or export. ### Phase 2 · An editor that stays out of the way Build: A Markdown editor with syntax highlighting rather than a rich text surface. Typewriter scrolling and a focus mode. Debounced atomic saves. A word count that updates without stuttering on a long sheet. Trap: Writing directly over the file means a crash mid-save can truncate a chapter, which for a writer is the worst possible failure. Write to a temporary file and rename over the target, which is atomic and cannot leave a half-written sheet. Done when: Typing and quitting immediately leaves the text on disk. Killing the process during a save leaves either the old file or the new one. A ten thousand word sheet types without input lag and its word count stays current. Focus mode dims everything but the current paragraph. Do not build yet: No goals or export yet. ### Phase 3 · Goals that are honest Build: Goals by word or character count, on a sheet or a group, with an optional deadline. Track writing sessions and words added per session. Show progress without streaks, grades or any language that treats a missed day as a failure. Trap: Counting net words means a day spent cutting shows as negative progress, which is exactly backwards: editing is writing. Track words added and words removed separately, and show both rather than a single number that punishes revision. Done when: A word goal on a group sums its sheets correctly. A session that removed words shows both added and removed rather than a negative total. Progress is shown without a streak counter or a grade. A deadline that passes shows the goal as unmet without any judgement language. Do not build yet: No editing passes or export yet. ### Phase 4 · Editing passes with a visible diff Build: Optional editing passes: readability, repetition and a grammar check through a local service. Present every result as a diff. Accept in whole or in part. Keep the original text so an accepted change stays reversible. Trap: Applying a change directly and relying on undo loses the original as soon as anything else is typed, and a writer who accepts a rewrite and then dislikes it has no way back. Store the original with the suggestion, so acceptance remains reversible beyond the undo stack. Done when: Every suggestion is shown as a diff before it can be applied. Accepting part of a suggestion applies only that part. The original is recoverable after acceptance and further editing. Disabling all passes leaves the editor completely unchanged, with no network activity. Do not build yet: No export yet. ### Phase 5 · Export Build: Export a sheet or a group to formatted output with a chosen style. Compile a group in the library's order with a separator you configure. Include or exclude sheets by keyword. Never modify the source files. Trap: Compiling in filesystem order rather than the library's explicit order will reorder a book, and the mistake is invisible until someone reads the export. Store an explicit position and use it, since alphabetical filenames stop working at chapter ten. Done when: A group exports as one document in the library's order, verified against a group where alphabetical and intended order differ. Excluding a keyword removes those sheets from the export. Source files are unmodified after export, verified by checksum. Exporting twice produces identical output. Do not build yet: Sync and distribution come last. ### Phase 6 · Sync, backup and leaving Build: Sync the library folder with a file sync tool or version control, and surface conflicts rather than hiding them. Keep the index outside the synced folder. Confirm the library is fully usable in another editor. Trap: Putting the search index inside the synced folder means two machines fight over the same database file and corrupt it. Keep it outside, and treat it as disposable so a corruption is a rebuild rather than a loss. Done when: A simultaneous edit on two machines surfaces as a visible conflict rather than a silent loss. The index lives outside the synced folder. Opening the library folder in a plain Markdown editor shows readable sheets with front matter. Deleting the index and relaunching rebuilds it with identical search results. Do not build yet: Nothing after this. ### Out of scope (and why) - The native polish of the commercial application, which is a decade of attention to how writing feels rather than a feature list. - Sync built into the application, and the mobile applications that make it work everywhere. - Publishing integrations that post directly to blogging platforms. - The library management depth that comes from years of writers asking for specific things. ### README must contain - That the library is a folder of Markdown files and the application is not required to read them - That groups are directories and keywords are front matter, so organisation survives without this - That saves are atomic and a crash cannot truncate a sheet - That goals count words added and removed separately, because editing is writing - That the index lives outside the library folder and is disposable
# Agent instructions · Ulysses indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri with a TypeScript front end, Plain Markdown, one file per sheet, in a folder you choose, SQLite for search, groups and goals, treated as disposable, A text editor built on a code editing component with Markdown syntax, Markdown to formatted output through a document toolchain. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Deriving a sheet's title from its filename and then letting the user rename the title in the interface creates an ambiguity: does the file move? Decide explicitly, put the title in front matter, and leave filenames alone unless the user renames them deliberately. - Writing directly over the file means a crash mid-save can truncate a chapter, which for a writer is the worst possible failure. Write to a temporary file and rename over the target, which is atomic and cannot leave a half-written sheet. - Counting net words means a day spent cutting shows as negative progress, which is exactly backwards: editing is writing. Track words added and words removed separately, and show both rather than a single number that punishes revision. - Applying a change directly and relying on undo loses the original as soon as anything else is typed, and a writer who accepts a rewrite and then dislikes it has no way back. Store the original with the suggestion, so acceptance remains reversible beyond the undo stack. - Compiling in filesystem order rather than the library's explicit order will reorder a book, and the mistake is invisible until someone reads the export. Store an explicit position and use it, since alphabetical filenames stop working at chapter ten.
# Build plan · Ulysses A writing environment over a folder of Markdown files: a sheet library with groups and filters, a typewriter-style editor, word and character goals with honest progress, optional editing passes shown as a diff, and export to formatted documents with the source always readable elsewhere. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A library over a folder Your Markdown files appear as a browsable library, and the structure on disk matches what you see. ### Steps 1. Map directories and files to groups and sheets The structure on disk is the structure in the application. No hidden database-only organisation. Files: `src/library.ts` ```sh npm create tauri-app@latest ``` 2. Read front matter for title and keywords So they travel with the file and are visible in any editor. Files: `src/frontmatter.ts` 3. Watch without fighting yourself Hash what you write and ignore your own events, or the watcher will reload under the cursor. Files: `src/watcher.ts` ### Done when - [ ] A folder opens as a library with directories as groups - [ ] External edits appear in the view - [ ] Renaming a title changes front matter, not the filename - [ ] Moving a file on disk moves it in the library ### Watch out - Deriving a sheet's title from its filename and then letting the user rename the title in the interface creates an ambiguity: does the file move? Decide explicitly, put the title in front matter, and leave filenames alone unless the user renames them deliberately. ## Phase 2 · An editor that stays out of the way Writing feels quiet, and nothing is ever lost. ### Steps 1. Use a code editing component with Markdown syntax Highlighting rather than rendering. A rich text surface fights Markdown and loses. Files: `src/editor.ts` 2. Save atomically with debounce Temporary file plus rename. Test by killing the process mid-save. Files: `src/save.ts` 3. Count words without stuttering Incrementally or off the main thread. Recounting a long sheet on every keystroke is felt immediately. Files: `src/count.ts` ### Done when - [ ] Text typed and immediately quit is on disk - [ ] A killed save leaves a complete file - [ ] A ten thousand word sheet types without lag - [ ] Focus mode dims everything but the current paragraph ### Watch out - Writing directly over the file means a crash mid-save can truncate a chapter, which for a writer is the worst possible failure. Write to a temporary file and rename over the target, which is atomic and cannot leave a half-written sheet. ## Phase 3 · Goals that are honest Progress is measured from what you actually wrote, including the days you did not. ### Steps 1. Track sessions with added and removed Both numbers. A day of cutting is progress and the interface should not disagree. Files: `src/sessions.ts` 2. Sum goals across a scope Sheet or group, computed from current counts rather than a running total that can drift. Files: `src/goals.ts` 3. Keep the language neutral No streaks, no grades, no red. A missed deadline is a fact, not a verdict. Files: `src/ui/Goal.tsx` ### Done when - [ ] A group goal sums its sheets correctly - [ ] A cutting session shows added and removed separately - [ ] No streak counter or grade appears anywhere - [ ] A passed deadline is stated without judgement ### Watch out - Counting net words means a day spent cutting shows as negative progress, which is exactly backwards: editing is writing. Track words added and words removed separately, and show both rather than a single number that punishes revision. ## Phase 4 · Editing passes with a visible diff Suggested changes are shown before they are applied, and the original survives. ### Steps 1. Run passes locally where possible A local grammar service keeps the manuscript on the machine. Say clearly if any pass does not. Files: `src/passes/` 2. Diff at word level Sentence-level diffs on prose are unreadable. Word level with sentence grouping is what a writer can judge. Files: `src/diff.ts` 3. Preserve originals On the suggestion record, so acceptance is reversible beyond undo. Files: `src/suggestions.ts` ### Done when - [ ] Every suggestion is shown as a diff first - [ ] Partial acceptance applies only the selected part - [ ] The original is recoverable after acceptance and further edits - [ ] Disabling passes produces no network activity ### Watch out - Applying a change directly and relying on undo loses the original as soon as anything else is typed, and a writer who accepts a rewrite and then dislikes it has no way back. Store the original with the suggestion, so acceptance remains reversible beyond the undo stack. ## Phase 5 · Export A group of sheets becomes one formatted document, and the source is untouched. ### Steps 1. Compile in explicit order The stored position, not the filename. Test with a group where the two disagree. Files: `src/export.ts` 2. Filter by keyword Include or exclude, so drafts and notes can live in the same group as the manuscript. Files: `src/export.ts` 3. Never touch the source Checksum before and after as a test. Files: `src/export.test.ts` ### Done when - [ ] A group exports in the library's order, not alphabetically - [ ] Keyword filtering excludes the right sheets - [ ] Source checksums are unchanged after export - [ ] Two exports of the same content are identical ### Watch out - Compiling in filesystem order rather than the library's explicit order will reorder a book, and the mistake is invisible until someone reads the export. Store an explicit position and use it, since alphabetical filenames stop working at chapter ten. ## Not in this build - The native polish of the commercial application. - Built-in sync and mobile applications. - Publishing integrations. - A decade of library management refinements. ## After v1, if you want it - Split view for writing against notes, which is the most missed feature in a single-pane editor - Revision history from version control surfaced in the interface - Custom export styles as files, so a manuscript format is version controlled
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. The folder of Markdown files. Your data, in your backup. LIBRARY_PATH=~/Writing/Library # Required. Search and goals. Deliberately outside the library so syncing cannot corrupt it. INDEX_PATH=~/.local/share/write/index.db # Required. How long after typing before writing. Lower loses less on a crash. SAVE_DEBOUNCE_MS=400 # Required. Where compiled documents are written. Never the library folder. EXPORT_DIR=~/Writing/Exports # Optional. Optional local grammar service. Leave unset and the editing passes are disabled entirely. GRAMMAR_ENDPOINT=http://localhost:8081
# Ulysses · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Ulysses, write in a focused Markdown library, organize sheets, set goals, and export polished documents. The hard boundary is native apple polish, icloud sync, mobile apps, and publishing integrations, plus polish, integrations, and proprietary language models. ## Product outcome A writing environment where the library is a folder you could open in anything, saves cannot truncate a sheet, goals count editing as work, suggestions are reversible diffs, and compiled exports never touch the source. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - macOS, Windows, or Linux - local files - optional OpenAI or Anthropic API key for semantic rewrites ## Explicit non-goals for v1 - The native polish of the commercial application. - Built-in sync and mobile applications. - Publishing integrations. - A decade of library management refinements. - native Apple polish, iCloud sync, mobile apps, and publishing integrations - cross-device sync - large proprietary style models - browser and office-suite extensions - team style guides ## Success criteria - A killed process during save is confirmed to leave a complete file - Compile order is confirmed correct on a group where alphabetical and intended order differ - Source checksums are unchanged after export - The library is confirmed readable in a second Markdown editor - Index rebuild produces identical search results
# Build brief · Ulysses The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a distraction-light Markdown writing library with sheets, goals and export that stays out of the way to replace Ulysses. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Shell: Tauri with a TypeScript front end · real file access and a small binary, on every desktop platform - Files: Plain Markdown, one file per sheet, in a folder you choose · the library is a directory; the application is a way of looking at it - Index: SQLite for search, groups and goals, treated as disposable · the files are the truth and the index rebuilds from them - Editor: A text editor built on a code editing component with Markdown syntax · reliable text editing is harder than it looks and is not where your effort belongs - Export: Markdown to formatted output through a document toolchain · one source, several destinations, none of which change the source ### Data model (create this before Phase 1) - sheets(id, path, title, group_id, position, word_count, char_count, mtime, content_hash) · one Markdown file each - groups(id, parent_id, name, position) · directories on disk, so the structure is visible outside the application - goals(id, target_kind, target_value, scope_kind, scope_id, deadline) · scope is a sheet or a group - sessions(id, started_at, ended_at, words_added, sheet_id) · writing sessions, which is what progress is actually made of - keywords(id, name), sheet_keywords(sheet_id, keyword_id) · stored in front matter so they travel with the file - Groups are directories and keywords are front matter, so the organisation survives without this application. ### Phase 1 · A library over a folder Build: Open a folder as a library. Directories become groups, files become sheets, front matter supplies the title and keywords. Watch for external changes. Never move a file without the user asking. Trap: Deriving a sheet's title from its filename and then letting the user rename the title in the interface creates an ambiguity: does the file move? Decide explicitly, put the title in front matter, and leave filenames alone unless the user renames them deliberately. Done when: A folder of Markdown files opens as a library with directories as groups. Editing a file in another editor updates the view. Renaming a sheet's title changes front matter and leaves the filename alone. Moving a file on disk moves it in the library after a refresh. Do not build yet: No editor features, goals or export. ### Phase 2 · An editor that stays out of the way Build: A Markdown editor with syntax highlighting rather than a rich text surface. Typewriter scrolling and a focus mode. Debounced atomic saves. A word count that updates without stuttering on a long sheet. Trap: Writing directly over the file means a crash mid-save can truncate a chapter, which for a writer is the worst possible failure. Write to a temporary file and rename over the target, which is atomic and cannot leave a half-written sheet. Done when: Typing and quitting immediately leaves the text on disk. Killing the process during a save leaves either the old file or the new one. A ten thousand word sheet types without input lag and its word count stays current. Focus mode dims everything but the current paragraph. Do not build yet: No goals or export yet. ### Phase 3 · Goals that are honest Build: Goals by word or character count, on a sheet or a group, with an optional deadline. Track writing sessions and words added per session. Show progress without streaks, grades or any language that treats a missed day as a failure. Trap: Counting net words means a day spent cutting shows as negative progress, which is exactly backwards: editing is writing. Track words added and words removed separately, and show both rather than a single number that punishes revision. Done when: A word goal on a group sums its sheets correctly. A session that removed words shows both added and removed rather than a negative total. Progress is shown without a streak counter or a grade. A deadline that passes shows the goal as unmet without any judgement language. Do not build yet: No editing passes or export yet. ### Phase 4 · Editing passes with a visible diff Build: Optional editing passes: readability, repetition and a grammar check through a local service. Present every result as a diff. Accept in whole or in part. Keep the original text so an accepted change stays reversible. Trap: Applying a change directly and relying on undo loses the original as soon as anything else is typed, and a writer who accepts a rewrite and then dislikes it has no way back. Store the original with the suggestion, so acceptance remains reversible beyond the undo stack. Done when: Every suggestion is shown as a diff before it can be applied. Accepting part of a suggestion applies only that part. The original is recoverable after acceptance and further editing. Disabling all passes leaves the editor completely unchanged, with no network activity. Do not build yet: No export yet. ### Phase 5 · Export Build: Export a sheet or a group to formatted output with a chosen style. Compile a group in the library's order with a separator you configure. Include or exclude sheets by keyword. Never modify the source files. Trap: Compiling in filesystem order rather than the library's explicit order will reorder a book, and the mistake is invisible until someone reads the export. Store an explicit position and use it, since alphabetical filenames stop working at chapter ten. Done when: A group exports as one document in the library's order, verified against a group where alphabetical and intended order differ. Excluding a keyword removes those sheets from the export. Source files are unmodified after export, verified by checksum. Exporting twice produces identical output. Do not build yet: Sync and distribution come last. ### Phase 6 · Sync, backup and leaving Build: Sync the library folder with a file sync tool or version control, and surface conflicts rather than hiding them. Keep the index outside the synced folder. Confirm the library is fully usable in another editor. Trap: Putting the search index inside the synced folder means two machines fight over the same database file and corrupt it. Keep it outside, and treat it as disposable so a corruption is a rebuild rather than a loss. Done when: A simultaneous edit on two machines surfaces as a visible conflict rather than a silent loss. The index lives outside the synced folder. Opening the library folder in a plain Markdown editor shows readable sheets with front matter. Deleting the index and relaunching rebuilds it with identical search results. Do not build yet: Nothing after this. ### Out of scope (and why) - The native polish of the commercial application, which is a decade of attention to how writing feels rather than a feature list. - Sync built into the application, and the mobile applications that make it work everywhere. - Publishing integrations that post directly to blogging platforms. - The library management depth that comes from years of writers asking for specific things. ### README must contain - That the library is a folder of Markdown files and the application is not required to read them - That groups are directories and keywords are front matter, so organisation survives without this - That saves are atomic and a crash cannot truncate a sheet - That goals count words added and removed separately, because editing is writing - That the index lives outside the library folder and is disposable
# Architecture · Ulysses ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri with a TypeScript front end | real file access and a small binary, on every desktop platform | | Files | Plain Markdown, one file per sheet, in a folder you choose | the library is a directory; the application is a way of looking at it | | Index | SQLite for search, groups and goals, treated as disposable | the files are the truth and the index rebuilds from them | | Editor | A text editor built on a code editing component with Markdown syntax | reliable text editing is harder than it looks and is not where your effort belongs | | Export | Markdown to formatted output through a document toolchain | one source, several destinations, none of which change the source | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Library | Mapping the folder to groups and sheets. | Not swappable without giving up the ownership property that justifies the design. | | Editor | Text editing and atomic saves. | Any editing component. The atomic save is a correctness property rather than an implementation detail. | | Goals | Sessions, counts and neutral presentation. | Pure computation over sessions. The added-and-removed distinction should survive any change. | | Passes | Optional editing suggestions. | Entirely optional and off by default. Whatever provides them, the diff and preserved original stay. | | Export | Compiling in explicit order. | Any toolchain, provided compile order comes from stored positions rather than filenames. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `LIBRARY_PATH` · required · The folder of Markdown files. Your data, in your backup. - `INDEX_PATH` · required · Search and goals. Deliberately outside the library so syncing cannot corrupt it. - `SAVE_DEBOUNCE_MS` · required · How long after typing before writing. Lower loses less on a crash. - `EXPORT_DIR` · required · Where compiled documents are written. Never the library folder. - `GRAMMAR_ENDPOINT` · optional · Optional local grammar service. Leave unset and the editing passes are disabled entirely. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Ulysses product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri with a TypeScript front end, Plain Markdown, one file per sheet, in a folder you choose, SQLite for search, groups and goals, treated as disposable, A text editor built on a code editing component with Markdown syntax, Markdown to formatted output through a document toolchain. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Deriving a sheet's title from its filename and then letting the user rename the title in the interface creates an ambiguity: does the file move? Decide explicitly, put the title in front matter, and leave filenames alone unless the user renames them deliberately. - Writing directly over the file means a crash mid-save can truncate a chapter, which for a writer is the worst possible failure. Write to a temporary file and rename over the target, which is atomic and cannot leave a half-written sheet. - Counting net words means a day spent cutting shows as negative progress, which is exactly backwards: editing is writing. Track words added and words removed separately, and show both rather than a single number that punishes revision. - Applying a change directly and relying on undo loses the original as soon as anything else is typed, and a writer who accepts a rewrite and then dislikes it has no way back. Store the original with the suggestion, so acceptance remains reversible beyond the undo stack. - Compiling in filesystem order rather than the library's explicit order will reorder a book, and the mistake is invisible until someone reads the export. Store an explicit position and use it, since alphabetical filenames stop working at chapter ten. - Putting the search index inside the synced folder means two machines fight over the same database file and corrupt it. Keep it outside, and treat it as disposable so a corruption is a rebuild rather than a loss.
# Delivery milestones · Ulysses Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A library over a folder Your Markdown files appear as a browsable library, and the structure on disk matches what you see. ### Steps 1. Map directories and files to groups and sheets The structure on disk is the structure in the application. No hidden database-only organisation. Files: `src/library.ts` ```sh npm create tauri-app@latest ``` 2. Read front matter for title and keywords So they travel with the file and are visible in any editor. Files: `src/frontmatter.ts` 3. Watch without fighting yourself Hash what you write and ignore your own events, or the watcher will reload under the cursor. Files: `src/watcher.ts` ### Done when - [ ] A folder opens as a library with directories as groups - [ ] External edits appear in the view - [ ] Renaming a title changes front matter, not the filename - [ ] Moving a file on disk moves it in the library ### Watch out - Deriving a sheet's title from its filename and then letting the user rename the title in the interface creates an ambiguity: does the file move? Decide explicitly, put the title in front matter, and leave filenames alone unless the user renames them deliberately. ## M2 · An editor that stays out of the way Writing feels quiet, and nothing is ever lost. ### Steps 1. Use a code editing component with Markdown syntax Highlighting rather than rendering. A rich text surface fights Markdown and loses. Files: `src/editor.ts` 2. Save atomically with debounce Temporary file plus rename. Test by killing the process mid-save. Files: `src/save.ts` 3. Count words without stuttering Incrementally or off the main thread. Recounting a long sheet on every keystroke is felt immediately. Files: `src/count.ts` ### Done when - [ ] Text typed and immediately quit is on disk - [ ] A killed save leaves a complete file - [ ] A ten thousand word sheet types without lag - [ ] Focus mode dims everything but the current paragraph ### Watch out - Writing directly over the file means a crash mid-save can truncate a chapter, which for a writer is the worst possible failure. Write to a temporary file and rename over the target, which is atomic and cannot leave a half-written sheet. ## M3 · Goals that are honest Progress is measured from what you actually wrote, including the days you did not. ### Steps 1. Track sessions with added and removed Both numbers. A day of cutting is progress and the interface should not disagree. Files: `src/sessions.ts` 2. Sum goals across a scope Sheet or group, computed from current counts rather than a running total that can drift. Files: `src/goals.ts` 3. Keep the language neutral No streaks, no grades, no red. A missed deadline is a fact, not a verdict. Files: `src/ui/Goal.tsx` ### Done when - [ ] A group goal sums its sheets correctly - [ ] A cutting session shows added and removed separately - [ ] No streak counter or grade appears anywhere - [ ] A passed deadline is stated without judgement ### Watch out - Counting net words means a day spent cutting shows as negative progress, which is exactly backwards: editing is writing. Track words added and words removed separately, and show both rather than a single number that punishes revision. ## M4 · Editing passes with a visible diff Suggested changes are shown before they are applied, and the original survives. ### Steps 1. Run passes locally where possible A local grammar service keeps the manuscript on the machine. Say clearly if any pass does not. Files: `src/passes/` 2. Diff at word level Sentence-level diffs on prose are unreadable. Word level with sentence grouping is what a writer can judge. Files: `src/diff.ts` 3. Preserve originals On the suggestion record, so acceptance is reversible beyond undo. Files: `src/suggestions.ts` ### Done when - [ ] Every suggestion is shown as a diff first - [ ] Partial acceptance applies only the selected part - [ ] The original is recoverable after acceptance and further edits - [ ] Disabling passes produces no network activity ### Watch out - Applying a change directly and relying on undo loses the original as soon as anything else is typed, and a writer who accepts a rewrite and then dislikes it has no way back. Store the original with the suggestion, so acceptance remains reversible beyond the undo stack. ## M5 · Export A group of sheets becomes one formatted document, and the source is untouched. ### Steps 1. Compile in explicit order The stored position, not the filename. Test with a group where the two disagree. Files: `src/export.ts` 2. Filter by keyword Include or exclude, so drafts and notes can live in the same group as the manuscript. Files: `src/export.ts` 3. Never touch the source Checksum before and after as a test. Files: `src/export.test.ts` ### Done when - [ ] A group exports in the library's order, not alphabetically - [ ] Keyword filtering excludes the right sheets - [ ] Source checksums are unchanged after export - [ ] Two exports of the same content are identical ### Watch out - Compiling in filesystem order rather than the library's explicit order will reorder a book, and the mistake is invisible until someone reads the export. Store an explicit position and use it, since alphabetical filenames stop working at chapter ten. ## M6 · Sync, backup and leaving (production only) The library moves between machines, and leaving costs nothing. ### Steps 1. Choose and document the sync mechanism Version control gives history and explicit conflicts; a sync drive is simpler and hides them. Files: `README.md` 2. Surface conflict files Watch for the naming patterns sync tools produce and list them as sheets needing attention. Files: `src/conflicts.ts` 3. Keep the index out of the folder Outside, disposable, rebuildable. Verify the rebuild produces identical results. Files: `src/index.ts` ### Done when - [ ] A simultaneous edit surfaces as a visible conflict - [ ] The index lives outside the synced folder - [ ] The library reads correctly in a plain Markdown editor - [ ] Index rebuild produces identical search results ### Watch out - Putting the search index inside the synced folder means two machines fight over the same database file and corrupt it. Keep it outside, and treat it as disposable so a corruption is a rebuild rather than a loss.
# Operations · Ulysses ## Backup The library folder is everything and is plain files. Version control it if you can, since history for writing is worth more than for most data. The index is disposable except for goal history. ## Restore Restore the folder and open it. The index rebuilds. If anything else is needed, something authoritative has leaked out of the files. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Nothing runs as a service. What is worth tracking during development is index rebuild time and editor input latency on the fifty sheet fixture, since both degrade quietly. ## Incident checklist A sync conflict is the realistic incident and is surfaced rather than resolved automatically. The second is a corrupt index, which the rebuild action handles, which is why rebuild is a visible command rather than a hidden repair. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A killed process during save is confirmed to leave a complete file - [ ] Compile order is confirmed correct on a group where alphabetical and intended order differ - [ ] Source checksums are unchanged after export - [ ] The library is confirmed readable in a second Markdown editor - [ ] Index rebuild produces identical search results ## Launch constraint Do not market omitted Ulysses capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. The folder of Markdown files. Your data, in your backup. LIBRARY_PATH=~/Writing/Library # Required. Search and goals. Deliberately outside the library so syncing cannot corrupt it. INDEX_PATH=~/.local/share/write/index.db # Required. How long after typing before writing. Lower loses less on a crash. SAVE_DEBOUNCE_MS=400 # Required. Where compiled documents are written. Never the library folder. EXPORT_DIR=~/Writing/Exports # Optional. Optional local grammar service. Leave unset and the editing passes are disabled entirely. GRAMMAR_ENDPOINT=http://localhost:8081
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Ulysses because the paid tool is already present everywhere they write and its suggestions have been tuned across millions of documents. The recurring cost buys model quality, extension compatibility, dictionaries, releases, and sync, not just the visible interface.
xnative Apple polish, iCloud sync, mobile apps, and publishing integrations
xcross-device sync
xlarge proprietary style models
xbrowser and office-suite extensions
xteam style guides
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Ulysses pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| monthly | $5.99/user | — | All Macs, iPads and iPhones on the subscriber's Apple Account |
| yearly | — | $3.33/user | All Macs, iPads and iPhones; Apple Family Sharing with up to 5 other family members |
| student | — | $1.83/user | All Macs, iPads and iPhones for 6 months; no Family Sharing |
| business / volume licensing | custom | — | Custom seat count for organizations |
free tierno free tier
billing$5.99 monthly or $39.99 yearly, both auto-renew; student option is $10.99 per 6 months and does not auto-renew
hidden costsUS prices can differ by App Store region; student pricing excludes Family Sharing, and business volume pricing is quote-only.
verified 2026-08-12 · source ↗
Vibecode Ulysses
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Ulysses replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Ulysses cost?
Ulysses costs about $5.99/month (Personal, checked 2026-07-31), which is $71.88 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing Ulysses?
Honestly: native Apple polish, iCloud sync, mobile apps, and publishing integrations; cross-device sync; large proprietary style models; browser and office-suite extensions; team style guides. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Ulysses?
Yes: SiYuan (A local-first writing library with blocks, links, huge documents, and real exports; less monastic than Ulysses, much harder to lose.) Zettlr (A serious Markdown desk with folders, writing targets, citations, and export machinery that occasionally admits Pandoc is under the hood.) The prompt is for when you want it exactly your way.