aboutsummaryrefslogtreecommitdiff
path: root/README.org
diff options
context:
space:
mode:
Diffstat (limited to 'README.org')
-rw-r--r--README.org506
1 files changed, 271 insertions, 235 deletions
diff --git a/README.org b/README.org
index 633baf4..dce92a3 100644
--- a/README.org
+++ b/README.org
@@ -1,335 +1,371 @@
-#+TITLE: pearl — Linear.app for Emacs
-#+OPTIONS: toc:2 num:nil
+* Pearl - Linear.app for Emacs and Org Mode
-[[https://www.gnu.org/licenses/gpl-3.0][https://img.shields.io/badge/License-GPLv3-blue.svg]]
+[[#features][Features]] | [[#installation][Installation]] | [[#quick-start][Quick Start]] | [[#commands][Commands]] | [[#configuration][Configuration]] | [[#development--testing][Development & Testing]] | [[#faq][FAQ]] | [[#history][History]] | [[#license][License]]
-=pearl= is an integration between Emacs and [[https://linear.app][Linear.app]]: fetch, read, edit, and create Linear issues as org-mode entries without leaving Emacs. Issues render as an org outline; their descriptions and comments live in the entry body, structured fields live in a property drawer, and dedicated commands push changes back to Linear.
+[[https://www.gnu.org/licenses/gpl-3.0][https://img.shields.io/badge/License-GPLv3-blue.svg]]
-* Features
+Pearl brings Linear issues into Emacs as a working Org file. Fetch your open issues, a project, a Linear Custom View, an ad-hoc filter, or a saved local query; Pearl renders each issue as an Org heading with the description and comments in the body and Linear metadata in a namespaced property drawer. Edit what you need, then push it back explicitly with conflict-aware commands.
-- *Fetch what you want.* List your open issues, narrow by project, build an ad-hoc filter interactively, run a Linear Custom View, or run a named local saved query.
-- *Issues as readable org.* Each issue is a heading; its description renders in the body (markdown converted to org), its comments render as a chronological sub-thread, and its structured fields (state, priority, assignee, labels, team, project) live in a namespaced =LINEAR-*= drawer.
-- *Edit and push back.* Edit a description in the body and sync it; change the title; set priority, state, assignee, or labels by command; add a comment, or edit one of your own. Each push is explicit and confirmed against the remote.
-- *Conflict-aware sync.* Pushing a description or title compares the local edit, the last-fetched baseline, and the current remote — a no-op sends nothing, a clean edit pushes, and a both-sides-changed case is refused and reported rather than clobbering.
-- *Self-describing active file.* The generated file records the source it came from, so one command refreshes it; refresh a single issue at point, or the whole view.
-- *Org TODO state sync.* Changing an issue's TODO keyword in org pushes the matching Linear workflow state.
-- *One menu for everything.* =M-x pearl-menu= opens a transient dispatcher (magit-style) with every command grouped and a key away.
+** Features
+:PROPERTIES:
+:CUSTOM_ID: features
+:END:
-* Installation
+- Fetch open issues, project issues, server-side Linear Custom Views, ad-hoc filters, or named local saved queries
+- Read issues as an Org outline: title as heading, description in the body, comments as a chronological subtree
+- Keep structured fields in =LINEAR-*= properties: id, URL, team, state, priority, assignee, labels, timestamps, and sync hashes
+- Edit descriptions, titles, and your own comments in place, then push them with a three-way conflict check
+- Set priority, state, assignee, and labels by command, using Linear ids behind display-name completion
+- Add comments, create issues, delete issues, and open the current issue or view in Linear
+- Refresh the active view from the source recorded in the file, or refresh one issue at point
+- Optionally sync Org TODO keyword changes back to Linear workflow states
+- Use one transient dispatcher, =M-x pearl-menu=, for the whole command surface
+- [[file:TESTING.org][Well-tested]] with isolated ERT files, request fixtures, and coverage support
+
+** Installation
+:PROPERTIES:
+:CUSTOM_ID: installation
+:END:
-** Prerequisites
+Pearl requires Emacs 27.1+, Org, a Linear API key, and the =request=, =dash=, =s=, and =transient= packages. =transient= ships with Emacs 28+, and package managers handle the rest.
-A Linear API key (Settings → Account → API → Personal API Keys) and the =request=, =dash=, =s=, and =transient= packages (=transient= ships with Emacs 28+).
+*** MELPA
-#+begin_src elisp
-(require 'package)
-(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)
-(package-initialize)
-#+end_src
+Pearl is not on MELPA yet.
-** MELPA (coming soon)
+*** package-vc-install (Emacs 29+)
-#+begin_src
-M-x package-install RET pearl RET
+#+begin_src emacs-lisp
+ (unless (package-installed-p 'pearl)
+ (package-vc-install "https://git.cjennings.net/pearl.git"))
#+end_src
-** Manual
-
-#+begin_src shell
-git clone https://github.com/cjennings/pearl.git
+*** use-package with =:vc= (Emacs 29+)
+
+#+begin_src emacs-lisp
+ (use-package pearl
+ :vc (:url "https://git.cjennings.net/pearl.git" :rev :newest)
+ :commands (pearl-menu pearl-list-issues pearl-run-view pearl-new-issue)
+ :bind ("C-c L" . pearl-menu)
+ :custom
+ (pearl-org-file-path (expand-file-name "gtd/linear.org" org-directory))
+ :config
+ (pearl-load-api-key-from-env))
#+end_src
-#+begin_src elisp
-(add-to-list 'load-path "/path/to/pearl")
-(require 'pearl)
-;; dependencies, if not already present:
-;; M-x package-install RET request RET
-;; M-x package-install RET dash RET
-;; M-x package-install RET s RET
+*** Straight
+
+#+begin_src emacs-lisp
+ (straight-use-package
+ '(pearl :type git :repo "https://git.cjennings.net/pearl.git"))
#+end_src
-** Doom Emacs
+*** Doom Emacs
In =packages.el=:
-#+begin_src elisp
-(package! pearl
- :recipe (:host github :repo "cjennings/pearl" :files ("*.el")))
+#+begin_src emacs-lisp
+ (package! pearl
+ :recipe (:type git :repo "https://git.cjennings.net/pearl.git" :files ("*.el")))
#+end_src
In =config.el=:
-#+begin_src elisp
-(use-package! pearl
- :commands (pearl-list-issues pearl-new-issue
- pearl-run-view pearl-enable-org-sync)
- :init
- (setq pearl-org-file-path (expand-file-name "gtd/linear.org" org-directory))
- :config
- (pearl-load-api-key-from-env))
+#+begin_src emacs-lisp
+ (use-package! pearl
+ :commands (pearl-menu pearl-list-issues pearl-new-issue
+ pearl-run-view pearl-enable-org-sync)
+ :init
+ (setq pearl-org-file-path (expand-file-name "gtd/linear.org" org-directory))
+ :config
+ (pearl-load-api-key-from-env))
#+end_src
-* Configuration
-
-** The API key
-
-Three ways, least to most secure:
-
-#+begin_src elisp
-;; 1. Direct (not recommended -- ends up in your config).
-(setq pearl-api-key "lin_api_...")
+*** Manual
-;; 2. Environment variable LINEAR_API_KEY.
-(pearl-load-api-key-from-env)
+#+begin_src bash
+ git clone https://git.cjennings.net/pearl.git
#+end_src
-3. =auth-source= (recommended). Add to =~/.authinfo.gpg=:
-
-#+begin_src
-machine api.linear.app login apikey password YOUR_API_KEY
+#+begin_src emacs-lisp
+ (add-to-list 'load-path "/path/to/pearl")
+ (require 'pearl)
#+end_src
-then load it:
-
-#+begin_src elisp
-(setq pearl-api-key
- (auth-source-pick-first-password :host "api.linear.app"))
-#+end_src
+Install =request=, =dash=, and =s= first if your package manager did not already pull them in.
-The host (=api.linear.app=) and login (=apikey=) must match the entry. macOS Keychain users can instead run =security add-generic-password -a apikey -s api.linear.app -w YOUR_API_KEY=.
+** Quick Start
+:PROPERTIES:
+:CUSTOM_ID: quick-start
+:END:
-** Output file
+1. Create a Linear API key from Linear's Settings -> Account -> API -> Personal API Keys.
-Issues are written to one active file, =pearl-org-file-path= (default =gtd/linear.org= under =org-directory=):
+2. Put it somewhere Pearl can read it. The simplest development setup is:
-#+begin_src elisp
-(setq pearl-org-file-path (expand-file-name "gtd/linear.org" org-directory))
-#+end_src
+ #+begin_src sh
+ export LINEAR_API_KEY=lin_api_...
+ #+end_src
-Running a different query or view *replaces* this file's contents (behind a dirty-buffer guard); one issue appears in one place.
+ Then load it from Emacs:
-** Default team
+ #+begin_src emacs-lisp
+ (pearl-load-api-key-from-env)
+ #+end_src
-#+begin_src elisp
-(setq pearl-default-team-id "your-team-id") ; skips the team prompt on create
-#+end_src
+ For normal use, keep the key in =auth-source= instead:
-* The command menu
+ #+begin_example
+ machine api.linear.app login apikey password YOUR_API_KEY
+ #+end_example
-=M-x pearl-menu= opens a transient dispatcher with every command grouped by what it does: fetch, view, the issue at point, create and org-sync, and setup. It's the fastest way to reach a command without remembering its name. Bind it to a key if you use it often:
+ #+begin_src emacs-lisp
+ (setq pearl-api-key
+ (auth-source-pick-first-password :host "api.linear.app"))
+ #+end_src
-#+begin_src elisp
-(with-eval-after-load 'pearl
- (global-set-key (kbd "C-c L") #'pearl-menu))
-#+end_src
+3. Choose the Org file Pearl owns:
-Every command below is also available directly via =M-x=.
+ #+begin_src emacs-lisp
+ (setq pearl-org-file-path (expand-file-name "gtd/linear.org" org-directory))
+ #+end_src
-* The active org file
+4. Run =M-x pearl-menu=, or start with =M-x pearl-list-issues=.
-A fetched file carries a self-describing header and one heading per issue:
+Pearl writes one active Org file. Running a different query or view replaces that file's contents after checking for dirty buffers. Refresh commands reuse the source stored in the file header.
-#+begin_src org
-#+title: Linear — My open issues
-#+STARTUP: show3levels
-#+TODO: TODO IN-PROGRESS IN-REVIEW BACKLOG BLOCKED | DONE
-#+LINEAR-SOURCE: (:type filter :name "My open issues" :filter (:assignee :me :open t))
-#+LINEAR-RUN-AT: 2026-05-23 19:30
-#+LINEAR-FILTER: assignee: me, open
-#+LINEAR-COUNT: 12
-#+LINEAR-TRUNCATED: no
-#
-# Body = the issue description; edit it, then M-x pearl-sync-current-issue to push.
-# Comments subtree = the thread; add with M-x pearl-add-comment.
-# Drawer fields change via M-x pearl-set-priority / -state / -assignee / -labels.
-# Refresh with M-x pearl-refresh-current-view (whole file) or -current-issue (one).
-
-* My open issues
-** TODO [#B] ENG-123 Issue title
+** Commands
:PROPERTIES:
-:LINEAR-ID: <uuid>
-:LINEAR-IDENTIFIER: ENG-123
-:LINEAR-URL: https://linear.app/.../ENG-123
-:LINEAR-TEAM-ID: <id>
-:LINEAR-TEAM-NAME: Engineering
-:LINEAR-PROJECT-NAME: Platform
-:LINEAR-STATE-NAME: In Progress
-:LINEAR-ASSIGNEE-NAME: Craig
-:LINEAR-LABELS: [bug, p1]
-:LINEAR-DESC-SHA256: <hash of the last-fetched description markdown>
-:LINEAR-DESC-ORG-SHA256: <hash of the rendered Org body>
-:LINEAR-DESC-UPDATED-AT: <remote timestamp>
-:LINEAR-TITLE-SHA256: <hash of the rendered title>
+:CUSTOM_ID: commands
:END:
-The issue description renders here as org, edited in place.
-
-*** Comments
-**** Author Name — 2026-05-23T10:00:00.000Z
-A comment, oldest first.
-#+end_src
-
-Every issue nests under one top-level heading named after the view (here =My open issues=), so you can sort the whole set with =C-c ^= on that parent. The =#+LINEAR-SOURCE:= line records what produced the file so =refresh-current-view= can re-run it. The =LINEAR-*= drawer stores ids and display names separately; commands show names and mutate by id, so there is no per-render network lookup. The =SHA256= properties are provenance for the sync conflict gate.
+*** Command menu
-* Fetching issues
+=M-x pearl-menu= opens a transient dispatcher with commands grouped by fetch, view, issue-at-point, creation, Org sync, and setup. Bind that command if you use Pearl regularly:
-| Command | What it fetches |
-|---------+-----------------|
-| =pearl-list-issues= | your open issues |
-| =pearl-list-issues-by-project= | open issues in a chosen project |
-| =pearl-list-issues-filtered= | an ad-hoc filter built interactively |
-| =pearl-run-view= | a Linear Custom View, run server-side |
-| =pearl-run-saved-query= | a named local saved query |
-| =pearl-refresh-current-view= | re-runs the file's recorded source |
-| =pearl-refresh-current-issue= | re-fetches the issue at point |
-
-=list-issues-filtered= picks a team (which scopes the rest), then completes the state, project, and labels from that team's actual values rather than free text, so a typo can't silently return nothing. It can save the filter as a local query.
-
-** Saved queries
-
-Name local queries in =pearl-saved-queries=, then run them with =pearl-run-saved-query=:
-
-#+begin_src elisp
-(setq pearl-saved-queries
- '(("My open work" :filter (:assignee :me :open t) :sort updated :order desc)
- ("Open bugs" :filter (:labels ("bug") :open t) :sort priority :order asc)))
+#+begin_src emacs-lisp
+ (global-set-key (kbd "C-c L") #'pearl-menu)
#+end_src
-Each entry is a filter plist plus optional =:sort= (=updated=, =priority=, or =title=) and =:order= (=asc= / =desc=). Sorting happens after fetch, so a refresh always lays headings out the same way. Queries are AND-only; for OR logic, build a Linear Custom View and run it with =run-view=.
-
-* Editing issues
-
-All issue commands work from anywhere inside an issue's subtree.
+Every command below is also available directly through =M-x=.
-** Description and title
+*** Fetching and refreshing
-Edit the description in the body, then =M-x pearl-sync-current-issue=. The push is gated: unchanged since fetch sends nothing; a local edit against an unchanged remote pushes; if the remote also changed since the fetch the push is refused and the conflict reported (refresh to reconcile). =pearl-sync-current-issue-title= does the same for the heading title (note: square brackets are stripped from titles, so a synced title drops them).
+| Command | What it does |
+|------------------------------+--------------------------------------------------|
+| =pearl-list-issues= | Fetch your open issues |
+| =pearl-list-issues-by-project= | Fetch open issues in a chosen project |
+| =pearl-list-issues-filtered= | Build an ad-hoc issue filter interactively |
+| =pearl-run-view= | Run a Linear Custom View server-side |
+| =pearl-run-saved-query= | Run a named local query from =pearl-saved-queries= |
+| =pearl-refresh-current-view= | Re-run the source recorded in the active file |
+| =pearl-refresh-current-issue= | Re-fetch the issue at point |
-** Fields
+Ad-hoc filtering starts with a team, then completes states, projects, and labels from that team's actual Linear values. Saved queries are local Lisp data:
-| Command | Effect |
-|---------+--------|
-| =pearl-set-priority= | None / Urgent / High / Medium / Low |
-| =pearl-set-state= | a workflow state from the issue's team |
-| =pearl-set-assignee= | a team member |
-| =pearl-set-labels= | team labels (empty selection clears) |
+#+begin_src emacs-lisp
+ (setq pearl-saved-queries
+ '(("My open work" :filter (:assignee :me :open t) :sort updated :order desc)
+ ("Open bugs" :filter (:labels ("bug") :open t) :sort priority :order asc)))
+#+end_src
-Each completes by display name, resolves to the Linear id, pushes, and updates the drawer.
+Sorting is local and deterministic. Query filters are AND-only; use a Linear Custom View for OR-heavy logic.
-** Comments
+*** Editing issues
-Comments render oldest-first under a =*** Comments= subtree, in both the bulk list and a single-issue refresh. =pearl-add-comment= posts a new comment and appends it.
+All issue commands work from anywhere inside an issue subtree.
-You can also edit your *own* comments. Edit a comment's body in place, then =M-x pearl-edit-current-comment= from inside its subtree. The push is gated like the description sync: unchanged sends nothing, a clean edit pushes, and a both-sides-changed case is refused (refresh to reconcile). Matching Linear's permissions, only comments you authored are editable — others (and bot or integration comments) are refused without a network call. Editability is shown by color: your own comments render green, everyone else's render greyed. Run =M-x pearl-highlight-comments= to recolor a buffer by hand.
+| Command | What it changes |
+|-----------------------------------+--------------------------------------------------|
+| =pearl-sync-current-issue= | Push the edited description body |
+| =pearl-sync-current-issue-title= | Push the edited heading title |
+| =pearl-set-priority= | Set None, Urgent, High, Medium, or Low |
+| =pearl-set-state= | Set a workflow state from the issue's team |
+| =pearl-set-assignee= | Set a team member as assignee |
+| =pearl-set-labels= | Replace labels; empty selection clears them |
+| =pearl-add-comment= | Add a new Linear comment |
+| =pearl-edit-current-comment= | Push edits to one of your own comments |
+| =pearl-delete-current-issue= | Soft-delete the current issue after confirmation |
+| =pearl-open-current-issue= | Open the issue URL in a browser |
+| =pearl-open-current-view-in-linear= | Open the active view in Linear |
-** Other commands
+Description, title, and comment pushes use the same conflict gate: unchanged local text sends nothing; a local edit against an unchanged remote pushes; if both local and remote changed since fetch, Pearl refuses to clobber either side and reports the conflict. Destructive conflict choices stash local text in =*pearl-conflict-backup*= first.
-| Command | Effect |
-|---------+--------|
-| =pearl-open-current-issue= | open the issue's URL in the browser |
-| =pearl-open-current-view-in-linear= | open the active view's URL |
-| =pearl-delete-current-issue= | delete the issue (confirms; soft delete to Trash) |
-| =pearl-new-issue= | create an issue |
-| =pearl-clear-cache= | clear the team/state/collection/view lookup caches |
+Only comments you authored are editable. Pearl refuses edits to comments from another person, a bot, or an integration before making an API call.
-* State mapping and org TODO sync
+*** Org TODO sync
-=pearl-state-to-todo-mapping= maps Linear workflow states to org TODO keywords for *rendering only* — it no longer decides which issues are fetched (inclusion is server-side via the filter). Default:
+=pearl-state-to-todo-mapping= maps Linear state names to Org TODO keywords for rendering and optional state sync:
-#+begin_src elisp
-'(("Todo" . "TODO")
- ("In Progress" . "IN-PROGRESS")
- ("In Review" . "IN-REVIEW")
- ("Backlog" . "BACKLOG")
- ("Blocked" . "BLOCKED")
- ("Done" . "DONE"))
+#+begin_src emacs-lisp
+ (setq pearl-state-to-todo-mapping
+ '(("Todo" . "TODO")
+ ("In Progress" . "IN-PROGRESS")
+ ("In Review" . "IN-REVIEW")
+ ("Backlog" . "BACKLOG")
+ ("Blocked" . "BLOCKED")
+ ("Done" . "DONE")))
#+end_src
-Customize it to match your workflow, and make sure your =org-todo-keywords= include every keyword you map to. With org sync enabled, changing an issue's TODO keyword pushes the corresponding Linear state:
+Make sure your =org-todo-keywords= include every keyword you map to. Enable and disable the save/TODO-change hooks with:
-#+begin_src elisp
-(add-hook 'find-file-hook
- (lambda ()
- (when (and buffer-file-name
- (string-equal (file-truename buffer-file-name)
- (file-truename pearl-org-file-path)))
- (pearl-enable-org-sync))))
+#+begin_src emacs-lisp
+ (pearl-enable-org-sync)
+ (pearl-disable-org-sync)
#+end_src
-=pearl-enable-org-sync= / =pearl-disable-org-sync= toggle the save-and-TODO-change hooks for the active file.
+** The Org File
+:PROPERTIES:
+:CUSTOM_ID: the-org-file
+:END:
-* Customization
+A fetched Pearl file is intentionally readable. The header records the source, run time, filter summary, count, and whether pagination truncated the result. Issues sit under one top-level view heading:
-| Variable | Purpose |
-|----------+---------|
-| =pearl-org-file-path= | the active output file |
-| =pearl-state-to-todo-mapping= | Linear state ↔ org keyword (render/sync) |
-| =pearl-saved-queries= | named local queries |
-| =pearl-default-team-id= | default team for issue creation |
-| =pearl-max-issue-pages= | pagination cap (default 10) |
-| =pearl-request-timeout= | API request timeout (seconds) |
-| =pearl-fold-after-update= | re-fold the page after a fetch/refresh (default on) |
-| =pearl-surface-buffer= | bring the active buffer to a window after a command (default on) |
-| =pearl-surface-select-window= | also move focus to that window (default off) |
-| =pearl-debug= | enable debug logging |
+#+begin_src org
+ #+title: Linear - My open issues
+ #+STARTUP: show3levels
+ #+TODO: TODO IN-PROGRESS IN-REVIEW BACKLOG BLOCKED | DONE
+ #+LINEAR-SOURCE: (:type filter :name "My open issues" :filter (:assignee :me :open t))
+ #+LINEAR-RUN-AT: 2026-05-23 19:30
+ #+LINEAR-COUNT: 12
+ #+LINEAR-TRUNCATED: no
+
+ * My open issues
+ ** TODO [#B] ENG-123 Issue title
+ :PROPERTIES:
+ :LINEAR-ID: <uuid>
+ :LINEAR-IDENTIFIER: ENG-123
+ :LINEAR-URL: https://linear.app/.../ENG-123
+ :LINEAR-STATE-NAME: In Progress
+ :LINEAR-ASSIGNEE-NAME: Craig
+ :LINEAR-LABELS: [bug, p1]
+ :LINEAR-DESC-SHA256: <last-fetched markdown hash>
+ :LINEAR-DESC-ORG-SHA256: <rendered Org body hash>
+ :END:
+
+ The issue description renders here as Org and can be edited in place.
+
+ *** Comments
+ **** Author Name - 2026-05-23T10:00:00.000Z
+ A comment, oldest first.
+#+end_src
-* FAQ
+The =LINEAR-*= properties store both ids and display names so common commands do not need a network lookup just to render. The hash properties are provenance for conflict-aware sync.
-** I edited the title in the heading and ran the description sync, but the title didn't change.
+** Configuration
+:PROPERTIES:
+:CUSTOM_ID: configuration
+:END:
-Title and description push through separate commands. =pearl-sync-current-issue= pushes the body; the heading title is =pearl-sync-current-issue-title= (menu =t=). Fields and comments are their own commands too — each edit pushes independently.
+Most users only need an API key and an output path. The rest are knobs for teams with large issue sets or stronger preferences about window behavior.
+
+| Variable | Purpose |
+|-----------------------------+---------------------------------------------------|
+| =pearl-api-key= | Linear API key |
+| =pearl-org-file-path= | Active Org output file |
+| =pearl-default-team-id= | Default team for issue creation |
+| =pearl-saved-queries= | Named local issue queries |
+| =pearl-state-to-todo-mapping= | Linear state to Org keyword mapping |
+| =pearl-max-issue-pages= | Pagination cap, 100 issues per page |
+| =pearl-request-timeout= | Synchronous request timeout in seconds |
+| =pearl-fold-after-update= | Re-fold the active page after fetch/refresh |
+| =pearl-surface-buffer= | Show the active buffer after a command updates it |
+| =pearl-surface-select-window= | Move focus to the surfaced buffer |
+| =pearl-debug= | Log request/response details to =*Messages*= |
+
+If a fetch stops at the pagination cap, Pearl writes =#+LINEAR-TRUNCATED: yes= in the file header. Raise =pearl-max-issue-pages= if your result set is larger than the default 1000 issues.
+
+** Development & Testing
+:PROPERTIES:
+:CUSTOM_ID: development--testing
+:END:
-** My title lost its square brackets after syncing.
+Clone the repo and install the Eask-managed dependencies:
-The renderer strips =[= and =]= from titles so org doesn't misparse them, so the heading holds the stripped form and a title push sends that. =Fix [URGENT] bug= becomes =Fix URGENT bug=. Keep brackets out of titles you mean to round-trip.
+#+begin_src bash
+ git clone https://git.cjennings.net/pearl.git
+ cd pearl
+ make setup
+#+end_src
-** A markdown heading or =*italic*= in a description turned into bold after I synced it.
+Useful development targets:
+
+| Target | What it does |
+|----------------------------------------+-------------------------------------------------------|
+| =make test= | Run unit and integration tests, excluding =:slow= tests |
+| =make test-all= | Run every test, including =:slow= tests |
+| =make test-unit= | Run unit tests only |
+| =make test-integration= | Run integration tests only |
+| =make test-file FILE=mapping= | Run one test file by fuzzy match |
+| =make test-one TEST=priority= | Run one test by fuzzy match |
+| =make test-name TEST='test-pearl-map-*'= | Run tests matching an ERT selector |
+| =make coverage= | Generate undercover/simplecov coverage data |
+| =make compile= | Byte-compile =pearl.el= |
+| =make lint= | Run the lint/checkdoc path from the test harness |
+| =make validate= | Check parentheses across source and tests |
+| =make clean= | Remove test artifacts and coverage output |
+
+Each test file runs in its own Emacs batch process for isolation. See [[file:TESTING.org][TESTING.org]] for the full test guide, naming conventions, fixture helpers, and coverage notes.
+
+** FAQ
+:PROPERTIES:
+:CUSTOM_ID: faq
+:END:
-Two markdown constructs are intentionally lossy on the round-trip: a markdown =# heading= renders as a bold line on fetch and pushes back as bold (it never returns to a heading), and single-asterisk =*italic*= is read as bold. Edit a description containing either and push, and it comes back rewritten. Use =**bold**= and real Org sub-structure deliberately; don't rely on md headings surviving.
+*** I edited the heading title and synced the description. Why did the title stay the same?
-** My comment edit was refused with "You can only edit your own comments."
+Titles and descriptions push through separate commands. Use =pearl-sync-current-issue= for the body and =pearl-sync-current-issue-title= for the heading title.
-By design — Pearl matches Linear's permissions. Only comments you authored are editable; another person's comment, or a bot or integration comment, is refused with no API call. Your own comments render green, everyone else's greyed.
+*** Why did square brackets disappear from a synced title?
-** A sync or comment edit was refused as a conflict.
+Pearl strips =[= and =]= from titles before rendering so Org does not misparse them. A title like =Fix [URGENT] bug= round-trips as =Fix URGENT bug=.
-The push is gated three ways: unchanged since fetch sends nothing; a local edit against an unchanged remote pushes; if the remote also changed since your last fetch, the push is refused and the conflict reported. Refresh to reconcile, or take the use-local / use-remote / merge prompt — any destructive choice stashes your local text in =*pearl-conflict-backup*= first, so nothing is lost.
+*** Why did a Markdown heading or single-asterisk italic change after syncing?
-** I hand-edited a drawer field (priority, state, assignee, labels) and it didn't push.
+The Markdown-to-Org round trip is intentionally lossy for a few constructs. Markdown =# heading= renders as a bold line, and single-asterisk =*italic*= is read as bold. Use these constructs carefully in descriptions you plan to edit from Org.
-=LINEAR-*= drawer fields change by command, not by hand: =pearl-set-priority=, =-state=, =-assignee=, =-labels=. Each resolves a display name to the Linear id, pushes, and rewrites the drawer. A value typed into the drawer by hand won't push and gets overwritten on the next refresh.
+*** Why did Pearl refuse my comment edit?
-** My fetch seems to be missing issues.
+Linear only lets you edit comments you authored. Pearl checks that before pushing and refuses edits to other people's comments, bot comments, and integration comments.
-The fetch is capped at =pearl-max-issue-pages= (default 10 pages × 100 = 1000 issues). A capped fetch sets =#+LINEAR-TRUNCATED: yes= in the header. Raise =pearl-max-issue-pages= if you're assigned more.
+*** Why did Pearl refuse a description, title, or comment sync as a conflict?
-** Names (team, state, assignee) are stale after I renamed them in Linear.
+The local text and remote text both changed since the last fetch. Refresh to reconcile, or use the conflict prompt. Pearl stashes local text before any destructive resolution.
-Pearl caches those lookups so it doesn't hit the network on every render. Run =M-x pearl-clear-cache=.
+*** Why did a hand-edited drawer field get overwritten?
-** The page collapses after every refresh — can I keep it expanded?
+Drawer fields are generated metadata. Change priority, state, assignee, and labels through Pearl commands so display names can resolve to Linear ids correctly.
-After a fetch or refresh Pearl re-folds the page to its =#+STARTUP= visibility (issue headings shown; descriptions, comments, and drawers folded), because =#+STARTUP= on its own only applies on a file's first visit. Set =pearl-fold-after-update= to nil to leave the buffer however you had it.
+*** Why are renamed teams, states, labels, or assignees stale?
-** Can I hide the property drawers completely, not just fold them?
+Pearl caches Linear lookup tables. Run =M-x pearl-clear-cache=.
-Pearl folds drawers with the rest of the page, but an expanded issue still shows the one-line =:PROPERTIES:= stub. For a tidier look, [[https://github.com/jxq0/org-tidy][org-tidy]] replaces each drawer with a small glyph. It composes with Pearl rather than fighting it: Pearl folds the outline (descriptions and comments) to a scannable list, and org-tidy collapses the drawers on top — even on an expanded issue. Editing still works, since org-tidy only adds display overlays and never touches the drawer text, and a Pearl fetch re-applies it cleanly. Turn it on with =M-x org-tidy-mode= (or =global-org-tidy-mode=); if a partial refresh ever leaves a drawer showing, =M-x org-tidy= re-tidies.
+*** Can I keep the file expanded after refresh?
-* Troubleshooting
+Yes. Set =pearl-fold-after-update= to nil.
-- =M-x pearl-test-connection= — check the API key and connectivity.
-- =M-x pearl-check-setup= — confirm the key loaded.
-- =M-x pearl-toggle-debug= (or =(setq pearl-debug t)=) — log requests and responses to =*Messages*=.
-- Stale names after renaming things in Linear → =M-x pearl-clear-cache=.
+** Troubleshooting
+:PROPERTIES:
+:CUSTOM_ID: troubleshooting
+:END:
-* Contributing
+- =M-x pearl-check-setup= checks whether the API key is loaded.
+- =M-x pearl-test-connection= checks API connectivity.
+- =M-x pearl-toggle-debug= enables request/response logging in =*Messages*=.
+- =M-x pearl-clear-cache= refreshes cached names after Linear-side changes.
-Contributions are welcome — open an issue or a pull request.
+** History
+:PROPERTIES:
+:CUSTOM_ID: history
+:END:
-* Acknowledgments
+Pearl is based on and inspired by Gael Blanchemain's [[https://github.com/gael/linear-emacs][linear-emacs]]. The package has since grown into a separately maintained Linear workflow for Org, including broader fetch modes, editable descriptions and comments, conflict-aware sync, view refresh, command menus, and a dedicated test suite.
-Based on and inspired by Gael Blanchemain's linear-emacs.
+Bug reports, feature requests, and pull requests are welcome.
-* License
+** License
+:PROPERTIES:
+:CUSTOM_ID: license
+:END:
-GPL-3.0. See [[file:LICENSE][LICENSE]].
+GPL-3.0-or-later. See [[file:LICENSE][LICENSE]].