Skip to content

release 2026.7.0 to stable channel - #275

Merged
iamfj merged 46 commits into
mainfrom
next
Aug 7, 2026
Merged

release 2026.7.0 to stable channel#275
iamfj merged 46 commits into
mainfrom
next

Conversation

@linearis-bot

@linearis-bot linearis-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

Purpose

Promote tested prerelease changes from next into stable channel on main.

Target stable version

2026.7.0

Ships in stable (releasable changes)

  • #272 fix(deps): update dependency graphql to v16.14.2 by @renovate
  • #273 fix(deps): update dependency commander to v15 by @renovate
  • #274 fix(deps): update dependency graphql to v17 by @renovate
  • #284 fix(projects): bound project query connections to avoid complexity limit by @jackhutson
  • #287 feat(issues): add --clear-assignee, --clear-project, and --clear-owner flags by @iamfj
  • #288 fix(cli): emit JSON envelope for argument-parse errors by @iamfj

Also included (no version impact)

  • #271 chore(deps): update dev dependencies (non-major) by @renovate
  • #277 chore(deps): update dev dependencies (non-major) by @renovate
  • #278 chore(deps): update github actions by @renovate
  • #279 chore(deps): update actions/setup-node action to v7 by @renovate
  • #280 chore(deps): update dependency typescript to v7 by @renovate
  • #285 docs(readme): add GraphQL coverage matrix by @iamfj

Maintenance commits without associated PR

  • chore(release): 2026.7.0-next.6 [skip ci] (9c942b3)
  • chore(release): 2026.7.0-next.5 [skip ci] (f4b13f7)
  • chore(release): 2026.7.0-next.4 [skip ci] (881ddf4)
  • chore(release): 2026.7.0-next.3 [skip ci] (b9310a0)
  • chore(release): 2026.7.0-next.2 [skip ci] (cc2a817)
  • chore(release): 2026.7.0-next.1 [skip ci] (0375ef9)

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 @next for rolling prerelease train, or exact version below for this staged candidate.

npm install -g linearis@next
npm install -g linearis@2026.7.0-next.6

Notes

  • This PR is auto-maintained when a prerelease is published from next.
  • Merge this PR to promote current prerelease train to stable.
  • Releasable classification mirrors semantic-release policy (feat/fix/perf/revert plus breaking changes, excluding ci/release/workflow scopes).

@linearis-bot
linearis-bot Bot requested a review from iamfj as a code owner July 6, 2026 08:02
@iamfj
iamfj temporarily deployed to npm-publish July 6, 2026 09:00 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

✅ knip — no dead code

No unused files, exports, types, or dependencies detected.

iamfj added 2 commits August 6, 2026 10:08
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
iamfj added 3 commits August 6, 2026 12:36
`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
semantic-release-bot and others added 3 commits August 6, 2026 10:39
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
iamfj added 11 commits August 6, 2026 12:43
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
semantic-release-bot and others added 6 commits August 6, 2026 10:51
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)
semantic-release-bot and others added 4 commits August 6, 2026 11:23
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
@iamfj
iamfj merged commit 66533ec into main Aug 7, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants