Linear.app issue sync extension for pm-cli.
Fetches issues from a Linear team and upserts them as pm items, keeping identifiers, priorities, estimates, statuses, labels, due dates, assignees, cycles, customer names, and project names in sync. Also provides a native import pipeline (pm linear import) and an exporter (pm linear export) that renders pm items as Linear issue-create payloads, and declares linear_id / linear_url provenance fields.
| Capability | Surface |
|---|---|
commands |
pm linear sync, pm linear validate, pm linear schema, pm linear capture |
importers |
pm linear import (+ legacy linear-sync importer) |
importers (exporter) |
pm linear export |
schema |
linear_id, linear_url item fields + command/importer/exporter flags |
preflight |
credential + reachability guard for commands that reach Linear |
Offline vs live.
--dry-runnever writes to Linear, but "no writes" and "no network" are not the same thing:
- Offline (no
LINEAR_API_KEYneeded):sync/import/linear-sync importwith--dry-runalone, andexport --dry-runwith or without--push. These build and print the exact GraphQL request/variables, or the would-beissueCreate/issueUpdatemutations, and make no network call.- Reaches Linear (live
LINEAR_API_KEYrequired): the real (non-dry-run)sync/import,export --push,validate --check-network, andsync/importwith--forward-only --dry-run— the live preview fetches issues in order to build the plan it reports, so it needs credentials even though it writes nothing.The preflight gate follows exactly that split: it guards commands that reach Linear, not commands that mutate it.
- pm-cli /
@unbrained/pm-cliSDK>=2026.10.24, resolvable from the installed extension - Node.js
>=22.18.0 - A Linear API key with read access to the relevant teams (only for live
paths; every
--dry-runand offlinevalidateworks without one)
The host minimum covers the guarded schema, history and preview APIs used by default imports. npm's peer requirement and the pm manifest enforce the same minimum. Existing tracker data stays readable; upgrade the host before loading this package on an earlier SDK.
- Go to Linear → Settings → API (
https://linear.app/settings/api) - Create a Personal API key (or a workspace key for shared use)
- Copy the key into your shell environment.
export LINEAR_API_KEY=<linear-api-key>Add this to your shell profile (~/.zshrc, ~/.bashrc, etc.) to persist it.
pm install npm:pm-linear --globalThe
npm:prefix is required. A barepm install pm-linearresolves only a local directory or a bundled alias, never the registry, and agithub.com/unbraind/pm-linearsource cannot work either — pm copies a GitHub source as-is without building it, and this repository does not commitdist/.
Or install per-project:
pm install npm:pm-linear --projectpm linear capture --thread ROOT_COMMENT_ID --item EXISTING_ITEM_ID proposes an
attributed, revisioned core evidence note from one explicitly selected discussion
and its direct replies. Preview and metadata-only disclosure are the defaults;
--disclosure text explicitly includes bodies and names. Deliver a reviewed
proposal with --apply --revision TOKEN. Reimport deduplicates through the SDK;
source edits append new evidence without replacing previous revisions. No items
are created and no discussions are discovered or harvested in the background.
TOON is the default; --json returns the same compact receipt. The typed SDK
exports captureLinearThread, options/report/note types and actionable
ThreadCaptureRefusal codes. See the complete example and disclosure contract.
Fetches issues from a Linear team and syncs them into pm items.
pm linear sync --team <slug> [options]
| Flag | Type | Default | Description |
|---|---|---|---|
--team |
string | (required) | Linear team slug (e.g. ENG, BACKEND) |
--project |
string | — | Filter by Linear project name |
--state |
string | — | Filter by state name (e.g. "In Progress") |
--cycle |
string | — | Filter by cycle name (e.g. "Sprint 7", "Q3"); case-insensitive substring match, issues with no cycle are excluded |
--assignee |
string | — | Filter by assignee email |
--label |
string | — | Filter by label name |
--updated-since |
string | — | Only issues updated at/after an ISO date or duration (e.g. 2026-01-01, -P7D) |
--status-map |
string | — | Override status mapping, e.g. "In Review=in_progress,Backlog=open" |
--map |
string | — | Remap Linear→pm fields, e.g. "identifier=ignore,priority=ignore" |
--project-map |
string | — | Tag items by Linear project name (additive). Bare flag tags with the verbatim name; "Mobile=mobile,Web=web" remaps |
--limit |
number | 100 |
Max issues to fetch |
--dry-run |
boolean | false |
Print the exact GraphQL request, no writes. Offline on its own; with --forward-only it fetches and simulates the native apply path and needs LINEAR_API_KEY |
--atomic |
boolean | false |
Refuse unavailable batch atomicity unless --forward-only explicitly accepts migration |
--forward-only |
boolean | false |
Explicitly accept retained prior successes and per-item outcomes; with --dry-run, fetch and simulate the apply path |
--skip-preflight-network |
boolean | false |
Skip the preflight reachability probe (offline/CI) |
--map linearField=pmField[,…] adjusts how Linear fields feed pm fields at
import time. The special value ignore suppresses a field:
--map entry |
Effect |
|---|---|
identifier=ignore |
Drop the [ENG-1] prefix from the pm title |
priority=ignore |
Skip priority (pm item gets the default) |
labels=ignore |
Skip label→tag import (also drops the cycle:<name> tag) |
status=ignore |
Skip status mapping (pm item stays open) |
assignee=ignore |
Suppress the provider value and clear any existing pm assignee |
estimate=ignore |
Skip the estimate:<points> context tag |
customer=ignore |
Skip the customer:<name> context tag |
--project-map is additive: imported items keep their label-derived tags and
gain one more tag for their Linear project. It mirrors --status-map.
--project-map value |
Effect |
|---|---|
(bare flag) / * / true |
Passthrough — tag each item with its verbatim Linear project name |
"Mobile App=mobile,Web=web" |
Remap — tag matched projects with the mapped value; an unmatched project falls back to its own name |
"Legacy=ignore" |
Suppress tagging for that specific project |
# Tag every imported item with its Linear project name
pm linear sync --team ENG --project-map
# Map project names to short tags
pm linear sync --team ENG --project-map "Mobile App=mobile,Web=web"# Sync all open issues from the ENG team
pm linear sync --team ENG
# Only sync issues currently In Progress
pm linear sync --team ENG --state "In Progress" --cycle "Sprint 7"
# Restrict to a single Linear project
pm linear sync --team ENG --project "Q3 Roadmap"
# Custom status mapping (Linear state name -> pm status)
pm linear sync --team ENG --status-map "In Review=in_progress,Backlog=open"
# Sync up to 200 issues from the BACKEND team
pm linear sync --team BACKEND --limit 200
# Preview what would be synced (no writes)
pm linear sync --team ENG --dry-runNative import pipeline. Pulls issues from a Linear team (and optional project) via the
GraphQL API and creates pm items. Accepts the same --team, --project, --state, --cycle,
--assignee, --label, --updated-since, --status-map, --map, --project-map,
--limit, and --dry-run flags as pm linear sync.
pm linear import --team ENG
pm linear import --team ENG --project "Q3 Roadmap" --limit 50
pm linear import --team ENG --dry-runRequires LINEAR_API_KEY (or falls back to LINEAR_DEFAULT_TEAM for the team). When
the key is missing it exits non-zero with a structured error rather than crashing.
Sync/import matching, offline import previews, and export read the entire local
tracker through the public in-process SDK listAllComplete({ includeBody: true }).
The read includes closed and canceled items, all item types, full metadata and
bodies, with strict source checks and unbounded row/output budgets. Extension
hooks are disabled for this observational read. There is no 10,000-item limit or
CLI JSON buffer ceiling.
Before planning writes, pm-linear verifies the completeness certificate, source
and output receipts, and the fields used for matching/export. It refuses missing
items, missing or invalid certificates, partial/unverified scans, pagination,
count mismatches, duplicate/invalid IDs, partial projections, field omissions,
compacted/omitted output, read-session projections, missing bodies, and malformed
rows with a CommandError. Import certifies before fetching issues; export
certifies before provider lookups or mutations. An unreadable tracker also fails
an offline preview instead of reporting a misleading zero matching count.
The error includes a recovery hint: install/upgrade the SDK, repair unreadable tracker artifacts, and retry. For a diagnostic complete CLI read, use:
pm list --all --full --include-body --strict-read --no-truncate --output-budget unbounded --output-limit unbounded --jsonSee real-tracker acceptance and refusal evidence.
The extension loads the SDK lazily so command discovery still works on older or standalone hosts. Local reads require a resolvable SDK at the minimum version; an unavailable SDK fails with the same recovery hint.
Renders pm items as Linear issue-create payloads. The exported payload is
symmetric with the importer: a pm item's priority, tags, deadline,
estimate:<n> tag, and cycle:<name> tag are carried into the Linear mutation as
priority (int), labelIds (resolved from tag names to the team's existing
labels at push time), dueDate (YYYY-MM-DD), estimate (int), and cycleId
(resolved from the cycle name to the team's cycle at push time) respectively —
alongside title, description, and the status→state mapping.
The importer encodes Linear's estimate and cycle as the namespaced tags
estimate:<points> and cycle:<name> (pm has no first-class field for either).
Export promotes those tags back to first-class Linear issue fields and does not
re-emit them as Linear labels, so the round-trip is lossless and clean.
- Default (no
--push): prints the JSON payload to stdout. Safe, read-only, no network. Items that already carry Linear provenance are flaggedalreadyInLinear: true. --push: creates/updates the issues in Linear. Only mutates Linear when both--pushis set andLINEAR_API_KEYis present, and requires--team <slug>to resolve the target team (and its labels/states). Items already linked to Linear are updated in place so the push is idempotent. Tags that don't match an existing team label are dropped rather than failing the push. A per-item create/update failure is isolated: it is logged, counted asskipped, and the batch continues rather than aborting the remaining items.
# Preview the payload (no network, no writes)
pm linear export
# Create the issues in Linear (requires LINEAR_API_KEY)
pm linear export --push --team ENG| Flag | Type | Default | Description |
|---|---|---|---|
--push |
boolean | false |
Create the issues in Linear (requires key + --team) |
--team |
string | — | Target Linear team slug (required with --push) |
--map |
string | — | Suppress export fields, e.g. "estimate=ignore,cycle=ignore" |
--dry-run |
boolean | false |
Offline — print the would-be issueCreate/issueUpdate mutations + variables, no network |
Cycle resolution. The
cycle:<name>tag carries a cycle name; Linear's mutation input needs acycleId. On--push, the exporter resolves the name against the team's cycles (fetched alongside its labels/states) and setscycleIdwhen it matches (by cycle name or number, case-insensitively). A cycle name that doesn't resolve — offline, unknown, or a workspace that doesn't model it — is skipped (the rest of the issue still pushes) with a single stderr warning per push. Use--map cycle=ignoreto drop cycle export entirely, or--map estimate=ignoreto drop estimate.
# Preview the exact Linear mutations that a --push would send (no network)
pm linear export --dry-run --team ENGpm linear schema --team ENG reads the team's current workflow states, team and
workspace labels, cycle/triage/estimation settings and default state. It uses the
pm SDK's local status registry to check every --status-map target. This command
always reaches Linear and needs a key with Read access to the target team.
It performs no tracker or Linear writes. linear validate remains a configuration
and credential diagnostic; it does not certify mapping compatibility.
# Compact TOON by default; JSON for a persisted automation receipt
pm linear schema --team ENG --status-map "Review=in_progress"
pm --json linear schema --team ENG --status-map "Review=in_progress"
# Copy schemaVersion from the report; use the same mapping when applying
pm linear sync --team ENG --forward-only --status-map "Review=in_progress" \
--schema-version "linear-schema-v1:<reported-sha256>"Every live sync, native import and legacy import negotiates automatically before
fetching issues and rechecks the version after fetching, before local writes.
--schema-version additionally binds an invocation to an earlier reviewed report.
Deleted/archived labels, label renames, state renames (even with the same status),
state type changes, team identity/settings changes, local status definition
changes and override changes invalidate the old version. Discovery follows both
metadata connections to completion, including workspace labels; partial metadata,
duplicate IDs and broken pagination fail closed. There are at most 1,000 pages.
Reports use format version 1 and a linear-schema-v1: SHA-256 version. Digest
ordering uses UTF-16 code units, independent of runtime locale and ICU. Reports
include settings, total counts, the first 32 state mappings, omittedStates, and
a digest of all labels. Negotiation and hashing always cover every mapping
and label; preview truncation never weakens the check. Sync also returns the
report in its structured schema field and emits it to stderr before applying.
No issue bodies or credentials appear in the report.
SDK callers can import negotiateLinearSchema, LinearSchemaOptions,
LinearSchemaReport, SyncOptions, SyncResult, LinearSchemaRefusal and
LinearSchemaRefusalCode from pm-linear. Refusals carry code, remedy, exit
code 2, and a fresh report when a version comparison fails.
| Refusal | Remedy |
|---|---|
access_denied |
Restore Read permission for the target team, states, settings and labels; check the team key/API schema support. |
schema_unavailable |
Restore connectivity or repair the local SDK/schema, then retry discovery. |
metadata_incomplete |
Restore full metadata and stable, complete pagination before retrying. |
mapping_invalid |
Remove stale overrides or register/correct the target pm status. |
schema_drift |
Run linear schema again, review the new report and use its version. |
An offline --dry-run still makes no network request and cannot certify remote
metadata. --forward-only --dry-run negotiates and previews against current metadata.
Negotiation proves read access for importing; it does not attest Linear write
permissions or gate export/push. Linear exposes no cross-request metadata snapshot
transaction: a provider change after the final check remains a race. Run schema
review and apply close together; --atomic is an explicit stronger-guarantee request and refuses with atomic_unavailable before import work unless --forward-only explicitly accepts migration.
syncLinearIssues returns SyncResult.issues: the complete fetched provider
array in provider order, including records filtered or skipped by local apply.
This applies to normal, replay, dry-run and deprecated atomic-flag modes. The
provider-free LinearImportReceipt reports native write outcomes;
importLinearForwardOnly returns that receipt because it accepts prepared rows and
fetches no issues. CLI JSON/TOON retain compact counts and importResults without
embedding the fetched issue payloads.
See the SDK example and measured validation.
Live --forward-only --dry-run receipts retain dryRun: true on sync and both import aliases, including empty or filtered results. Counts describe simulated changes; summaries say “Would sync/import”, and no item/history mutation is applied.
A refused or unconfirmed native projection returns projection_failed with a remedy, never an ok ownership observation. Wait for the native lease owner or resolve the import policy/conflict, then re-read both systems. Read-only tracker files are supported; source bytes and permissions remain unchanged. A skipped copy write cannot certify provider values as unchanged.
pm linear ownership --item ITEM --base acknowledged.json compares the last
explicitly acknowledged observation with current local and Linear values. It
returns a compact host-rendered TOON receipt, or JSON with --json. No field
text is included: the reusable observation contains field hashes and revision
digests. Digests are change indicators, not anonymization of low-entropy data.
pm linear ownership --item pm-example --json > preview.json
# Review both systems. Only after acknowledging the observation:
jq '.observation' preview.json > acknowledged.json
pm linear ownership --item pm-example --base acknowledged.json --jsonThe preview reports local_edit, provider_edit, conflict, aligned,
unchanged, or unknown_base per field. disjoint: true requires local and
provider edits on different fields, with no conflicts or unknown fields. An
absent, foreign, changed-mapping, malformed, duplicate, or previously divergent
field base never implies a safe overwrite. Baselines are supplied explicitly;
preview does not write acknowledgements, notes, settings, histories or Linear.
The comparison covers the import projection: title (including the issue
identifier), body, status, priority, tags (including cycle/estimate/customer),
deadline and assignee, plus provenance description. Both sides use SDK
canonicalization; due dates use the existing export date mapping. The preview
calls the same buildItemPlan/resolveStatus mapping as import, including
configured --status-map overrides and the importer's provider-name fallback.
Resulting local statuses use the SDK registry's alias normalization. There is no
separate preview lifecycle policy. Every payload family clears absent mapped
assignees and deadlines through SDK unset, including legacy inputs and ignored
assignment mappings. A missing or null provider assignee does not preserve a
local assignment; review the ownership difference before importing. Ownership acquires the same issue fields
and runs the production import coordinator in an isolated copy, preserving saved
estimate conversions. An unchanged imported issue's acknowledged observation
replays as unchanged. Custom import field/project mappings are not
inferred; use this command for the documented default import projection.
This is an observation for review with no atomic cross-system guarantees. Repeated local and provider reads detect observable drift, but cannot establish a transaction or prevent later edits. The command offers no apply operation. Re-read both systems before any separate write. Linear does not gain ADO's revision-test primitive through this command.
--max-output-bytes accepts integers 2048..65536 (default 8192); the complete
representation of the JSON receipt must fit or the command refuses instead of
truncating it. The budget measures compact UTF-8 JSON; host pretty-printing may
add whitespace. One
provider issue is selected, with two bounded read requests, no automatic retries,
at most 128 labels and a 128 KiB response cap. Partial label connections refuse.
Provider text fields are bounded to 32768 characters, priority to integers 0..4,
and estimate to finite numbers 0..1000000. Baseline JSON files are limited to
16 KiB and eight dense field records. Existing local content is read through
the SDK without applying new content filters. Local isolation is bounded to
20000 filesystem entries and 64 MiB; a larger tracker refuses before provider
access.
The typed SDK export is previewLinearOwnership(options, pmRoot) with
LinearOwnershipOptions, LinearOwnershipSnapshot, LinearOwnershipReport
and LinearOwnershipRefusal. Stable refusal codes are invalid_input,
local_read_failed, selection_required, access_denied, source_invalid,
source_changed and output_budget; each carries a remedy and exit code 2.
source_invalid also names the schema-owned field (for example,
issue.cycle.name or response.data) without echoing its value. Nullable cycle
names and missing optional relation objects are accepted. The shared HTTP
transport validates envelopes for import, export, schema, capture, closure and
validation as well, using LinearSourceRefusal for invalid source contracts.
Import uses the SDK's JSON-array tag input so literal control words, commas
and quotes survive as labels. New imports explicitly clear relationship/scope
defaults instead of inheriting an agent's focus.
Provider values are data. Both import writers use typed SDK options, including
leading-dash titles and literal @file strings; imports never read process
stdin. Until pm-cli#1614 exposes
a supported literal mutation option, LinearImportRefusal reports
unrepresentable_value, the native field, a remedy and exit code 2 for a body
or metadata description whose trimmed value is -, or an assignment whose
trimmed value is none/null (case-insensitive). The same shared guard runs
before import writes and before ownership hashing. Ownership applies the shared
SDK mutation rows only to an owned disposable copy with hooks disabled, then
reads the projected fields back through the SDK. This includes input parsing
and lifecycle effects; it does not certify permission to apply. Dates must be
documented literal calendar dates. The entire selected import batch is checked before its
first item write; unsupported values are preserved rather than silently changed.
Existing local records remain readable and exportable. Structured captured
comments use SDK JSON notes, so control words remain literal inside their text.
The explicit mapping revision makes earlier preview-policy baselines foreign
and review-required rather than reinterpreting them.
See the SDK example and measured validation.
Field ownership previews, focused dependency closure, thread capture, schema negotiation, validate, sync/import and export work in plain pm trackers without a git executable. Live provider operations still need Linear access; offline previews work without credentials. Release tooling that reads git tags needs a repository, separately from tracker operations.
Readiness diagnostics. Reports whether LINEAR_API_KEY and LINEAR_DEFAULT_TEAM
are configured — without leaking the key (only a short prefix + length are
shown) — and whether the workspace is ready for writes. Offline by default;
--check-network (opt-in) probes the Linear API to confirm the key is accepted.
pm linear validate # offline: config presence only
pm --json linear validate # structured report
pm linear validate --check-network # live: confirm the key is accepted (needs key + network)| Flag | Type | Default | Description |
|---|---|---|---|
--check-network |
boolean | false |
Probe the Linear API to confirm the key is accepted (needs network) |
The extension declares two custom item fields (registerItemFields):
| Field | Type | Description |
|---|---|---|
linear_id |
string (optional) | Linear issue UUID |
linear_url |
string (optional) | Canonical Linear issue URL |
Because pm create has no generic setter for extension-declared fields, imported items
encode provenance in their description behind a stable [linear] marker
([linear] linear_id=… linear_url=…). pm linear export reads this marker back so
already-linked items are not re-created on --push.
The extension registers a linear-sync importer for use in pm config or pipelines:
{
"importers": [
{
"name": "linear-sync",
"config": {
"team": "ENG",
"limit": 100
}
}
]
}Set LINEAR_DEFAULT_TEAM as a fallback if team is not specified in config:
export LINEAR_DEFAULT_TEAM=ENG| Linear priority | Linear label | pm priority |
|---|---|---|
| 1 | Urgent | 1 (Urgent) |
| 2 | High | 2 (High) |
| 3 | Medium | 3 (Medium) |
| 4 | Low | 4 (Low) |
| 0 | No priority | 3 (Medium) |
The importer maps every Linear issue to one of pm's four statuses —
open, in_progress, closed, blocked:
| Linear state type / name | pm status |
|---|---|
type started (In Progress, In Review) |
in_progress |
type completed / canceled / duplicate (also legacy cancelled) |
closed |
| name contains "in progress" / "in review" | in_progress |
| name contains "blocked" | blocked |
| name contains "done" / "completed" / "canceled" / "cancelled" | closed |
| everything else (Todo, Backlog, Triage, unstarted) | open |
A --status-map "Linear State=pm_status,…" overrides this heuristic
(matched case-insensitively on the Linear state name) and is inverted to
drive the export/push direction (pm status → Linear state name). The inverted
direction echoes the Linear state name with its original casing preserved
(Backlog, not backlog).
pm create/pm update is the only setter available to a standalone extension,
so imported issues map onto pm's first-class fields (there are no
meta.linear_* fields):
| pm field | Linear source |
|---|---|
title |
[ENG-123] Issue title (the [ENG-123] prefix can be dropped with --map identifier=ignore) |
body |
description |
status |
mapped from state.type / state.name (see above) |
priority |
mapped from priority |
tags |
label names |
tags (cycle:<name>) |
cycle.name — the issue's Linear cycle, encoded as a namespaced tag |
tags (project) |
project.name — only when --project-map is supplied (see above) |
deadline |
dueDate (if set) |
assignee |
assignee.email (or assignee.name when no email); absent/null or --map assignee=ignore clears the local assignee |
description marker |
[linear] linear_id=… linear_url=… provenance (see below) |
Cycle encoding. pm has no first-class cycle/sprint field reachable from a standalone extension's
pm create, so the Linear cycle is stored as acycle:<name>tag (additive, de-duplicated). Filter on it withpm list --tag "cycle:Sprint 7".
pm linear export reverses the core mapping so a round-trip is lossless for the
fields it carries:
| Linear input field | pm source |
|---|---|
title |
title |
description |
body |
stateName / stateId |
mapped from status (via inverted --status-map) |
priority |
priority (pm 1–4 → Linear 1–4, else 0 "No priority") |
labelIds |
tags (resolved to existing team labels; unknowns dropped; estimate:/cycle: tags excluded) |
dueDate |
deadline (normalized to YYYY-MM-DD) |
estimate |
estimate:<n> tag (integer; suppress with --map estimate=ignore) |
cycleId |
cycle:<name> tag (name resolved to the team's cycle id at push time; skipped if unresolved; suppress with --map cycle=ignore) |
Export does not currently push assignee back to Linear (import-direction only).
| Variable | Required | Description |
|---|---|---|
LINEAR_API_KEY |
Yes | Linear personal or workspace API key |
LINEAR_DEFAULT_TEAM |
No | Default team slug for the importer (if not set in config) |
# Install dev deps
npm install
# Build TypeScript → dist/
npm run build
# Watch mode
npm run devThe compiled output lands in dist/index.js, which is the entry point referenced by manifest.json.
MIT
This package is release-ready for GitHub, npm, and Bun-compatible installs. CI runs type checking, build, production dependency audit, package packing, Bun install verification, and pm-changelog validation. The daily release workflow publishes only when commits exist after the latest release tag and uses pm-changelog to generate CHANGELOG.md and GitHub release notes.
This repo tracks its project management in .agents/pm/ and ships a committed .gitattributes
that maps those tracker artifacts to pm-cli's field-aware Git merge drivers, so concurrent-branch
tracker edits merge cleanly instead of hard-conflicting. The driver definitions live in per-clone
Git config; npm install / npm ci wires them automatically via the prepare script, scripts/prepare-merge-driver.ts: the launcher template pm-ops ships, copied unchanged, which a test compares byte for byte with the pinned template. It runs pm-ops's installer, which calls pm merge install when the pm CLI is on PATH and skips with a notice when it is not. A production install of a clone (npm ci --omit=dev) has no pm-ops, so the launcher skips with one notice, while a stale or broken pm-ops fails the install. Registry installs of this package never run prepare. Being Node-based, it behaves identically on POSIX shells and Windows cmd.exe. To (re)run manually: npm run merge:install.
After merging a branch that touched .agents/pm/, reconcile any residual history-hash drift with
pm merge reconcile (pm-cli ≥ 2026.7.22): preview with pm merge reconcile --dry-run, apply with
pm merge reconcile --message "post-merge reconcile", then confirm with pm validate, which scans the
whole tracker and flags remaining history drift across every affected item (pm merge reconcile
itself lists each affected stream in its output; pm history --verify <id> spot-checks one item). The field-aware driver already unions every author's
content, so reconcile only re-greens the hash chain (no data loss) — see the authoritative
pm-cli merge-safety guide. The
older blunt pm history-repair --all remains available as a lower-level primitive.
pm linear closure acquires missing prerequisite context for explicitly selected,
linked pm items. It reads incoming Linear blocks relations, including archived
relations, and follows them within declared budgets. Core pm deps, pm-context
and pm-graph continue to handle relationships already available locally.
pm linear closure --items pm-example --max-depth 3 --max-nodes 32 --max-requests 64 --json
pm linear closure --items pm-one,pm-two --max-relations 128The second form uses the host's default TOON rendering. For larger receipts, set
host --output-budget unbounded --output-limit unbounded to retain every row.
Only Read access is needed in LINEAR_API_KEY. A selection must already carry
the provenance written by pm linear import or sync; unlinked selections are
refused before any HTTP request. There is no full-team query or apply mode.
For selected -> B -> C -> D, with only selected imported, frontier is
[B,C,D], measured is {nodes:4,requests:4,relations:3}, and every issue has
its depth and local item ID (or null). All known missing blockers remain listed,
including identities beyond the node/depth cap. issues[].read distinguishes
acquired context from identities awaiting a successful read.
complete: false preserves boundaries with a reason and remedy for denied
access, unavailable transport, or an exhausted cap. A hidden relation whose
identity is unavailable produces a boundary on its source. Neither complete
acquisition nor an empty frontier establishes readiness to act. Read imported
context and unresolved boundaries before acting. No local or Linear status name
is interpreted or used to hide a prerequisite.
| Flag | Default | Accepted integer range |
|---|---|---|
--max-depth |
3 | 0..8 |
--max-nodes |
32 | 1..128 |
--max-requests |
64 | 1..256 |
--max-relations |
128 | 1..1024 |
--max-output-bytes |
16384 | 1024..131072 |
Select 1..16 distinct items. Node counts include selected roots; relation counts include non-blocking rows examined. Each actual HTTP attempt consumes one request; failures never trigger hidden retries. Pages contain at most 50 rows and transport responses are capped at 256 KiB. JSON receipts reserve 256 bytes for transport wrapping; oversized output is refused rather than truncated.
import { fetchLinearDependencyClosure, LinearClosureRefusal } from "pm-linear";
const report = await fetchLinearDependencyClosure(
{ items: ["pm-example"], maxDepth: 3, maxRequests: 16 }, trackerRoot,
);
// On refusal, LinearClosureRefusal exposes code, remedy and exitCode (2).The SDK and command work without Git and leave the source tracker byte-identical.
Because the pinned SDK writes derived read caches and feedback, certified reads
run only after a bounded physical isolation copy (at most 20,000 entries / 64 MiB),
without an item-folder filter. SDK complete enumeration and locateItem then
select the exact item files for a final read view with its configuration. Even
missing settings/schema files are bootstrapped only in owned copies. Copies are
removed after each read; the source's permissions never change. Membership is re-read before
returning; observed local edits, paging drift, malformed data or ambiguous
provenance produce typed refusals. Provider observations are not an atomic
cross-issue snapshot. See validation and limits.
Linear estimates remain native points. Imports select the issue's immutable
team ID, estimate scale, zero setting and extended-scale setting and retain them
alongside estimate:<points>. They never populate estimated_minutes from points.
An explicit scale-bound lookup can expose minutes in the inspection receipt:
pm linear estimates --team ENG --json
pm linear import --team ENG --estimate-mapping conversion.json
pm linear estimates --team ENG --scale reviewed-team.json --limit 20linear estimates is read-only. Counts cover the complete SDK corpus, while the
row sample defaults to 20 (maximum 100). scale_changed and mapping_stale identify
invalidated conversions; unrelated teams remain unaffected. Missing conversions
return minutes: null and retain usable native points. JSON and default TOON
share the same typed result. See the schema, SDK example and retry guarantees.
Imports skip an item if its planning snapshot disagrees with the SDK revision
observation, preserving peer conversions and native edits. estimateSkips names
the affected items; retry reads their current state while independent items proceed.
Imports retain earlier successes and retry against current SDK state. They never
roll back or implicitly restore deleted identities; reserved/foreign IDs skip with
a remedy. Explicit conversions stay with live items through fresh-input retries.
Estimate inspection, import, sync and export work in ordinary initialized tracker
directories without a git executable. Linear network operations require provider
credentials; linear estimates --scale and export previews work offline.
All linear sync, linear import, linear-sync import and typed SDK imports
apply each item forward-only. An explicit --atomic request refuses with atomic_unavailable (exit 2) before provider access or native writes. To migrate, remove --atomic and explicitly pass --forward-only, or add --forward-only to an existing invocation. In the SDK, use importLinearForwardOnly; SyncOptions.atomic: true and deprecated importLinearAtomic require forwardOnly: true before any work. The default sync/import mode remains forward-only. If item B fails, item A stays applied; importMode
is forward-only, atomic is false, and importResults lists every selected
native target as created, updated, unchanged, skipped or unconfirmed. A conflict has code
import_conflict and a remedy. A skipped prerequisite skips its dependents.
Each write re-reads the SDK history revision paired with its saved base. Changed revisions, including ABA edits, skip the item. Updates submit only changed fields; an unchanged status is omitted, including on completed issues. Imports retain the SDK’s assignee-release effect for the configured primary close/cancel roles, so replaying a completed issue does not reassign it. New items explicitly clear ambient relationship scope. A fresh retry uses current state and converges without replaying a frozen journal. Deleted/reserved identities require explicit native inspection and restore; imports never resurrect them implicitly.
pm linear import --team ENG --json
# Inspect importResults; rerun after resolving an import_conflict.
pm linear sync --team ENG --jsonThe SDK has no item precondition (#1576)
and its workspace transaction does not exclude ordinary writers
(#1620). A window remains between the
last SDK read and the mutation. Native completed transitions can internally write
fields and close separately; their exact one-item plan is validated with the SDK
in an isolated copy first. This does not guarantee isolation from a peer or an
I/O/policy failure inside the final native call. Inspect a failed target before
retrying; the importer never rolls back successful writes or peer state. Failed native calls re-read through the SDK: a complete current target is unchanged/import_reconciled without attributing a new write, an unchanged target is skipped, and a changed/incomplete target is unconfirmed with import_incomplete. Its dependents skip.
Failed, conflicted or unconfirmed rows set success: false and nonzero exit_code on SDK receipts and every JSON/TOON command/import alias. Filters and complete readback reconciliation remain successful. Duplicate native provenance or duplicate selected provider IDs select no target, report import_conflict, and write nothing for those rows.
Prepared SDK rows must carry the same provider identity in linearId and their provenance marker. Missing or inconsistent prepared provenance returns an explicit conflict before target selection. Existing native free-text descriptions remain usable for reads and exports.
Export previews preserve duplicate native records as conflict-marked payloads. Mutation plans and pushes omit ambiguous provider targets and report exportConflicts with a nonzero exit, while independent rows can proceed.
Import dry-run and native-close validation share a separate bounded mirror policy: 65,536 source entries and the same 64 MiB byte limit. This admits legitimate item/history pairs beyond 10,000 native items. Observational ownership and closure commands retain their 20,000-entry limit. Every mirror rejects unsupported source symlinks and changes only owned copies.