Conversation
…non-major) chore(deps): update dev dependencies (non-major)
…ncies-(non-major) fix(deps): update dependency graphql to v16.14.2
## [2026.7.0-next.1](v2026.6.0...v2026.7.0-next.1) (2026-07-06)
fix(deps): update dependency commander to v15
## [2026.7.0-next.2](v2026.7.0-next.1...v2026.7.0-next.2) (2026-07-06)
✅ knip — no dead codeNo unused files, exports, types, or dependencies detected. |
The README claimed "broad domain coverage" and mentioned a coverage
trade-off in passing, but gave no way to tell whether a specific
operation was supported. Users and agents had to discover gaps by
running a command and getting nothing back.
Diffed the Linear GraphQL schema (164 queries, 373 mutations) against
the root fields referenced in graphql/{queries,mutations} and against
the commands registered in src/commands, then generalised the result to
domain level. The "### Domains" table is replaced by a "## Coverage"
section holding a single table over the entire surface: each area
carries an extent rating (complete / core-with-gaps / narrow slice /
no CLI surface) alongside what it supports and what it does not.
Two deliberate choices:
- One table, not two. Splitting covered from uncovered buried the
uncovered half below the fold and let a reader mistake the first
table for the whole surface. Rating uncovered areas red in the same
table makes the shape of the gap legible at a glance.
- Domain granularity, not per-operation. A raw root-field checklist
would be 537 rows, would understate coverage (several commands read
via nested fields rather than root queries), and would need updating
on every schema refresh. The domain view answers the question a user
actually has — "can I do X with this?" — and stays accurate longer.
Every count in the section is exact rather than rounded, since a
rounded headline next to exact sub-counts reads as an arithmetic error.
The integrations row counts root fields whose name contains
"integration" against .context/linear-schema.graphql: 65 mutations and
8 queries, so 73 root fields. That boundary is a judgement call — a
looser match that also swept in attachmentLink* and issueImport* would
give 83, but those fields are already accounted for in the attachments
and imports rows, so counting them here would double-count. The
name-match definition keeps the rows disjoint.
Rows state verb lists rather than CRUD shorthand where the shorthand
would over-claim: initiative updates have no delete mutation in the
API, and project labels are readable (`labels list --type project`) but
not creatable from the CLI, while project label *assignment* does work
via `projects create/update --labels`.
Docs-only change; the build/test checklist does not apply.
…l-coverage-table docs(readme): add GraphQL coverage matrix
chore(deps): update dependency typescript to v7
chore(deps): update actions/setup-node action to v7
chore(deps): update github actions
`issues update` could set an assignee or a project but never remove one. Both fields were assembled behind a truthiness guard (`if (ids.assigneeId)`), so no `assigneeId`/`projectId` key ever reached `issueUpdate` when the user wanted them gone, and there was no CLI spelling that produced one. For the agent-orchestration use case in #282 — assignee as a soft ownership lock — a lock you can take but never release is not a lock. Add `--clear-assignee` and `--clear-project`, each sending an explicit `null` to the Linear API. This follows the convention already established by the six existing clear flags on this command (`--clear-labels`, `--clear-due-date`, `--clear-estimate`, `--clear-cycle`, `--clear-parent-ticket`, `--clear-project-milestone`): reject the setter and the clearer together, skip ID resolution when clearing so no pointless resolver round-trip happens, and assemble the input with if/else-if. Because a project milestone belongs to a project, `--clear-project` nulls `projectMilestoneId` in the same mutation and rejects `--project-milestone` up front. Detaching the project while keeping the milestone would leave the issue pointing at a milestone of a project it no longer belongs to, and that state is silent — the issue looks fine until someone reads the milestone. Requiring the caller to pass `--clear-project --clear-project-milestone` was considered and rejected: refusing the contradictory combination at parse time while allowing it to arise implicitly is inconsistent. Moving an issue to a *different* project with `--project` is left alone; the new milestone is the caller's to pick. The alternative considered was a sentinel value such as `--assignee ""`. It was rejected: it is ambiguous against a legitimately empty argument, invisible in `--help`, and inconsistent with the existing clear flags. No service or GraphQL change was needed — `UpdateIssueInput` picks these fields from the codegen `IssueUpdateInput`, where they are already nullable. Closes #282
`initiatives update --owner` had the same write-only shape as the issue assignee. The owner could be set but never removed, because `ownerId` was only ever written into the update input when `--owner` was present. Add `--clear-owner`, which writes an explicit `null` into `UpdateInitiativeInput`. The resolver call is skipped when clearing, so no user lookup is performed for a value that is being discarded, and passing `--owner` together with `--clear-owner` is rejected up front. The error uses `invalidParameterError` rather than the plain `Error` that `issues.ts` throws for the same class of conflict. This file and `projects.ts` consistently use the former, so the local convention wins over cross-file uniformity. `--clear-owner` on its own satisfies the existing "at least one option must be provided" guard, since it writes a key into the input object. Refs #282
…ress-issue feat(issues): add --clear-assignee, --clear-project, and --clear-owner flags
## [2026.7.0-next.3](v2026.7.0-next.2...v2026.7.0-next.3) (2026-08-06)
Linearis promises JSON on every non-help path and uses a distinct exit code (42) to make authentication failures programmatically detectable. Argument-parse failures honoured neither: Commander wrote plain text to stderr and exited 1 — the same code a legitimate "entity not found" uses. An agent could not tell "I called the CLI wrong" from "the issue does not exist", and the plain-text message carried no recovery path. Root cause of the worst case: the root command and every domain register an action handler (the overview / domain help) while declaring no arguments. With that combination Commander routes an unrecognised operand to `_excessArguments()` rather than `unknownCommand()`, so `linearis issues get ABC-123` reported "too many arguments for 'issues'" instead of naming the unknown subcommand, and `.showSuggestionAfterError()` could never fire. `interceptParseErrors()` walks the finished command tree and installs a per-command `exitOverride`, so each callback closes over the command that failed. That yields the exact failing scope without re-parsing argv — the alternative would need option-arity heuristics to tell an option value from a subcommand name. `Command.error()` writes its message to stderr *before* calling `_exit`, so `writeErr` is suppressed too; the same text is still available on `CommanderError.message` and is reused verbatim (minus the `error: ` prefix) for every code except UNKNOWN_COMMAND, which is rephrased to name the token. Two codes need their own phrasing rather than that fallback. UNKNOWN_COMMAND names the offending token. And `commander.help` — raised with exit code 1, so it does not take the exit-0 passthrough, whenever a command has subcommands, no action handler and no operand to dispatch on (`issues threads`, `projects threads`, `initiatives threads`, all built by `addCommentReactionCommands`) — carries Commander's internal `(outputHelp)` placeholder as its message, which means nothing to a caller. It becomes `Missing subcommand for "<path>"` routed to the "list valid subcommands" instruction, since `available_commands` is populated for those groups. Exit code 2 was chosen for malformed invocation: it is the conventional Unix usage-error code, and it keeps 1 meaning "the request was well-formed but could not be fulfilled". `available_commands` is read from `cmd.commands`, the same source `formatDomainUsage` uses, so it cannot drift from USAGE.md, and `instruction` resolves to the nearest ancestor owning a `usage` subcommand. Help, version, and bare `linearis` / `linearis <domain>` throw a CommanderError with exit code 0 after writing to stdout; those pass through untouched, so all existing successful output is byte-identical. Also fixes an unreported defect on the same line: `program.parseAsync()` was neither awaited nor caught, so anything rejecting during parse surfaced as a raw Node unhandled-rejection stack trace. `handleParseFailure` is now the terminal handler for that promise. Closes #281
Nothing documented the exit codes, which made the newly distinct code 2 (and the existing 42) undiscoverable for the agents that are the main consumers of this CLI. Adds an "Exit codes" section to the README covering all four codes and the usage-error envelope, and extends the development docs "Output Format" block with outputUsageError, outputAuthError, and a pointer to cli-errors.ts. Refs #281
With `showSuggestionAfterError` — Commander's default — a near miss is appended
to the message as a parenthesised second line, and `describeUsageError` reused
that string verbatim:
$ linearis issues list --limt 5
{ "message": "unknown option '--limt'\n(Did you mean --limit?)", ... }
An embedded newline in a machine-readable field forces every consumer to know
Commander's formatting to get either half back out, and the envelope's own
`instruction` field already occupies the "here is what to do next" slot.
Split the hint onto its own optional `suggestion` key, unwrapped from its
parentheses, leaving `message` a single line. The split runs before the
per-code message rewrite rather than after, so the hint survives for
UNKNOWN_COMMAND too — an unrecognised subcommand is precisely the case
Commander can suggest a near miss for, and that is where it helps most.
…nvelope
Naming a group without a subcommand had two contradictory contracts, decided by
whether that group happened to be registered with `.action(() => group.help())`:
$ linearis issues threads # exit 2, INVALID_USAGE envelope on stderr
$ linearis issues relations # exit 0, Commander help text on stdout
Same class of invocation, opposite results. The exit-0 half is the worse one for
this CLI's primary consumer: an agent gets human help text on stdout where it
expects JSON, and an exit code that says the call succeeded when nothing ran.
It is also redundant — `linearis issues usage` is the machine-readable reference
and `linearis issues --help` the human one, both still exit 0.
So the fourteen `group.action(() => group.help())` registrations are dropped and
every group converges on the envelope, which names the scope and lists
`available_commands`. The root keeps its action handler: `linearis` alone prints
the overview, which is a real result rather than an incomplete invocation.
That leaves the root as the only command pairing an action handler with no
declared arguments, so it is now the sole caller of the excessArguments to
UNKNOWN_COMMAND remapping; domains reach `unknownCommand()` directly, which has
the side benefit that Commander's near-miss hint fires for subcommands
(`linearis issues lst` suggests "Did you mean list?").
The shared outcome also gets its own code rather than reusing INVALID_USAGE.
The earlier reasoning for reuse was that the published enum should not grow, but
this branch is what publishes it, so adding `MISSING_SUBCOMMAND` is free now and
a breaking change later. It is a distinct condition: the invocation is not
malformed, it is incomplete, and the recovery is to pick from
`available_commands` rather than to fix a token. INVALID_USAGE stays as the
genuine catch-all for unclassified Commander failures.
`output.ts` imported the payload type from `cli-errors.ts`, which imports `outputUsageError` back from `output.ts`. The cycle is type-only today, so it erases at build time and nothing observes it — but it is one value import in `output.ts` away from becoming a real module cycle, and the next person to add one gets to discover that. `errors.ts` already owns `USAGE_ERROR_CODE`, the constant the payload's `exit_code` carries, and imports neither module. Moving the interface and its code union there leaves `cli-errors.ts` (classify) and `output.ts` (write) both depending on `errors.ts` and no longer on each other's types. `UsageErrorCode` stays private to its module, re-derived in `cli-errors.ts` as `UsageErrorPayload["error"]` so the classifier's return type cannot drift from the field it populates.
`handleParseFailure` special-cased `AuthenticationError` to preserve exit code 42, but nothing can reach it. The handler only sees rejections from `program.parseAsync()`, and every action handler is wrapped by `handleCommand()`, which maps that error itself; `auth login` deliberately bypasses the wrapper and catches internally. The only other code that runs during parse is Commander's option parsers, none of which touch auth. It was the one branch of the function with no test — for the good reason that there is no way to exercise it through the public entry point. Testing it would mean calling `handleParseFailure` with an error the CLI cannot produce, which asserts the branch exists rather than that it works. Removed, with the fallthrough to `outputError` documented instead: anything unexpected still leaves as JSON, just with the generic exit code 1.
Every "you need to authenticate" message told the caller to run `linearis auth`. That worked while the group had an action handler that printed its own help, but 35e690d dropped those handlers so a bare group is now a MISSING_SUBCOMMAND usage error. The exit-42 envelope's `instruction` therefore handed back a command that exits 2 — the recovery path dead-ended in a second error, which is worse for an agent following the envelope literally than for a human who would just read the help. All three sites now name `linearis auth login`, the command that actually runs the interactive flow, and the skill doc quotes the new wording so agents match on the same string. Refs #281
`commander.missingMandatoryOptionValue` and `commander.optionMissingArgument` were not in `classify()`, so they fell through to INVALID_USAGE — the code reserved for failures the CLI could not identify. Nine commands declare a `requiredOption`, which makes "forgot a required flag" one of the most likely ways to call this CLI wrong; `linearis attachments create --url x` reported INVALID_USAGE where a caller has every right to expect a specific code. Both now get their own code. They are kept apart rather than merged because the recoveries differ: MISSING_REQUIRED_OPTION means a flag is absent, MISSING_OPTION_ARGUMENT means it was passed with nothing after it — an agent retrying automatically needs to know which. Commander's own message already names the offending flag in both cases, so no rewriting is needed. Timing matters more than the size of the change: the usage envelope is new on this branch and unreleased, so widening the code union costs nothing now and would be a breaking contract change once published. Refs #281
Dropping the per-group `.action(() => group.help())` handlers in 35e690d made every command group match the condition under which Commander adds its implicit `help` subcommand: subcommands present, no action handler. So `linearis issues help` and `linearis issues threads help` started printing human-readable help on stdout and exiting 0 — the precise contract MISSING_SUBCOMMAND was introduced to remove, an agent getting help text where it expects JSON and a success code for a call that ran nothing. It was also inconsistent: `linearis help` and `linearis version help` still returned UNKNOWN_COMMAND, because the root and `version` kept their action handlers, and `help` never appeared in `available_commands`. Disable it in the `interceptParseErrors` walk, which already visits every command after registration and is where the rest of the JSON-contract enforcement lives. `help` is now an UNKNOWN_COMMAND like any other unrecognised subcommand. The `--help` option is separate and unaffected, so the human path is still one flag away. Refs #281
`linearis issues threads` fails with MISSING_SUBCOMMAND and an instruction to run `linearis issues usage` — but that output named `threads` as a single line and never listed `react`, `unreact`, or `unreact-id`. The caller was sent to a reference that could not answer the question it was sent to answer, the same dead end 14ab072 fixed for the bare `auth` group. `issues relations`, `issues replies`, and `initiatives updates` had it too. Walk the command tree depth-first instead of one level, rendering each command under the path it is actually invoked by (`threads react <thread> [emoji]`) and keying its options section the same way (`threads react options:`), which also disambiguates the several `react`/`unreact` leaves that now coexist in one listing. Groups keep their own line as the heading their children hang off. The alternative was to reword the envelope's instruction to lean on `available_commands` instead. That treats the symptom: SKILL.md tells agents `usage` is authoritative and always current, so the fix belongs in the output that makes that claim. Refs #281
The agent-facing skill taught only one machine-readable failure contract, exit 42 for authentication, even though this branch added a second one — exit 2 with a usage envelope carrying `available_commands` and an `instruction`. README and docs/development.md both describe it, but an agent driving the CLI reads SKILL.md, and without it a bare group or a mistyped flag looks like an ordinary failure to paraphrase rather than a structured recovery path to follow. Kept to the same reactive shape as the auth entry, branching on the envelope the CLI already emitted rather than pre-checking. Refs #281
The skill now documents the exit-code-2 usage-error envelope (UNKNOWN_COMMAND, MISSING_SUBCOMMAND, and the rest of the family) and corrects the auth recovery hint to 'linearis auth login'. Consumers pin and update the plugin by the version in the marketplace manifest, so a skill change that alters how an agent recovers from failures has to be published under a new version or existing installs never pick it up. Minor rather than patch: the skill gained a new documented behaviour contract, additively, with no instruction removed. The marketplace metadata version moves in lockstep with the single plugin it lists — keeping them equal is cheaper to reason about than tracking two independent counters for a one-plugin catalogue. Also extends the plugin description to mention error envelopes and exit codes, since that is now a substantive part of what the skill teaches and the description is what the marketplace listing shows. Refs #281
…ue-triage fix(cli): emit JSON envelope for argument-parse errors
## [2026.7.0-next.4](v2026.7.0-next.3...v2026.7.0-next.4) (2026-08-06)
The Node 22 leg of the unit-test matrix fails `npm ci` with EUSAGE whenever
the lockfile is refreshed:
npm error Missing: conventional-commits-filter@6.0.1 from lock file
npm error Missing: conventional-commits-parser@7.1.2 from lock file
npm error Missing: argue-cli@3.1.0 from lock file
The cause is the npm version, not the Node version. Node 22 bundles npm 10.9,
Node 24 bundles npm 11, and package-lock.json is written by npm 11 (the project
default and Renovate). The two majors disagree on optional peerDependencies:
`@conventional-changelog/git-client` 3.x declares optional peers on
conventional-commits-filter ^6.0.1 and conventional-commits-parser ^7.0.1, npm
11 omits them from the tree, and npm 10 insists on materializing them (plus
argue-cli, a transitive dep of the parser) — then aborts because they are not
in the lockfile.
Install npm 11 in the shared setup action so the installer is identical on
every Node version. The matrix keeps testing Node 22 at runtime, which is what
the engines floor is about; only the package manager is normalized.
Alternatives considered: regenerating the lockfile under npm 10 makes CI pass
once but regresses on the next npm 11 `npm install`, and dropping Node 22 from
the matrix would give up engines-floor coverage. `engines.npm` was deliberately
not added to package.json — it would emit EBADENGINE warnings for consumers
installing the published package with npm 10.
Refs #277
…aining
Biome 2.5.6 flags `(ownProto?.value as { x: number }).x` with
lint/correctness/noUnsafeOptionalChaining: if the descriptor lookup returned
undefined the chain short-circuits and the member access throws a TypeError
instead of failing the assertion.
Assert on the descriptor value directly. Coverage is unchanged — a missing
descriptor yields undefined, which still fails toEqual — and the cast goes
away with it.
Refs #277
The dev-dependency bump takes Biome from 2.5.2 to 2.5.6, which changes how long `it.each([...])(name, callback)` calls are broken across lines and makes `check:ci` fail on cli-errors.test.ts. Apply the new formatter output; the change is mechanical, no assertion is touched. Also run `biome migrate`: the `$schema` URL was still pinned at 2.4.11 (nothing updates it when Renovate bumps the package), and `linter.rules.recommended` is deprecated in favour of `linter.rules.preset`. Both were emitted as infos by `biome check`; migrating now keeps the config in step with the CLI that the lockfile installs. The useLiteralKeys and noNonNullAssertion overrides are unchanged. Refs #277
…non-major) chore(deps): update dev dependencies (non-major)
fix(deps): update dependency graphql to v17
## [2026.7.0-next.5](v2026.7.0-next.4...v2026.7.0-next.5) (2026-08-06)
Linear's complexity estimator charges an unbounded connection at its default page size (50) per parent row. ProjectListFields selected unbounded teams and labels connections, pricing the default `projects list` (first: 100) at ~13950 against a budget of 10000 — so default-limit lists failed with "Query too complex" even on near-empty workspaces, since the estimator prices page size, not data. - bound teams/labels (25) in ProjectListFields, members/initiatives (25) in ProjectDetailFields, and projectMilestones (25) in the default-connections fragment; the same list query now prices ~7450 - lower the projects read issues default from 50 to 25: each issue in the response costs ~260 (CompleteIssueFields carries four unbounded connections), so the old default read exceeded the budget on real workspaces - replace the update --label-mode add|remove pre-read (full project detail including milestones and issues) with a lean label-IDs-only query Closes #276 Closes #283
… in tests Review follow-ups for #284: - assert in tests that every bounded fragment connection carries a literal first: argument, so a future tidy-up that unbounds them fails CI instead of reintroducing #276 - select pageInfo.hasNextPage on the five bounded connections in the detail fragment so truncated pages are detectable in read and mutation responses; measured live, pageInfo prices at ~23 complexity per parent row, so the signal lives only where the parent count is one — selecting it in ProjectListFields put the default list back over budget (12120) and is deliberately omitted there - getProjectLabelIds now selects hasNextPage on its first: 250 read (Linear's per-connection maximum) and throws on truncation instead of letting a partial label set be written back as complete via the full-replacement labelIds input Refs #284
fix(projects): bound project query connections to avoid complexity limit
## [2026.7.0-next.6](v2026.7.0-next.5...v2026.7.0-next.6) (2026-08-07)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose
Promote tested prerelease changes from
nextinto stable channel onmain.Target stable version
2026.7.0Ships in stable (releasable changes)
Also included (no version impact)
Maintenance commits without associated PR
Compare
main...next
Install release candidate
Note
Please test prerelease changes in this PR and share feedback directly on this PR so we can address issues before stable release. Mention your installed version (
linearis --version) and repro steps. Use@nextfor rolling prerelease train, or exact version below for this staged candidate.Notes
next.