All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Windows: the CLI ran at all.
import fcntlsat at module scope, so every command — not justqueue— died withModuleNotFoundErroron Windows from v0.18.0 onward.fcntlandmsvcrtare both optional imports now, and the drain lock dispatches per platform:fcntl.flockon POSIX,msvcrt.lockingon Windows. - Windows
LK_LOCKgives up after ~10s whereflock'sLOCK_EXwaits forever;blocking=Trueretries instead of reporting the timeout as "another drain holds the lock". - A real I/O error during a blocking lock (bad fd, invalid argument) is raised instead of retried forever. Only documented contention codes retry.
linear projects update --priority— set a project's priority (urgent|high|medium|low|none), the same names--priorityalready takes on issues. Previously the only way to change a project's priority from the CLI was a rawprojectUpdateGraphQL call.- Priority is now readable.
linear projectsprints a Priority column andlinear projects <name>prints aPriority:line, so a change made with--prioritycan be confirmed without opening Linear.
linear queue/linear queue list/linear queue drain— durable local queue for closes that cannot be written immediately (RUSH-2307).linear update <id> --done --proof ...persists the intent before attempting the API; on a Linear rate limit (429) or transient error the intent is retained in~/.linear-cli/queue/and retried later.linear queue drainapplies pending closes with exponential backoff;linear queue drain --onceapplies a single due intent;linear queue drain --dry-runpreviews them. The nextlinear update --donealso drains automatically.- Concurrent drain safety.
queue_drain_lock(fcntl.flockon~/.linear-cli/queue/.drain.lock) serializes drains on one machine so two processes cannot double-apply or corrupt intents. - Retry-After honored.
gqlsurfaces HTTPRetry-Afterin GraphQL error extensions; drain prefers that delay over exponential backoff (still capped). - Idempotent queued closes. Duplicate intents for the same ticket collapse to the latest proof/comment. A drain that finds the issue already in the desired state removes the intent without re-posting proof.
- Bounded queue growth.
MAX_QUEUE_SIZEcaps the number of distinct intents; new intents are rejected when full, but existing intents can still be updated.MAX_QUEUE_ATTEMPTSlimits retries so stuck items do not linger forever.
gqlsurfaces HTTP status codes (and Retry-After) in GraphQL error extensions so callers can distinguish rate limits (429) and transient errors from permanent failures.
- README and
skill.mddocument the queue surface and a rate-limit runbook for agents (close batches with queue, never tight-loop).
linear projects update <name|id>— set project description, name, lead, start/target dates, and state. Resolves the project strictly (mistyped name aborts).--stateaccepts a status type (backlog/planned/started/paused/completed/canceled) or a workspace status name;--lead/--start/--targetacceptnoneto clear.--description-filereads a multi-line body (or-for stdin).linear initiatives— workspace initiatives for agent workflows: list / show / create / update / link / unlink / archive.linkandunlinkattach projects via Linear'sinitiativeToProject*mutations; show lists linked projects with progress. Status values:Proposed|Planned|Active|Completed|Canceled.projects showprints Description when set, so a post-update check does not need--json.
- README and skill.md cover
projects updateand theinitiativesgroup.
- Config file is private.
save_config/ first-write / legacy migration now create~/.linear-clias0700andconfig.jsonas0600(the API key lives there). A pre-existing loose-mode config is re-chmod'd on everyload_config. install.shfails closed on a bad download. Pins a release tag (defaultv0.16.1, not floatingmain) and verifies SHA-256 before moving the binary into place. Override withLINEAR_CLI_VERSION/LINEAR_CLI_SHA256only when deliberately installing a different revision.
- BREAKING:
delegateis the only ownership model.agent:<name>labels no longer own anything.linear tasks --agent <name>filtered on anagent:<name>label andlinear tasks --boardgrouped its columns by the same label, so an issue delegated to Claude through Linear's own delegation UI did not appear in Claude's queue, and an issue merely taggedagent:claudedid. Both now read the nativedelegatefield; "unowned" means exactlydelegateis null. A leftoveragent:*label is inert — it is an ordinary label with no effect on any queue. - An unknown
--agentaborts instead of listing an empty queue. The name is resolved against the workspace agent roster (linear agents); a typo exits non-zero and prints the delegatable names. An unattended drain reading a silent empty list as "queue clear" was the failure this prevents. The configured default agent only warns:get_agentsdegrades to an empty roster on a transient API error, and bricking the most-used command on a users-query blip is worse than the typo it would catch. The filter compares delegate names case-insensitively either way, so the raw name still matches. --labelnow composes with--agent. It used to be dropped whenever an agent filter was active, because ownership was itself a label and the two label filters fought. Ownership is the delegate field now, so the two are orthogonal.
-
linear migrate-agent-labels— one-time migration off the legacy labels. Dry run by default;--applywrites. It sets the delegate from a resolvableagent:<name>label, strips the migrated labels, and deletes eachagent:*label once nothing carries it.It never overwrites an existing delegate. An issue already delegated to someone other than its label claims, or carrying two
agent:*labels naming different agents, is reported asCONFLICTand left untouched. A label whose suffix is not a delegatable agent (a machine name, a workflow flag) is reported asUNRESOLVEDand kept together with any sibling label on that issue — mixed state needs a human, and migrating half of it silently is worse.Decisions are per issue, not per label, so an issue with two
agent:*labels gets exactly one write; two writes computed from the same pre-mutation snapshot would resurrect each other's stripped label and silently overwrite the delegate. A label is deleted only when nothing carries it workspace-wide and including archived issues —list_team_labelsalso returns workspace-scoped labels, so a team-scoped scan finding no hits does not mean unused, and Linear'sissuesconnection excludes archived issues unless asked, so a label carried only by archived work would have looked free to delete. A label carried by more than 200 issues is kept outright: the gate subtracts what the run clears, so a truncated carrier list whose every entry happened to be cleared would subtract to empty and read as free. The dry run projects the post-migration state, so it previews the deletes the run would make rather than reporting every label as still carried by the very issues it just said it would strip.--applyexits non-zero when anything is left behind, including a failedissueUpdateor a failedissueLabelDelete, and the summary counters report writes that actually landed rather than writes that were planned. A dry run exits 0: it is an inspection, and reporting work-to-do as a failed migration left no path to a green run.
milestone_rollupdeclared its project-id variable asString!, but Linear'sissues(filter: { project: { id: { eq } } })comparator expectsID. On the live API the query errored every call, and (since the rollup returns{}on error) every milestone silently showed(0 issues). Corrected toID!;linear projects "<name>"andlinear milestones list "<name>"now report real per-milestone progress. Added a regression test asserting the variable type.
- Milestone visibility. Every issue now carries its
projectMilestoneandcycleinISSUE_FIELDS— solinear tasksandlinear tasks <ID>show which milestone an issue belongs to, and the milestone×cycle join is queryable in one fetch (previously the milestone was settable via--milestonebut never shown). linear tasks --milestone "<name|uuid>"filters the queue to one milestone. Pair with--projectto disambiguate a milestone name across projects.linear tasks --by-milestonegroups the list by milestone with aNo milestonebucket for issues matched to a project but no milestone (the "what isn't matched to a deliverable" surface). Each row is annotated with its cycle. Auto-on when--projectscopes the list.- Per-milestone progress rollups.
linear projects "<name>"andlinear milestones list "<name>"now show(done/total done, N%)per milestone, plus aNo milestonerollup — computed from a single slim issue query per project.
- Scoping
linear tasksto--projector--milestonenow defaults to all cycles (the whole deliverable), not just the active cycle's slice. Pass an explicit--cycleto narrow. A barelinear tasksstill defaults to the active cycle. linear milestones listshows each milestone's progress rollup in place of its raw UUID in the human view (the id remains in--json).
linear create --image PATH(repeatable) uploads each image viaupload_file()and embeds it asin the issue description.
linear inboxis now agent-actionable:- Actions:
--read <id>marks specific notification(s) read (repeatable);--read-allmarks everything read (vianotificationUpdate/notificationMarkReadAll). - Richer
--json: each notification now carries the issue (identifier,url,state), the comment (id,url,body), the threadparentComment, and the actor — enough for an agent to follow the ticket/thread and act. - The plain listing prints the follow-up recipe: reply with
linear update <ID> --comment "...", dismiss withlinear inbox --read <id>.
- Actions:
linear inbox— show your Linear inbox (notifications): comments, mentions, assignments, and status changes on issues you follow. Unread only by default;--allincludes already-read,--limit Nbounds the fetch,--jsonfor raw output. Backed by the GraphQLnotificationsquery — no browser needed.
- Add arcade-style README diagrams for the task lifecycle, the human/agent flow,
the zero-dependency / zero-supply-chain differentiator, and the comparison
scoreboard.
assets/flow.svgupgraded to match.
linear cycles delete. It mapped to Linear'scycleArchive, which discards a cycle's sprint history (issues, velocity, the numbered slot) — a destructive, rarely-needed op that doesn't belong in an agent-facing CLI. Cycles are now list/create/update only. Label/milestone/project deletes are unaffected.
- Cycle display for numbered (nameless) cycles. Linear's auto-numbered
cycles carry
name: nulland only anumber, but every display site readcycle.name— so a ticket that was in the active cycle rendered asNone(tasks <id>) orno cycle(create/updateconfirms), making cycle assignment look broken when the write had actually succeeded. All cycle selections now requestnumberand render through a singlecycle_label()helper (name, elseCycle {number}, elseno cycle); named cycles are unchanged.tasks --cycle activeheaders now readCycle 20instead of the genericActive cycle.
linear tasks --project NAME|UUID— scope the task list to one project. Resolution is strict: an unknown project aborts with close-match suggestions instead of silently dropping the filter and returning the whole team queue (unattended consumers act on whatever this lists).
Brings the human side to parity with the agent side: humans are assignable by
name just as agents are delegatable by name, and users no longer buries the
two people among the app users.
usersnow groups its output into Humans (assign with--assign) and Agents (delegate with--delegate), using Linear'sappuser flag — the same flagagentsuses. No more mixing real people with OAuth app users.--jsonstill emits the flat list, now including theappfield per user.--assign(oncreateandupdate) accepts a human's name or displayName (case-insensitive), not just an email — so--assign bismaworks like--delegate claude. Email andnonestill work. Agents (app users) are excluded from name resolution on purpose: an unmatched name warns and points atlinear users, steering agent hand-offs to--delegate.
Trims the issue surface to what this workspace actually uses, and discourages casual issue nesting. The model stays lean: task · project · milestone · cycle · label · status · priority · assignee/delegate.
--estimateoncreateandupdate(and the Estimate line in issue detail). Story-point estimation isn't used here, so the flag was pure ceremony — it's gone from the code, not just hidden. A strayestimatekey in a--from-filebulk line is now silently ignored rather than sent.
--parent(sub-issues) now prints a non-blocking tip to stderr when it nests an issue ("prefer a top-level issue under a project/milestone unless you truly need it"). It's a nudge, not a prompt — bulk (--from-file) and agent runs stay unblocked, and the parent is still set.
Makes the CLI own the full project lifecycle and adds milestone management, so a
workspace can be reorganized entirely from the shell instead of the web UI. Also
closes a silent-failure footgun where a mistyped --project no-op'd every issue
in a batch while reporting success.
projects create --name <name> [--description ...] [--lead <email>] [--start YYYY-MM-DD] [--target YYYY-MM-DD]— create a project on the current team (projectCreate). Prints the new project's id and URL.projects archive|delete <name|id>— remove a project (projectDelete; moves it to the workspace trash, recoverable in Linear's UI). Linear exposes no distinct project-archive mutation, so both verbs are the same operation.projects show <name|id>— explicit detail view. Bareprojects <name>still works (shorthand forshow). Accepts a project id and prefers an exact name match over substring, soRushstays addressable onceRush App/Rush CLIexist. Detail now also shows the issue count, start date, and id.milestonescommand —list <project>,create --project --name [--target] [--description],move <milestone> --to <project> [--from <project>],set-target-date <milestone> <YYYY-MM-DD|none> [--project], anddelete <milestone> [--project](projectMilestone{Create,Update,Delete}). Setting a milestone's target date from the CLI is reliable where the web date-picker is not.projectslist now shows a per-project issue count (fromscope), so a bulk backfill can be verified without aggregatingtasks --json.
- A named
--project/--milestonethat doesn't resolve is now a hard error with close-match suggestions, not a warn-and-skip on a success exit.updateresolves the project/milestone once, up front; an unknown name aborts the whole run (non-zero) before any issue is touched, instead of silently no-op'ing every ticket in a batch while reporting success. Resolving once also removes a per-issue project lookup on bulk updates.
Makes delegation legible: the CLI now reads and displays the delegate, so a
handed-off issue shows who's on it. Before this, --delegate was write-only —
you could hand an issue to an agent but querying it afterward only ever showed
the human assignee.
delegate { name }is now requested in the list and detail queries.tasks/ list rows render the delegate alongside the assignee asAssignee → delegate(e.g.Muqsit → claude); rows with no delegate are unchanged.showprints aDelegate:line underAssignee:when the issue is delegated.
Makes the CLI aware of Linear's agent members (app users like Claude, Codex,
Kimi, …) and adds first-class delegation — the supported way to hand an issue to
an agent (Linear silently ignores an app user in assigneeId).
agents— list the workspace's agent members, auto-detected via the Linearappuser flag. The roster is cached in config and auto-refreshed every 6h;agents --refreshforces an immediate re-fetch after installing/removing an agent app.--jsonfor scripting.update --delegate <name>andcreate --delegate <name>— delegate an issue to an agent by name (case-insensitive, e.g.--delegate claude), ornoneto clear. Resolves the name against the cached roster (refreshing once on a miss), then setsdelegateId. The human stays the assignee; the agent becomes the delegate.
- Docs and the hero flow diagram (
assets/flow.svg) now teach delegation instead of the retiredagent:*lane-label hand-off —--label agent:fooexamples are replaced with--delegate <name>; the diagram shows the ticket carrying both an assignee (human) and a delegate (agent).
- Agent roster caching reuses the volatile-config guard, so
--teamoverrides never persist another workspace's roster.
Finishes the shell-native management story for cycles and labels, completes the
pagination sweep, and makes update a batch tool. Closes the remaining open
issues (#3, #4, #5, #6, #7, #8).
cycles create|update|delete— manage cycles from the shell instead of dropping to raw GraphQL (cycleCreate/cycleUpdate/cycleArchive).cycles --ids(and--json) surface the cycle UUIDs. (#4)labels create|update|delete— label CRUD viaissueLabelCreate/issueLabelUpdate/issueLabelDelete, with fuzzy id-or-name resolution, for cleaning up accidental/one-off labels. (#5)tasks --cycle <name|id>— scope the list to any specific cycle by fuzzy name, number, or UUID (on top of the existingactive|next|all|none).backlogis now an accepted alias fornone. (#6)tasks --since YYYY-MM-DD— floor the list to issues created on/after a date. (#6)- Bulk
update: pass multiple identifiers (update RUSH-1 RUSH-2 --cycle none) or pipe them with--stdin(... | linear update --stdin --label x). Per-issue[i/n]progress, errors rolled up at the end (one bad ticket never aborts the batch), single-ticket output unchanged. (#7)
tasks --boardno longer queries the non-existentteam.nextCyclefield —--board --cycle next(and every board view) now resolves the cycle the same way as the list view, so it returns results instead of a GraphQL error. (#3)tasks --boardis now fully paginated and accepts all cycle scopes; it previously hit the old single-pageactiveCycle.issuespath and silently capped at 50. (#8)- Pagination now also covers
cycles,labels,users, andprojects(and the label lookups used byupdate/create), via a sharedpaginate_connectionhelper — no list silently truncates at Linear's default page size. (#8)
- Detail view now renders symmetric relations (
related,duplicate) from the inverse side too — previously onlyblocksinverted to "Blocked by", so arelated/duplicatelink was invisible from the issue that didn't author it. Found while live-testing relation writes against a real team.
Closes the load-bearing read gaps for agent workflows. The headline is pagination: lists no longer silently truncate at Linear's 50-issue page cap — on a real cycle this surfaced 137 tasks where the old code showed 50, the exact cause of "search before you create" missing existing tickets and filing duplicates.
- Full pagination on every issue list (
tasks,--query,--board) — followspageInfoto the end instead of stopping at the first 50 results tasks --cycle all(whole team: every cycle + backlog) and--cycle none(backlog only) — previously listing was locked to the active/next cycletasks --assignee me|none|<email>— filter by real assignee, not just theagent:label lanelinear states— list the team's workflow states, so agents stop guessing status names and learning they were wrong only when a mutation failsupdate --blocks,--blocked-by,--relates(repeatable) — create issue relations viaissueRelationCreate- Detail view (
tasks ANT-N) now shows relations (Blocks / Blocked by / Related to / Duplicate of), plus project, cycle, parent, url, estimate, and due date
tasks --jsonshape:{ scope, cycle, count, issues }(theissuesarray is unchanged;cycleis null forall/nonescopes)- Detail-view query enriched —
--jsonno longer drops project/cycle/parent/ url/estimate/dueDate/relations
0.2.0 - 2026-06-07
Broad expansion of the create/update surface to cover the fields agents actually
need — projects, milestones, sub-issues, story points, reassignment — plus
discovery commands and bulk create. Symmetry fix: every field settable on
create is now changeable on update with the same flag.
--project NAME|IDand--milestone NAMEoncreateandupdate. Name resolution with smart-pick on ambiguity (most-recently-updated match) — no flag dance required--parent ANT-Noncreateandupdatefor sub-issues;noneon update detaches--estimate Noncreateandupdate(story points)--titleand--descriptiononupdate(rename / replace body)--description-file PATHoncreateandupdate, with-for stdin (multi-paragraph markdown without shell-escape pain)--unlabel NAMEonupdate(repeatable) — mirrors--labeladd semantics for clean agent hand-off--priorityand--assignonupdate(symmetry fix — both werecreate-only)--query "text"ontasks— case-insensitive search across title + description; composes with all existing filters--from-file plan.jsonloncreate— bulk creation, one JSON object per line,-for stdin. Continue-on-error; tab-separated output--team KEYglobal override (top-level flag, stateless — doesn't mutate config)linear projects— list projects in the team with status, progress, leadlinear projects <name>— detail view with milestones inlinelinear labels— list available labels for the teamlinear users— list active users (for--assignlookup)
linear createtitle argument is now optional: if only--descriptionis provided, the title is derived from its first sentence/line (markdown noise stripped, capped at 80 chars). Only true error case: neither title nor description givencmd_createinternally refactored: shared_build_create_input+_send_issue_createhelpers used by both single and bulk paths — no duplicated resolution logic
0.1.2 - 2026-04-26
install.sherror message claimed Python 3.10+ was required; the script actually works on 3.9+ (matches README, badge, comparison table, and FAQ)SECURITY.mdclaimed env vars were rejected as a key-resolution path; the CLI has always supportedLINEAR_API_KEYenv var and macOS Keychain. Rewrote the section to document all three paths (config / env / Keychain) with their tradeoffs
0.1.1 - 2026-04-24
Documentation polish on top of v0.1.0. No code changes beyond the version bump.
- Hero demo video (1080p, 17s, with audio) embedded in README, sourced from the v0.1.0 release asset
assets/flow.svg— replaces the ASCII flow diagram with a dark-grid SVG showing the human-files / agent-implements / human-reviews loop- "Zero supply chain attack surface" section in README, with matching badge
- "Works with" harness logos in README (Claude / Codex / Gemini / Cursor) under
assets/harnesses/
- Replaced personal email and handle in
assets/flow.svgwith example values
0.1.0 - 2026-04-24
First public release.
linear tasks— your queue in the active cycle (assignee-as-queue model)linear tasks --board— whole team board across cycleslinear tasks <ID>— issue detail view with commentslinear tasks --json— machine-readable output for piping tojqor subagentslinear update— claim (--pickup), comment (--comment), close with proof (--done --proof), set status, change cycle, add labelslinear create— open a new issue with priority, labels, descriptionlinear cycles— list cycles for the active teamlinear setup— configure API key, default team, and agent identity- Proof-first completion:
--proofaccepts files (uploads as attachment), URLs (records as link), or text (appends as comment) - Per-agent lane filtering via
--agentsetup flag (e.g. only see tasks labeled forclaude) - Auto-migration from
~/.agents/linear.jsonif present - Single-file Python distribution (~43 KB), zero third-party dependencies