| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
| |
A comment whose markdown doesn't survive md->org->md (a heading, single-asterisk italics) was flagged changed on every save, because the dirty scan compared org-to-md of the rendered body against the stored markdown hash. When the comment was authored by someone else, that surfaced as "1 read-only comment skipped" on every save even though nothing had been edited.
Descriptions already dodge this: 9424b84 gave them LINEAR-DESC-ORG-SHA256 and compared Org-to-Org. I did the same for comments. pearl--format-comment now writes LINEAR-COMMENT-ORG-SHA256 (the rendered body hash), and a new pearl--comment-dirty-p compares the current body against it, falling back to the old markdown hash for comments rendered before the baseline existed. Both detection sites use it: the save scan and the edit-current-comment no-op check. The markdown hash stays for the remote-conflict gate.
|
| |
|
|
|
|
|
|
| |
An issue's Linear labels now appear as Org tags on its heading (** TODO [#B] Title :bug:backend:), so the org-native gestures work: filter by tag, build a tag agenda, sparse-tree on :bug:. Before this, labels lived only in the :LINEAR-LABELS: drawer, which Org's tag machinery can't see.
pearl--label-name-to-tag slugifies a label name to a tag: downcase, non-[[:alnum:]_] runs become a single underscore, Unicode letters preserved. pearl--label-tags builds the deduplicated set, first occurrence winning on a collision. The renderer appends them, and pearl-edit-labels rewrites them through pearl--set-heading-label-tags after updating the drawer.
This is render-only. The :LINEAR-LABELS: drawer stays the source of truth, hand-edited heading tags are ignored by save and the dirty scan and get rewritten from Linear on the next change or fetch, and the way to change labels is still pearl-edit-labels. Bidirectional tag editing (heading tags back to Linear) is out of scope.
|
| |
|
|
| |
I added pearl-copy-issue-url. It copies the Linear URL of the issue at point to the kill ring and the system clipboard, reading the stored LINEAR-URL drawer property so there's no network call, and works from anywhere inside the issue subtree. kill-new carries it to the clipboard through interprogram-cut-function, so nothing shells out to a clipboard tool. Bound at u c in the url group, beside u o (open in browser).
|
| |
|
|
| |
I replaced the old save/edit/new/delete prefix writeup with pearl-mode, which auto-enables in Linear buffers, and the full scheme: the hot keys directly under the prefix plus the fetch, edit, new, delete, and url groups.
|
| |
|
|
|
|
|
|
| |
Pearl is now drivable entirely from the keyboard. pearl-mode turns on automatically in any buffer Pearl renders, detected by the #+LINEAR-SOURCE header on org-mode-hook, and binds the command map under pearl-keymap-prefix (default "C-; L"), so the keys are live without any global setup.
I filled out pearl-prefix-map so every day-to-day command is reachable. The hot-path commands bind directly under the prefix (list, refresh view, refresh issue, save, edit description, new comment). The rest sit in category sub-maps (fetch, edit, new, delete, url). The common ones appear in both places, so edit-description is the direct d and also e d. Delete moved from d to k so d is the direct edit key, and the old save sub-map is gone now that save is the direct s and S.
The org-mode-hook is registered at load rather than through an autoload cookie. An autoloaded hook would fire before pearl loaded, calling a void function, and would pull the whole package into every org buffer the user opens. Freshly fetched buffers also enable the mode directly in the writer.
|
| |
|
|
|
|
| |
The buffer should mirror Linear: a value on the page appears exactly as Linear stores it, so the heading title matches what you'd see opening the issue in Linear itself. I flipped pearl-title-case-headings to default nil so titles render verbatim. I also stopped running the view name in the #+title through the title-caser, which was coupled to the same flag. The smart-title-case path stays as opt-in tidying for anyone who wants it.
Two transformations remain, both forced by Org's syntax rather than chosen: state names become TODO keywords (Org keywords can't contain spaces, so the real name stays in the LINEAR-STATE-NAME drawer), and descriptions and comments convert between Markdown and Org. Documented under "Fidelity to Linear" in the README.
|
| |
|
|
|
|
| |
pearl-add-comment and pearl-delete-current-issue guarded position with the nearest-heading check, so from inside a comment subtree, or any heading without a LINEAR-ID, they errored "Not on a Linear issue heading". Both docstrings already claimed they work from anywhere in the subtree, so the behavior was just wrong.
The edit and save commands already climb to the issue via pearl--goto-issue-heading-or-error. I switched these two to the same guard, so every ticket-scoped command resolves the enclosing issue from anywhere in its subtree, including from inside a comment. The redundant LINEAR-ID re-checks are gone since the guard guarantees it. Walk-up tests added for both.
|
| |
|
|
|
|
| |
After a refresh, merge, or comment add, pearl folds property drawers so the page stays scannable. When org-tidy-mode is on, org-tidy owns drawer display with its own compact inline symbol, so a plain native fold doesn't match. Worse, org-tidy's overlays go stale when pearl rewrites the buffer, leaving the new drawers untidied.
I added pearl--hide-or-tidy-drawers, which re-tidies via org-tidy-buffer when org-tidy-mode is active and falls back to the native fold otherwise. I routed the three drawer-hide paths (full render, single-issue refresh, comment add) through it. org-tidy stays an optional soft dependency, guarded by bound-and-true-p and fboundp, so a setup without it behaves exactly as before.
|
| |
|
|
| |
Adding a comment left three rough edges. Its property drawer rendered expanded while every other drawer in the page was folded, so I fold the new subtree's drawers right after inserting it. The Comments heading's count went stale on add: a freshly created subtree now shows π¬ Comments 1/1, and an existing π¬ Comments N/M bumps to N+1/M+1. And the fallback that creates the Comments subtree when an issue had none was still inserting a glyph-less "Comments". It now leads with the π¬ like the rest.
|
| |
|
|
| |
Running pearl-list-issues on a buffer with unsaved edits used to print a passive "refresh deferred" message and do nothing visible, so it read as a no-op. Now it prompts: (d)iscard and rebuild, (m)erge by LINEAR-ID keeping the edits, or (q) cancel. Branch C of pearl--update-org-from-issues dispatches on the choice: discard rebuilds in place, merge hands off to the same by-id merge that pearl-refresh-current-view uses, and cancel leaves the buffer untouched.
|
| |
|
|
| |
The merge refresh protected a subtree from being overwritten only when its description body had changed, since pearl--subtree-dirty-p is description-only. A local edit to the title, state, priority, assignee, labels, or a comment looked untouched and got re-rendered away. I added pearl--subtree-has-local-edits-p, which wraps the comprehensive per-field detector the save path already uses, and pointed both merge protection sites at it. A merge now keeps any ticket you've touched in any field. The same gain flows to pearl-refresh-current-view.
|
| |
|
|
|
|
| |
The comment overlay started at the line beginning, so it sat on top of the leading stars. With a package like org-superstar that dims those stars (and composes a bullet from them) via a face, our overlay's face overrode it and the dimmed stars reappeared in our color, a stray "***" before the bullet. The darker, body-spanning read-only overlay made it obvious, but the overlay had covered the stars since comment highlighting first landed.
I start the overlay just past the leading stars now (the point right after the `^\*+ ` match), so the stars and the bullet keep org's own fontification and only the heading text and body get our face. The highlight tests check the face on the heading text and assert the stars are left uncovered.
|
| |
|
|
| |
A comment you can't edit only had a grey heading, and the grey (`shadow`) was light enough to blend with the body text of your own comments. Now the read-only overlay covers the whole comment subtree, heading and body, so the comment recedes as a unit. And `pearl-readonly-comment` uses a darker background-aware grey (gray40 on a dark theme, gray60 on light, `shadow` as the fallback). Editable comments keep the heading-only overlay so your own comment text stays normal-colored, with the green heading as the "this one's yours" cue.
|
| |
|
|
|
|
| |
`pearl-state-to-todo-mapping` and its derived regex stopped doing real work once keyword rendering moved to slugifying the Linear state name (`pearl--state-name-to-keyword`) and the keyword-to-state direction moved to resolving through the team's workflow states. The defcustom fed only `pearl--get-todo-states-pattern`, which had no caller left, and the two cache vars existed only to serve that dead function. State extraction reads `org-get-todo-state` off the buffer's `#+TODO` line, so nothing live touched the mapping.
I removed the defcustom, both cache vars, and the dead function, and dropped the three pattern tests that exercised it. Removing the defcustom orphaned the mapping bindings that ~12 test setup macros still carried. They were passing through slugification rather than the map, so I stripped them too and kept the `org-todo-keywords` bindings the temp buffers actually need. Also dropped the stale README config row.
|
| |
|
|
|
|
| |
The Comments heading rendered the count marker as `Comments π¬ 5/18`, with the glyph wedged between the word and the count. I moved the π¬ to the front of the heading, rendering `π¬ Comments 5/18`, so it reads as an icon for the section rather than part of the number. `pearl--comment-count-marker` now returns just ` 5/18`, and `pearl--format-comments` leads with the glyph.
The append locator that finds an issue's Comments subtree now tolerates the leading π¬, and still matches the old `Comments π¬ N/N` layout so a buffer rendered before this change keeps working on the next add-comment.
|
| |
|
|
|
|
| |
Comment headings only got their green (editable) / grey (read-only) coloring after `pearl-refresh-current-issue`, an add-comment, or a view merge (the three paths that call `pearl-highlight-comments`). A plain `pearl-list-issues` rendered through `pearl--update-org-from-issues`, which never called it, so a fresh fetch showed no coloring until I refreshed.
I added the highlight call to branches A and B of `pearl--update-org-from-issues`, right after the content lands in the buffer. Branch C defers a dirty buffer without writing, so it stays uncolored. The regression test renders the comment fixture and asserts both headings carry the editability overlay (own to editable, other's to read-only) straight from the bulk render.
|
| |
|
|
|
|
| |
`make coverage` only reported the JSON's file size, so reading the real number meant opening the report by hand. I added a self-contained `scripts/coverage-summary.el` that parses the SimpleCov JSON and prints per-file covered/total lines with a percent, a line-weighted project figure, and a source-weighted figure. A tracked source missing from the report counts as 0% rather than dropping out silently, so a source that never got instrumented still shows up.
A new `coverage-summary` target runs it against the last report, and `make coverage` chains it onto the tail of a local run (CI emits coveralls.json, not simplecov.json, so it's skipped there). Covered by `tests/test-pearl-coverage-summary.el`: parser hit/executable/merge-key semantics, the per-file records, and the error paths for a missing or malformed report.
|
| |
|
|
| |
inbox/ holds cross-agent message drops and wrap-up lint follow-ups β local workflow state, the same class as .ai/ and todo.org. Keep it out of the tracked tree.
|
| |
|
|
|
|
| |
Cycling a ticket's TODO keyword (C-c C-t) now counts as a state edit. The dirty scan gains a keyword-cycle arm: state is dirty when the explicit id moved off its baseline (the picker arm, as before) or when the keyword diverged from the synced state name's slug. At save, the picker arm pushes the explicit id; the keyword-cycle arm resolves the keyword to a team state id by slug match, first by position, and pushes that. A keyword no team state matches is reported skipped rather than guessed.
set-heading-state now slugifies the keyword instead of the static-map lookup, so the picker writes the same keyword the renderer derives. Without that, a picker edit to a non-standard state would read as permanently cycled. That retires pearl--map-linear-state-to-org, whose only caller this was, and its tests.
|
| |
|
|
| |
The spec predates save-model-v2, which removed the org-sync push path its "sync-back: cycled keyword β Linear state" section relied on. Mark that section obsolete: state now reconciles at save through pearl--save-state-field, and the cycled-keyword β state-id resolution is the c8 task. Record the invariant c8 leans on β every fresh render sets the keyword to slugify(state-name) β and the one stale-render caveat a refresh closes.
|
| |
|
|
|
|
| |
A same-source refresh updates issue subtrees in place and keeps retained dirty subtrees the merge skips, so the #+TODO line has to stay honest across it -- a refresh can surface a new state, and a kept heading's keyword must still be declared. pearl--update-derived-todo-header scans the final displayed buffer (every LINEAR-ID heading's TODO keyword + :LINEAR-STATE-TYPE: drawer), unions that with the fetched teams' states, and rewrites the line via pearl--derive-todo-line. Scanning the final buffer rather than the fetched issue list is what covers the retained subtrees. A legacy heading with no state-type drawer is classified by org-done-keywords. pearl--merge-query-result calls it after the merge, beside the source-header update.
The merge and integration test setups stub pearl--team-states so the gather stays network-free.
|
| |
|
|
|
|
|
|
| |
The renderer wrote a fixed #+TODO line and mapped state names through a static defcustom, so a workspace's real states (Dev Review, PM Acceptance, ...) never showed up. Now the displayed issues' teams drive both.
pearl--gather-header-states collects each team's full state set (cached via pearl--team-states, teams in first-seen order, states by position) plus every displayed issue's own state, so a failed team fetch still leaves its issues' states declared. pearl--build-org-content takes that list and derives the #+TODO via pearl--derive-todo-line. pearl--format-issue-as-org-entry renders each keyword with pearl--state-name-to-keyword instead of the static map -- slugify reproduces the old defaults, so standard states are unchanged -- and writes a :LINEAR-STATE-TYPE: drawer so a later merge scan can classify a retained heading. The team-states query gains type and position.
The integration test stubs pearl--team-states so the render stays network-free. The merge-refresh header rebuild is the next commit. Until then a same-source refresh keeps the previous header.
|
| |
|
|
|
|
| |
Two pure functions behind the coming workspace-faithful #+TODO line. pearl--state-name-to-keyword slugifies a Linear state name to an Org keyword: upcased, non-alphanumeric runs collapsed to a single hyphen, edges trimmed, Unicode letters kept (so "Γ
ngstrΓΆm" survives), and an all-symbol name falls back to TODO. pearl--derive-todo-line turns an ordered state list into the "ACTIVE... | DONE..." string: completed/canceled/duplicate types go after the bar, the rest before, order preserved within each side, de-duplicated by slug, with the hardcoded default returned for an empty list.
There's no caller yet -- the render integration and the gather pipeline land next. This is the derivation half of the keyword spec. The sync-back half it described is superseded by save-model-v2 (state reconciles at save) and the separate keyword-cycle task.
|
| |
|
|
| |
The atomic savers got this guard last commit, and the free-text savers share the same async exposure. pearl--save-field-push advances the provenance hash and runs the :after-push thunk through the marker after the push returns, and the description saver's outcome callback resyncs the Org hash the same way -- both signal if the buffer was killed in between. Both now check pearl--marker-live-p and skip the buffer touch while still reporting the remote outcome. I moved pearl--marker-live-p above the first caller so it's defined before all three use sites (no forward reference).
|
| |
|
|
|
|
| |
The per-field atomic savers advance the baseline and rewrite the display through the marker (org-entry-put marker / org-with-point-at marker) inside the async push callback. If the buffer is killed before the push lands, those touch a dead marker and signal. The deleted pearl--push-issue-field carried this guard, but it went away with the immediate-push setters and the atomic engine never picked it up.
pearl--run-atomic-field-save now skips the commit when the marker's buffer is dead (a new pearl--marker-live-p, which also passes a nil marker for synthetic callers) and still reports the remote outcome -- the push already landed, so only the local buffer update is moot. All three commit sites are guarded: the clean push, the converged-remote case, and the use-remote conflict resolution.
|
| | |
|
| |
|
|
|
|
| |
The README still described the old per-field push commands and the org-sync hook mode, both removed in save-model-v2. I rewrote the editing section around the one write path: you edit in the buffer, save the ticket, and Pearl reconciles each changed field against the remote. It now leads with pearl-save-issue / pearl-save-all, spells out how each field is edited (the cookie for priority, a picker for state/assignee/labels, hand-editing for text), explains that picking a constrained field marks it changed rather than pushing, and describes the conflict gate for both free-text and atomic fields.
The old "Org TODO sync" section drops the removed enable/disable hooks and keeps the state-to-keyword mapping as render config, with pearl-edit-state as the way to change state. The intro, the feature list, and the use-package and prefix-key examples lose their stale references. I kept the keyword-cycle-to-state and keyword-derivation work named as planned follow-ups rather than claiming they work, since they're not in yet.
|
| |
|
|
|
|
|
|
| |
The opt-in org-sync mode pushed Linear state immediately β on every TODO-keyword cycle (the org-after-todo-state-change hook) and on every buffer save (the after-save hook scanning the whole file). That's the last immediate-push path, and it contradicts the one-write-path model the rest of v2 builds, so I removed it: pearl-enable-org-sync / -disable-org-sync, pearl-org-hook-function, pearl-sync-org-to-linear, pearl-sync-current-heading-to-linear, and the now-orphaned pearl--process-heading-at-point, pearl--map-org-state-to-linear, and pearl--update-issue-state-async. This finishes the "make org-sync private" task β there was nothing left to make private.
Cycling a TODO keyword now marks state dirty (reconciled at the next save) instead of pushing. Saving the buffer pushes nothing. Re-introducing an auto-push on save is the separate "Automatic sync on save" task, which would call the save engine rather than this whole-file scanner.
I kept pearl--extract-org-heading-properties and pearl--get-todo-states-pattern. They're general heading/keyword readers, not org-sync-specific, and the keyword-cycle save (the keyword-derivation follow-up) needs exactly them. The obsolete sync tests are replaced by two that assert the contract: the sync commands are gone, and a keyword cycle pushes nothing.
|
| |
|
|
|
|
|
|
| |
The structured fields now reconcile at save (the previous commits), so the immediate-push setters are the duplicate path that made the package feel like it had two save models. I retired them. pearl-set-priority is deleted outright β priority is edited org-natively through the heading cookie. pearl-set-state, pearl-set-assignee, and pearl-set-labels become pearl-edit-state / -assignee / -labels: the same completing-read picker, but it writes the buffer representation (keyword/name/id, the assignee drawer, the labels drawer plus the live LINEAR-LABEL-IDS set) and marks the field dirty instead of pushing. The change goes out at the next pearl-save-issue / pearl-save-all. pearl-compose-current-description is renamed pearl-edit-description (its compose-and-push behavior is unchanged β v2 doesn't touch the compose buffers).
The transient, the C-; L e keymap, the affordance preamble, and the README follow the new names, and the priority slots are dropped (no command β it's the org cookie). Deleting the setters left pearl--push-issue-field and pearl--priority-choices with no callers, so both are gone, along with the killed-buffer test that only exercised the removed helper. I filed a follow-up: the atomic savers' async commit callbacks need the same killed-buffer guard that helper carried.
The setter command tests are rewritten to assert the buffer-write-and-no-push contract. The reconcile-and-push path they used to cover is exercised in test-pearl-save.
|
| |
|
|
|
|
| |
The four structured savers now run from the save engine. pearl--save-field-thunks queues a priority / state / assignee / labels saver for each dirty structured field alongside title, description, and comments, so save-issue and save-all reconcile a changed cookie, keyword-picker id, assignee, or label set at save time like any other edit. save-all's confirm prompt and the dirty-scan gate both count the structured fields, and a new pearl--issue-has-dirty-fields-p replaces the title/description/comment check that was duplicated in save-issue and the file-wide scan.
This is the commit that turns the engine on. The old immediate-push setters still exist until the next commit retires them, so for this one commit both paths are live β the setter cutover and the org-sync removal follow.
|
| |
|
|
|
|
|
|
|
|
| |
These are three more savers on the atomic engine, each the same spec shape the assignee saver proved, with its own field handling. Priority reads the cookie's number, diffs it against LINEAR-PRIORITY, and pushes priority. State is the picker arm β the explicit LINEAR-STATE-ID against LINEAR-STATE-ID-SYNCED, pushing stateId, with use-remote rewriting the keyword and name from the fetched node (the keyword-cycle arm stays gated on the derivation task). Labels canonicalize the id set to a sorted space-joined string so the diff is order-insensitive, and push labelIds.
Canonicalizing labels to a string also keeps the missing-baseline check honest: an issue with no labels has a present-but-empty baseline, which a nil-vs-empty-list comparison would misread as a legacy file. A shared set of pearl--node-* helpers pulls each field out of the raw issue node.
I also fixed pearl--get-linear-priority-name, which mapped None to "Medium" β it now reads "None", so the conflict prompt names the priority honestly.
It's still not wired into save-issue β the next commit does that.
|
| |
|
|
|
|
|
|
| |
pearl--run-atomic-field-save is the structured-field sibling of the text engine pearl--run-field-save. It runs the atomic three-way over a spec's live and baseline values. A missing baseline skips with missing-property (a legacy file β refresh first). Live equal to baseline is unchanged. Otherwise it fetches the remote and compares: remote already equal to live converges and advances the baseline, remote unmoved from baseline is a clean push, and a genuine divergence runs the atomic conflict gate. It reuses the existing save-outcome contract, so structured fields report the same status/reason shape as title and description.
pearl--save-assignee-field is the first saver on it β the cleanest relation field. Its live id is LINEAR-ASSIGNEE-ID diffed against LINEAR-ASSIGNEE-ID-SYNCED, the remote comes from the issue node, the push sends assigneeId, and use-remote rewrites the display name from the fetched node. Priority, state, and labels follow the same template in the next commit.
It's not wired into save-issue yet β that's the next commit. I tested it in isolation: the engine's three-way and conflict branches over a synthetic spec, plus the assignee saver end to end with a stubbed fetch and update.
|
| |
|
|
|
|
|
|
| |
The text conflict gate (pearl--resolve-conflict) is built for mergeable text β it can open an smerge buffer, stash to the kill ring, and advance a SHA hash. None of that fits a state, priority, assignee, or label conflict, where the value is a single id or scalar that can't be combined.
pearl--resolve-atomic-conflict is the sibling for those fields: it prompts use-local / use-remote / cancel (no merge option), and delegates the actual push and the baseline advance to the caller's two callbacks, so it stays free of any field-specific mechanics. It maps to the same outcome contract as the text gate β cancel to conflict/cancelled, use-local to pushed or failed/push-failed, use-remote to resolved-remote.
It's not wired into a saver yet β the per-field savers in the next commit call it.
|
| |
|
|
|
|
| |
The save scan now flags priority, state, assignee, and labels as dirty when their live value differs from the synced baseline, so structured fields ride the same reconcile-at-save path as title and description. Each check is id/scalar-only and runs with no network: the priority cookie's number against LINEAR-PRIORITY, the explicit state id against LINEAR-STATE-ID-SYNCED (the picker arm β keyword cycling lands later), the assignee id against its baseline, and the label id set against its baseline (order-insensitive, since Linear label order isn't meaningful). A missing baseline on a legacy file reads as not-dirty; the saver's missing-property guard covers that edge.
This also makes the priority cookie faithful. The renderer mapped both None and Medium to [#C], so a None issue couldn't be told apart from a Medium one and would have read dirty the moment its baseline said 0. None now renders with no cookie, 1:1 with the four cookie levels β matching what the priority setter already did.
|
| |
|
|
| |
make test-file FILE=test-pearl-save grabbed test-pearl-saved.el β the glob *FILE*.el matched both and head -1 picked one arbitrarily. It now prefers an exact FILE or FILE.el match, and errors with the candidate list when a bare substring matches more than one file, instead of silently running the wrong one.
|
| |
|
|
|
|
| |
Save-model v2 reconciles a field by diffing its live value against a baseline last synced from Linear, but the structured fields (priority, state, assignee, labels) only stored their current value β there was nothing to diff against. The renderer now writes a synced baseline beside each: LINEAR-PRIORITY (the numeric priority), LINEAR-STATE-ID-SYNCED, LINEAR-ASSIGNEE-ID-SYNCED, and LINEAR-LABEL-IDS / LINEAR-LABEL-IDS-SYNCED (the live and baseline id sets, with LINEAR-LABELS staying display-only names).
At fetch, live equals baseline, so nothing reads dirty on a fresh issue. The merge refresh re-renders changed subtrees through the same renderer, so baselines advance there too, and a retained dirty subtree is left untouched.
|
| |
|
|
|
|
| |
The v2 spec folds the structured fields β priority, state, assignee, labels β onto the save engine and removes immediate push, so the package has one write path instead of two. It extends the shipped v1 save-model spec and supersedes the "field setters stay immediate" half of v1 decision 8. I incorporated two Codex review rounds.
I also added the supersession pointer to the top of the v1 spec so the v1/v2 relationship is legible from either file.
|
| | |
|
| |
|
|
|
|
|
|
| |
A fetched description with markdown inside backticks β `**bold**`, `[text](url)`, `_italic_` β got its span contents converted along with the surrounding prose, so the Org buffer showed `*bold*` and `[[url][text]]` inside what should read as literal code. Markdown treats a code span as opaque; the converter didn't.
Both converters now tokenize code spans first and apply link/bold/italic conversion only to the text between them, keeping span contents literal. The md->org and org->md directions each split into an emphasis-only helper (pearl--md-emphasis-to-org, pearl--org-emphasis-to-md) called on the gaps, with the span passed through untouched. Keeping the split symmetric is what preserves the round-trip identity a fetch + no-edit push depends on.
A new test asserts the verbatim rendering for bold, link, and italic inside a span, plus emphasis still converting outside one and between two spans.
|
| |
|
|
| |
The helper returns the body of the Org entry at point, before any child heading. It started out reading issue descriptions, but comment editing and deletion reuse it for comment bodies too, so "issue" in the name was misleading. I renamed it across its callers and the sync/refresh/save/comment tests, and reworded the docstring to say "entry".
|
| |
|
|
| |
pearl-set-state, pearl-set-assignee, and pearl-set-labels each kept an (unless issue-id (user-error ...)) check that became unreachable once they switched to pearl--goto-issue-heading-or-error, which already signals when no enclosing issue heading carries LINEAR-ID. I removed the three dead guards; the climbing guard is the single source of that error now.
|
| |
|
|
|
|
| |
pearl--push-issue-field (the field setters) and pearl-refresh-current-issue ran goto-char on the captured marker in their success callbacks with no liveness check. If the buffer was killed before the async response returned, the dead marker signaled "Marker does not point anywhere" from the callback. I wrapped the buffer work in a buffer-live-p check; the field setters still report the remote success, and refresh reports that the buffer was closed.
This closes the last gap in the command/context test-coverage task. The other seven items it named were already covered by the quick-bug batch's tests; this adds the killed-buffer regression for the issue paths (delete-current-comment already had its own).
|
| |
|
|
| |
I ran the live check the spec named as its one prerequisite: WorkflowState.position exists and returns a float (Dev Review 947.14, In Progress 2, Planning 0), so the per-team position ordering works as designed and the no-position fallback is unnecessary. Recorded it in the status and prerequisites and folded in the round-5 review (the only finding was that status/rubric hygiene). The spec is now cleanly READY with no open prerequisites.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A batch of small, independent fixes a code-review pass turned up, each its own :bug:quick:solo: todo. They share no code path beyond pearl.el, so the body lists them.
Single-issue refresh false-stash: pearl-refresh-current-issue decided whether to stash "local edits" by hashing org->md against LINEAR-DESC-SHA256, the lossy markdown round-trip. A clean issue whose description has lossy markdown (a # heading, single-asterisk italics) looked dirty and got stashed before every refresh. It now uses pearl--subtree-dirty-p, the Org-hash-first check the merge refresh already uses.
Comment outcome parent id: pearl--save-comment-field read :issue-id from the comment heading, which carries LINEAR-COMMENT-ID, not LINEAR-ID, so comment outcomes got :issue-id nil against the contract. A new pearl--issue-id-at-point climbs to the enclosing issue.
save-all viewer-unavailable count: when the viewer lookup failed, save-all passed a nil viewer id and the prompt counted every dirty comment as read-only, disagreeing with the actual skipped/viewer-unavailable outcome. The counts and prompt now carry an explicit viewer-unavailable tally.
Request-counter leak: pearl--graphql-request-async incremented pearl--active-requests before pearl--headers, which signals when the key is unset, so neither callback ran to decrement it. I build the headers before the increment.
Issue commands from comment subtrees: save-issue, refresh, compose-description, open-issue, and the field setters read LINEAR-ID from the nearest heading, so running them from inside a comment hit the comment heading and rejected with "Not on a Linear issue heading". A new pearl--goto-issue-heading-or-error climbs to the issue heading; the commands use it.
Project selector on no projects: pearl-select-project ran (string= selected "None") even when a team had no projects and selected was nil. I guard the nil case. (The reported wrong-type crash doesn't reproduce here, since string= treats nil as the symbol "nil", but the guard makes the optional-no-projects path explicit.)
Query filter validation: pearl-run-saved-query, pearl-list-issues-filtered, and pearl-list-issues compiled their authoring plist without calling pearl--validate-issue-filter, so an unknown key in a saved query was silently ignored. They now validate at the command boundary, so typos surface as a clear user-error.
|
| |
|
|
|
|
|
|
|
|
| |
Pearl could render, create, and edit comments but not delete one, so removing your own comment meant leaving Emacs for the web app. I added the delete command and filled the reserved C-; L d c keymap slot.
pearl-delete-current-comment runs from inside a comment's subtree. It's own-only, reusing the viewer gate from pearl-edit-current-comment: a comment authored by someone else, a bot, or an integration is refused with no commentDelete call. It confirms before the destructive mutation, and on success removes that comment's Org subtree, leaving sibling comments and the issue body untouched. pearl--delete-comment-async mirrors pearl--delete-issue-async.
The delete is permanent. I verified Linear's commentDelete against the live API: the comment is not found immediately after, with no restore path, so unlike issueDelete this isn't a recoverable soft delete. Because of that, a comment with unsaved local edits (or no stored hash) gets a stronger "discard your local edits" confirmation rather than the plain prompt.
I surfaced it as K in the transient Delete group and as d c in the prefix keymap, and inverted the keymap test that asserted d c was unbound.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
I folded the Codex reviews into four draft specs and brought each to Ready. The reviews surfaced real failure modes, not nits, so the changes are substantive.
The comment-deletion spec is Ready: I verified Linear's commentDelete contract against the live API. The mutation is commentDelete(id: String!) returning success, and the comment is not found immediately after, so there's no restore path and undo is impossible. The dirty-local-comment policy is allow-with-discard-wording, the prompt names both the remote delete and the local removal, and the d c keymap / K transient slots are settled.
The multi-account spec is Ready. The original "set the globals on switch and let downstream stay oblivious" model leaked across accounts in the async code, so it's redesigned around an account-context layer: dispatch-time context snapshots, #+LINEAR-ACCOUNT file ownership with wrong/unmarked-buffer guards, a runtime (not persisted) active-account, a mode-line indicator, and an exact startup rule. The five safety calls are settled as the safer defaults.
The sort-order spec is Ready. Client-side sort now moves whole issue subtrees by LINEAR-ID byte-for-byte rather than reparsing and rewriting, which would have dropped unsaved edits. Header persistence is atomic with the reorder, and Custom Views refuse server-side sort in v1 since customView.issues has no verified orderBy.
The labels-as-org-tags spec is Ready: collisions render the shared tag once with the drawer authoritative, Pearl owns the whole issue-heading tag set, manual tag edits are overwritten and never pushed, and Unicode alphanumerics are preserved.
|
| |
|
|
|
|
|
|
|
|
| |
The command surface had no muscle-memory path. Everything went through M-x or the transient, and the transient grouped commands by an ad-hoc mix (issue-at-point, create, org-sync) rather than by what you're doing.
I added pearl-prefix-map, an opt-in prefix keymap organized as save / edit / new / delete. It isn't bound at load, because a global multi-key prefix isn't reliably free across terminals and GUIs. Each binding is a (label . command) menu item, so which-key shows the label without pearl depending on which-key. The user binds the map to a free prefix (the README suggests C-; L). So C-; L s s saves the ticket at point, s a saves the whole file, e p edits priority, n t creates a ticket, m opens the transient.
I regrouped the transient to mirror those categories: Save, Edit, New, Delete for ticket actions, and Fetch, View, Setup for the workspace. I added the two save commands and relabeled every entry to the verb wording. The per-field push entries (edit-desc, edit-title) are gone since the save model subsumes them, though the commands still work from M-x. I pulled the org-sync commands (enable, disable, push-file) from the menu too, since they're plumbing, tracked separately for going private.
The keymap binds the existing command names. Aligning those names with the labels (edit-state, new-comment) is a separate task, as is the delete-comment command that will fill the d c slot.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Editing a ticket meant remembering which per-field command to run: pearl-sync-current-issue for the description, pearl-sync-current-issue-title for the heading, pearl-edit-current-comment for a comment. Three fields, three commands, and no single way to edit several fields (or several tickets) and push them at once.
I added a layered save engine and two commands over it. pearl--run-field-save does the fetch, the three-way conflict gate, and the push for one field, emitting a single structured outcome instead of messaging from its callback. The three interactive sync commands are now thin wrappers over the per-field savers, so they keep working and gain the outcome. pearl-save-issue diffs the ticket at point and pushes only what changed. pearl-save-all scans the whole file, confirms once naming the field counts, and saves every diff in a sequential pass that continues past a per-ticket conflict. A queue runner keeps at most one conflict-resolution buffer live at a time.
I also fixed a correctness bug: a description push now advances both LINEAR-DESC-SHA256 (the markdown the remote gate hashes against) and LINEAR-DESC-ORG-SHA256 (the rendered-Org baseline the local scan uses). Advancing only the markdown hash left the Org hash stale, so the next local scan would flag a just-saved ticket as dirty again.
The conflict resolver gained an optional outcome callback so the engine can report cancel, use-local, use-remote, and the deferred smerge commit or abort as distinct outcomes. I removed the now-dead pearl--commit-sync-decision. The keybinding scheme and the transient menu retarget are a separate follow-up.
Spec: docs/ticket-save-model-spec.org.
|
| | |
|
| |
|
|
|
|
| |
First layer of the unified save model (docs/ticket-save-model-spec.org). pearl--issue-dirty-fields returns which of title, description, and comment candidates changed in the issue subtree at point, with no network call. Description dirtiness reuses pearl--subtree-dirty-p, which hashes the rendered Org against LINEAR-DESC-ORG-SHA256, so a description whose markdown is lossy under org->md isn't falsely flagged. Comment candidates hash org->md of each body against LINEAR-COMMENT-SHA256, matching how pearl-edit-current-comment computes its no-op check.
Ownership is a separate phase: pearl--classify-comment-candidates splits candidates into own versus read-only once the viewer id is known, since the local scan can't tell who authored a comment without it. The save-issue and save-all commands that consume these scanners land in the next layers.
|