Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
47b509e
feat(projects)!: drop archive in favour of delete
iamfj Aug 10, 2026
0357b2d
refactor(projects): split the command module by subgroup
iamfj Aug 10, 2026
c66da2b
feat(projects): post and manage project status updates
iamfj Aug 10, 2026
a6e1a16
feat(projects): administer the workspace project status flow
iamfj Aug 10, 2026
4bf8dcc
feat(labels): full CRUD and retire/restore for project labels
iamfj Aug 10, 2026
de351cf
feat(projects): full-text search and external sync disable
iamfj Aug 10, 2026
fcb959f
refactor(projects): apply label modes incrementally
iamfj Aug 10, 2026
4e81324
feat(projects): chronological project activity timeline
iamfj Aug 10, 2026
bdd3de0
feat(projects): manage project dependency relations
iamfj Aug 10, 2026
2e9440c
feat(projects): read one dependency and page them workspace-wide
iamfj Aug 10, 2026
6f360c7
docs: mark the projects coverage row complete
iamfj Aug 10, 2026
9f5e87a
fix(projects): anchor relation updates to the right end
iamfj Aug 10, 2026
0cd58e6
refactor(activity): share the timeline merge between activity commands
iamfj Aug 10, 2026
1e3b428
fix(projects): count archived statuses when appending
iamfj Aug 10, 2026
b143d2b
fix(milestones)!: keep a milestone lookup inside its project
iamfj Aug 10, 2026
6833231
docs: correct the resolver entries in the file catalog
iamfj Aug 10, 2026
b3886e4
fix(projects): report the relation page bound instead of a miss
iamfj Aug 10, 2026
5824b02
fix(projects): scope relation milestones on the UUID path
iamfj Aug 10, 2026
9810d79
refactor(common): pin update health to the codegen enums
iamfj Aug 10, 2026
8bdb998
docs: correct the project-label teamId rationale
iamfj Aug 10, 2026
59d5890
feat(labels): make the group and parent flags reversible
iamfj Aug 10, 2026
ed7cc89
fix(projects): reject pagination flags on per-project relations
iamfj Aug 10, 2026
30664cd
fix(projects): say where the projects went when archiving fails
iamfj Aug 10, 2026
9655f4f
docs: split the run-on search comment in the projects query
iamfj Aug 10, 2026
fe3508c
fix(projects): refuse to guess which relation a project pair means
iamfj Aug 10, 2026
d5eb26d
fix(projects): refuse a lone relation match past the page bound
iamfj Aug 10, 2026
1be4ab1
fix(projects): refuse to guess between same-named project statuses
iamfj Aug 10, 2026
d99ee89
fix(labels): scope --parent to the same team as the label
iamfj Aug 10, 2026
d94e51e
fix(projects): reject an empty relation update before resolving it
iamfj Aug 10, 2026
9ebdef7
fix(projects): bound and page the project status flow
iamfj Aug 10, 2026
1fdc5e1
refactor(projects): state the relation page bound once
iamfj Aug 10, 2026
e01ca0e
fix(projects): reject a malformed status position
iamfj Aug 10, 2026
fd13029
fix(labels): reject an empty --parent on update
iamfj Aug 10, 2026
fa994e8
docs(projects): drop the stale milestone fallback comment
iamfj Aug 10, 2026
d1287f6
test(projects): keep the rejection assertions inside the type check
iamfj Aug 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,9 @@ A schema is the input contract only: it cannot know your team's workflow states,

## Coverage

Linear's GraphQL API exposes **537 root operations** (164 queries, 373 mutations). Linearis wires **83 of them** directly, plus a number of nested reads — chosen to cover planning and issue work end to end rather than the whole API.
Linear's GraphQL API exposes **520 root operations** (159 queries, 361 mutations). Linearis wires **114 of them** directly, plus a number of nested reads — chosen to cover planning and issue work end to end rather than the whole API.

Both figures are checkable rather than asserted: `npm run count:root-fields -- --verify` parses `graphql/**/*.graphql` and cross-checks the result against a live introspection of the schema.

The table below is the honest picture of the whole surface — what works today, and what you'll need the [Linear MCP](#linearis-vs-linear-mcp) or a raw API call for.

Expand All @@ -154,26 +156,26 @@ The table below is the honest picture of the whole surface — what works today,
| Area | Extent | What you can do | Not covered |
|---|---|---|---|
| `auth` | ✅ | Interactive login, token status, logout | — |
| Discussions | ✅ | Root threads and replies on issues, projects, and initiatives; edit, delete, resolve/unresolve; emoji reactions on any of them | Custom workspace emoji management |
| Discussions | ✅ | Root threads and replies on issues, projects, and initiatives; edit, delete, resolve/unresolve; emoji reactions on any of them | Custom workspace emoji management, and comment threads on a status update — `CommentCreateInput.projectUpdateId` makes an update its own discussion surface, which neither `projects updates` nor `initiatives updates` exposes |
| `issues` | ✅ | List, filter, full-text search, read, create, update, batch create/update, archive/unarchive, delete/restore, snooze; assign labels/assignee/delegate/state/priority/project/cycle/team (including moves between teams); subscribe/unsubscribe, share/unshare, reminders; find the issue for a git branch (`from-branch`); relations (list/add/remove); activity history | Deliberately excluded: the AI-assist and integration-suggestion queries (Figma file lookup, filter/repository suggestions, title-from-customer-request) — see the Integrations row — and `issuePriorityValues`, a static list already in the help text |
| `initiatives` | 🟡 | List, read, create, update, archive/unarchive, delete; attach/detach projects; initiative-to-initiative relations; initiative updates (list, read, create, update, archive/unarchive); discussions | Initiative labels, lead-team reassignment, relation reordering |
| `projects` | 🟡 | List, read, create, update, archive/unarchive, delete; assign project labels by name (`--labels`, `--label-mode`, `--clear-labels`); discussions | Project updates (status posts), project-label CRUD, project relations, project status administration, Slack channel creation |
| `projects` | | List, full-text search, read, create, update, delete (trash) and unarchive (restore), disable external sync; assign project labels by name (`--labels`, `--label-mode`, `--clear-labels`); status updates (list, read, create, edit, archive/unarchive, remind); dependency relations (list, read, add, update, remove); administer the workspace project status flow (`projects statuses`); discussions; activity timeline | Deliberately excluded: Slack-channel creation and the AI filter suggestion, which are `[Internal]` integration plumbing — see the Integrations row. Project labels live under `labels --type project`; milestone delete/move under `milestones` |
| `documents` | 🟡 | List, read, create, update, delete | Content history, document full-text search, unarchive |
| `milestones` | 🟡 | List, read, create, update (per project) | Delete, reordering/move between projects |
| `attachments` | 🟡 | List on an issue, create from a URL, delete, disable external sync | Update, and the provider-specific link mutations (GitHub PR/issue, GitLab MR, Slack, Jira, Zendesk, Intercom, Front, Salesforce, Discord) |
| `files` | 🟡 | Upload a file, download via signed URL | Delete uploads, image-from-URL, CSV export reports |
| `teams` | 🟡 | List, read, create, update; list/add/remove members | Delete, workflow-state administration, triage responsibility, git automation, SLA configuration |
| `labels` | 🟠 | Issue labels: list, read, create, update, delete; project labels: list (`--type project`) | Project-label create/update/delete, initiative labels, retire/restore |
| `labels` | 🟡 | Issue and project labels alike (`--type issue\|project`): list, read, create, update, delete, retire/restore; label groups (`--group`, `--parent`) | Initiative labels |
| `cycles` | 🟠 | List cycles, read a cycle with its issues | Create, update, archive, shift all, start upcoming cycle |
| `users` | 🟠 | List workspace members | Read a single user, update, role changes, suspend/unsuspend, user settings, session management |
| Integrations | 🔴 | — | All 73 integration root fields (65 mutations, 8 queries): Slack, GitHub, GitLab, Jira, Figma, Sentry, PagerDuty, Intercom, Salesforce, and more |
| Integrations | 🔴 | — | All 70 integration root fields (62 mutations, 8 queries): Slack, GitHub, GitLab, Jira, Figma, Sentry, PagerDuty, Intercom, Salesforce, and more. Also the `[Internal]` per-entity integration plumbing excluded from the `issues` and `projects` rows: Slack channel creation and dismissal on a project, Slack/Teams/Jira project posts, and the AI-assist suggestion queries (issue and project filter suggestions, Figma file lookup, title-from-customer-request) |
| Organization & admin | 🔴 | — | Org settings, invites, domains, webhooks, OAuth apps, audit log, SSO |
| Releases | 🔴 | — | Releases, release pipelines, stages, release notes |
| Customers (CRM) | 🔴 | — | Customers, needs, tiers, customer statuses |
| Views & templates | 🔴 | — | Custom views, favorites, templates, view preferences |
| Notifications | 🔴 | — | Inbox, subscriptions, snooze, mark read, push subscriptions |
| Agent sessions | 🔴 | — | Agent sessions, activities, skills, semantic search |
| Roadmaps | 🔴 | — | Roadmaps and roadmap-to-project links |
| Roadmaps | 🔴 | — | Roadmaps and roadmap-to-project links. `roadmapToProject*` is deprecated in favour of `initiativeToProject*`, which `initiatives` already wires |
| Imports & exports | 🔴 | — | Jira/Asana/Clubhouse/GitHub/CSV import jobs |
| Schedules | 🔴 | — | Time schedules and on-call rotations |

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ Shared utilities used across layers.
- **src/commands/cycles.ts** - Cycle listing and reading
- **src/commands/teams.ts** - Team listing
- **src/commands/users.ts** - User listing
- **src/commands/projects.ts** - Project listing
- **src/commands/projects/** - Project commands (`index.ts` registers the domain, `entity.ts` holds CRUD and discussions)
- **src/commands/labels.ts** - Label listing
- **src/commands/comments.ts** - Comment creation
- **src/commands/embeds.ts** - File operations
Expand Down
16 changes: 11 additions & 5 deletions docs/files.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ Each resolver converts a human-friendly identifier (name, key, or slug) into a U
- **cycle-resolver.ts** -- `resolveCycleId(client, nameOrId, teamFilter?)`
- **status-resolver.ts** -- `resolveStatusId(client, nameOrId, teamId?)`
- **issue-resolver.ts** -- `resolveIssueId(client, issueIdOrIdentifier)`
- **milestone-resolver.ts** -- `resolveMilestoneId(gqlClient, sdkClient, nameOrId, projectNameOrId?)`
- **milestone-resolver.ts** -- `resolveMilestoneId(gqlClient, nameOrId, projectNameOrId?)` — a project scope is authoritative, never widened to the workspace
- **project-status-resolver.ts** -- `resolveProjectStatusId(client, nameOrId, { includeArchived? })`
- **project-relation-resolver.ts** -- `resolveProjectRelation(client, relationOrProjectId, relatedProjectId?)` — returns `{ id, inverted }`; `inverted` tells callers writing per-end fields to swap the two ends

## Service Layer (`src/services/`)

Expand All @@ -35,8 +37,12 @@ Business logic and CRUD operations. Services use `GraphQLClient` exclusively and
- **cycle-service.ts** -- `listCycles`, `getCycle`
- **team-service.ts** -- `listTeams`
- **user-service.ts** -- `listUsers`
- **project-service.ts** -- `listProjects`
- **label-service.ts** -- `listLabels`
- **project-service.ts** -- `listProjects`, `searchProjects`, `getProject`, `createProject`, `updateProject`, `applyProjectLabels`, `disableProjectExternalSync`, `unarchiveProject`, `deleteProject`
- **project-update-service.ts** -- Project status posts: `listProjectUpdates`, `getProjectUpdate`, `createProjectUpdate`, `editProjectUpdate`, archive/unarchive, `remindProjectUpdate`
- **project-status-service.ts** -- The workspace project status flow: list/get/create/update, `reassignProjectStatus`, archive/unarchive
- **project-relation-service.ts** -- Project dependencies: list (per project and workspace-wide), get, create, update, delete
- **project-activity-service.ts** -- Merges project discussions, history and status updates into one chronological timeline
- **label-service.ts** -- `listLabels`, `listProjectLabels`, and get/create/update/delete/retire/restore dispatching on `LabelType`
- **comment-service.ts** -- `createComment`
- **file-service.ts** -- File upload and download operations for Linear uploads

Expand All @@ -52,8 +58,8 @@ CLI orchestration. Each file registers a command group via a `setup*Commands(pro
- **cycles.ts** -- Cycle listing and detail reading
- **teams.ts** -- Team listing
- **users.ts** -- User listing
- **projects.ts** -- Project listing
- **labels.ts** -- Label listing
- **projects/** -- Project commands. `index.ts` owns `PROJECTS_META` and registers the domain; `entity.ts` holds CRUD, search, sync and discussions; `updates.ts`, `statuses.ts` and `relations.ts` hold the subgroups
- **labels.ts** -- Label commands for both issue and project labels (`--type`)
- **comments.ts** -- Comment creation
- **embeds.ts** -- File download from Linear upload URLs

Expand Down
70 changes: 69 additions & 1 deletion graphql/mutations/labels.graphql
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# ------------------------------------------------------------
# GraphQL mutations for Linear issue labels
# GraphQL mutations for Linear issue and project labels
#
# The two label kinds are separate types with parallel mutations;
# `labels --type issue|project` picks between them.
# ------------------------------------------------------------

mutation CreateIssueLabel($input: IssueLabelCreateInput!) {
Expand All @@ -26,3 +29,68 @@ mutation DeleteIssueLabel($id: String!) {
entityId
}
}

# Retire an issue label
#
# Retired labels stay on the issues that already carry them but cannot be
# applied to new ones — a softer alternative to delete.
mutation RetireIssueLabel($id: String!) {
issueLabelRetire(id: $id) {
success
issueLabel {
...LabelFields
}
}
}

mutation RestoreIssueLabel($id: String!) {
issueLabelRestore(id: $id) {
success
issueLabel {
...LabelFields
}
}
}

mutation CreateProjectLabel($input: ProjectLabelCreateInput!) {
projectLabelCreate(input: $input) {
success
projectLabel {
...ProjectLabelFields
}
}
}

mutation UpdateProjectLabel($id: String!, $input: ProjectLabelUpdateInput!) {
projectLabelUpdate(id: $id, input: $input) {
success
projectLabel {
...ProjectLabelFields
}
}
}

mutation DeleteProjectLabel($id: String!) {
projectLabelDelete(id: $id) {
success
entityId
}
}

mutation RetireProjectLabel($id: String!) {
projectLabelRetire(id: $id) {
success
projectLabel {
...ProjectLabelFields
}
}
}

mutation RestoreProjectLabel($id: String!) {
projectLabelRestore(id: $id) {
success
projectLabel {
...ProjectLabelFields
}
}
}
31 changes: 31 additions & 0 deletions graphql/mutations/project-relations.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# ------------------------------------------------------------
# GraphQL mutations for Linear project dependency relations
# ------------------------------------------------------------

mutation CreateProjectRelation($input: ProjectRelationCreateInput!) {
projectRelationCreate(input: $input) {
success
projectRelation {
...ProjectRelationCoreFields
}
}
}

mutation UpdateProjectRelation(
$id: String!
$input: ProjectRelationUpdateInput!
) {
projectRelationUpdate(id: $id, input: $input) {
success
projectRelation {
...ProjectRelationCoreFields
}
}
}

mutation DeleteProjectRelation($id: String!) {
projectRelationDelete(id: $id) {
success
entityId
}
}
58 changes: 58 additions & 0 deletions graphql/mutations/project-statuses.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# ------------------------------------------------------------
# GraphQL mutations for the workspace project status flow
# ------------------------------------------------------------

mutation CreateProjectStatus($input: ProjectStatusCreateInput!) {
projectStatusCreate(input: $input) {
success
status {
...ProjectStatusCoreFields
}
}
}

mutation UpdateProjectStatus($id: String!, $input: ProjectStatusUpdateInput!) {
projectStatusUpdate(id: $id, input: $input) {
success
status {
...ProjectStatusCoreFields
}
}
}

# Archive a status
#
# Linear refuses this while projects are still assigned to the status, or
# when it is the last status of its type.
mutation ArchiveProjectStatus($id: String!) {
projectStatusArchive(id: $id) {
success
entity {
...ProjectStatusCoreFields
}
}
}

mutation UnarchiveProjectStatus($id: String!) {
projectStatusUnarchive(id: $id) {
success
entity {
...ProjectStatusCoreFields
}
}
}

# Move every project off one status and onto another
#
# The payload carries no entity — only whether the reassignment ran.
mutation ReassignProjectStatus(
$originalProjectStatusId: String!
$newProjectStatusId: String!
) {
projectReassignStatus(
originalProjectStatusId: $originalProjectStatusId
newProjectStatusId: $newProjectStatusId
) {
success
}
}
52 changes: 52 additions & 0 deletions graphql/mutations/project-updates.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# ------------------------------------------------------------
# GraphQL mutations for Linear project status updates
#
# `projectUpdateDelete` is deliberately not wired: Linear deprecates it
# in favour of `projectUpdateArchive`, which is reversible.
# ------------------------------------------------------------

mutation CreateProjectUpdate($input: ProjectUpdateCreateInput!) {
projectUpdateCreate(input: $input) {
success
projectUpdate {
...ProjectUpdateCoreFields
}
}
}

mutation EditProjectUpdate($id: String!, $input: ProjectUpdateUpdateInput!) {
projectUpdateUpdate(id: $id, input: $input) {
success
projectUpdate {
...ProjectUpdateCoreFields
}
}
}

mutation ArchiveProjectUpdate($id: String!) {
projectUpdateArchive(id: $id) {
success
entity {
...ProjectUpdateCoreFields
}
}
}

mutation UnarchiveProjectUpdate($id: String!) {
projectUpdateUnarchive(id: $id) {
success
entity {
...ProjectUpdateCoreFields
}
}
}

# Nudge someone to post the next update
#
# The payload carries no entity — there is nothing to return but whether
# the notification was created.
mutation CreateProjectUpdateReminder($projectId: String!, $userId: String) {
createProjectUpdateReminder(projectId: $projectId, userId: $userId) {
success
}
}
50 changes: 43 additions & 7 deletions graphql/mutations/projects.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -30,28 +30,64 @@ mutation UpdateProject($id: String!, $input: ProjectUpdateInput!) {
}
}

mutation ArchiveProject($id: String!) {
projectArchive(id: $id) {
# Restore a trashed project
#
# Linear collapses "archived" and "trashed" into a single state, so this
# restores whatever projectDelete put away.
mutation UnarchiveProject($id: String!) {
projectUnarchive(id: $id) {
success
entity {
...ProjectDetailWithDefaultConnectionsFields
}
}
}

mutation UnarchiveProject($id: String!) {
projectUnarchive(id: $id) {
# Trash a project
#
# Reversible: UnarchiveProject restores it.
mutation DeleteProject($id: String!) {
projectDelete(id: $id) {
success
entity {
...ProjectDetailWithDefaultConnectionsFields
}
}
}

mutation DeleteProject($id: String!) {
projectDelete(id: $id) {
# Stop syncing a project with an external tracker
#
# Mirrors `attachments disable-sync`: the link stays, the updates stop.
mutation DisableProjectExternalSync(
$projectId: String!
$syncSource: ExternalSyncService!
) {
projectExternalSyncDisable(projectId: $projectId, syncSource: $syncSource) {
success
entity {
project {
...ProjectDetailWithDefaultConnectionsFields
}
}
}

# Add one label to a project
#
# Incremental, unlike `ProjectUpdateInput.labelIds`, which replaces the
# whole set — so `--label-mode add` needs no read of the current labels
# and cannot drop the ones it did not see.
mutation AddProjectLabel($id: String!, $labelId: String!) {
projectAddLabel(id: $id, labelId: $labelId) {
success
project {
...ProjectDetailWithDefaultConnectionsFields
}
}
}

mutation RemoveProjectLabel($id: String!, $labelId: String!) {
projectRemoveLabel(id: $id, labelId: $labelId) {
success
project {
...ProjectDetailWithDefaultConnectionsFields
}
}
Expand Down
Loading
Loading