@aivi/tracker-linear 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -2
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +34 -0
- package/dist/client.d.ts +277 -0
- package/dist/client.js +362 -0
- package/dist/config.d.ts +94 -0
- package/dist/config.js +171 -0
- package/dist/desk.d.ts +13 -0
- package/dist/desk.js +70 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +8 -0
- package/dist/links.d.ts +48 -0
- package/dist/links.js +83 -0
- package/dist/mcp.d.ts +32 -0
- package/dist/mcp.js +126 -0
- package/dist/module.d.ts +38 -0
- package/dist/module.js +767 -0
- package/dist/projects.d.ts +40 -0
- package/dist/projects.js +119 -0
- package/dist/routes.d.ts +16 -0
- package/dist/routes.js +40 -0
- package/dist/runprogress.d.ts +60 -0
- package/dist/runprogress.js +120 -0
- package/dist/setup-project.d.ts +63 -0
- package/dist/setup-project.js +254 -0
- package/dist/setup.d.ts +3 -0
- package/dist/setup.js +526 -0
- package/dist/tracker.d.ts +178 -0
- package/dist/tracker.js +435 -0
- package/dist/webhook.d.ts +97 -0
- package/dist/webhook.js +40 -0
- package/dist/work.d.ts +66 -0
- package/dist/work.js +98 -0
- package/docs/linear.md +90 -0
- package/package.json +56 -4
package/README.md
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @aivi/tracker-linear
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Linear module: one app receiving every webhook, the assistant for people,
|
|
4
|
+
workers in git worktrees, and the Linear MCP proxy. Agent sessions run on
|
|
5
|
+
the host's channel machinery — a delegation becomes one bound conversation
|
|
6
|
+
and one worker turn of the lane's agent in its own worktree.
|
|
7
|
+
|
|
8
|
+
## Exports
|
|
9
|
+
|
|
10
|
+
| Path | Contents |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| `@aivi/tracker-linear` | `createLinearModule`, `openLinearStore`, `describeWorkers`, `LINEAR` |
|
|
13
|
+
| `./config` | the `plugin` declaration: module id, config schema, project sections |
|
|
14
|
+
| `./setup` | `aivi add @aivi/tracker-linear`'s setup guide |
|
|
15
|
+
| `./setupProject` | the `tracker`-role project contributor: writes a project's teams and lanes |
|
|
16
|
+
| `./cli` | the `aivi linear` command |
|
|
17
|
+
|
|
18
|
+
## Model
|
|
19
|
+
|
|
20
|
+
- **One app, one persona, lanes pick agents.** The primary app carries the
|
|
21
|
+
workspace's data feed and every agent-session webhook on one route.
|
|
22
|
+
- **A repository is a Linear team.** Routing reads the issue's team only;
|
|
23
|
+
lanes, branch names, and labels live per team.
|
|
24
|
+
- **Two ids, on purpose.** The module id is this package's short name —
|
|
25
|
+
`tracker-linear` — and so `plugins.tracker-linear`, the `/status` id, the log
|
|
26
|
+
category and the project section's key. The *platform* id stays `linear`: the
|
|
27
|
+
SQLite prefix, the session and message id prefixes, the lease owner, the
|
|
28
|
+
webhook URL Linear's dashboard holds and the `aivi linear` command. Those say
|
|
29
|
+
who a conversation is on, not which package speaks for it, and renaming a
|
|
30
|
+
prefix would orphan every conversation already bound to it.
|
|
31
|
+
- **Stop means stop.** The turn is discarded, capacity released, worktree
|
|
32
|
+
and session kept; only an unverifiable stop is `blocked`.
|
|
33
|
+
|
|
34
|
+
## Dependencies
|
|
35
|
+
|
|
36
|
+
`@aivi/host` as a peer (runs in-process with webhook routes on the same
|
|
37
|
+
listener).
|
|
38
|
+
|
|
39
|
+
## Docs
|
|
40
|
+
|
|
41
|
+
[linear](../../docs/linear.md) ·
|
|
42
|
+
[plan (what is left)](../../docs/plans/linear.md)
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** The Linear module's operator commands, mounted into the server CLI through
|
|
2
|
+
* the `./cli` subpath contract (@aivi/plugin's `(ctx) => Command`): status and
|
|
3
|
+
* resolve, over the same conversation store the module runs on. */
|
|
4
|
+
import type { PluginCliContext } from '@aivi/plugin';
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
export default _default;
|
|
7
|
+
declare function _default(ctx: PluginCliContext): Command;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { resolveBlocked } from '@aivi/plugin';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
const enabled = async (ctx) => {
|
|
4
|
+
const loaded = await ctx.loaded();
|
|
5
|
+
const config = loaded.config.plugins['tracker-linear'];
|
|
6
|
+
if (!config)
|
|
7
|
+
throw new Error('Linear is not configured in config.json (no plugins.tracker-linear block)');
|
|
8
|
+
return config;
|
|
9
|
+
};
|
|
10
|
+
export default (ctx) => {
|
|
11
|
+
const linear = new Command('linear').description('the Linear module');
|
|
12
|
+
linear
|
|
13
|
+
.command('status')
|
|
14
|
+
.description('Inspect Linear conversations (workers and the assistant)')
|
|
15
|
+
.action(async () => {
|
|
16
|
+
await enabled(ctx);
|
|
17
|
+
const { describeWorkers, openLinearStore } = await import('./index.js');
|
|
18
|
+
await ctx.withStore(store => {
|
|
19
|
+
const inbox = openLinearStore(store);
|
|
20
|
+
ctx.print({ conversations: describeWorkers(inbox) });
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
linear
|
|
24
|
+
.command('resolve <id>')
|
|
25
|
+
.description('Release a blocked worker')
|
|
26
|
+
.requiredOption('--reason <text>', 'what was found and done')
|
|
27
|
+
.requiredOption('--confirm-stopped', 'the external side has stopped')
|
|
28
|
+
.action(async (id, options) => {
|
|
29
|
+
await enabled(ctx);
|
|
30
|
+
const { openLinearStore } = await import('./index.js');
|
|
31
|
+
await resolveBlocked(ctx, id, options.reason, store => openLinearStore(store));
|
|
32
|
+
});
|
|
33
|
+
return linear;
|
|
34
|
+
};
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import type { Logger } from '@aivi/core';
|
|
2
|
+
/** What one Linear app needs to talk to the API: a client-credentials token, nothing persisted. */
|
|
3
|
+
export interface LinearCredentials {
|
|
4
|
+
clientId: string;
|
|
5
|
+
clientSecret: string;
|
|
6
|
+
}
|
|
7
|
+
export interface LinearClientOptions {
|
|
8
|
+
/** `https://api.linear.app` unless a test points elsewhere. */
|
|
9
|
+
baseUrl?: string | undefined;
|
|
10
|
+
/** Called with string URLs only; exactly what the setup context can give. */
|
|
11
|
+
fetch?: ((url: string, init?: RequestInit) => Promise<Response>) | undefined;
|
|
12
|
+
log?: Logger | undefined;
|
|
13
|
+
/** Scopes requested with the token; the agent scopes are what make the app delegable and mentionable. */
|
|
14
|
+
scope?: string | undefined;
|
|
15
|
+
/** Refresh this long before `expires_in` says the token is gone (default one hour). */
|
|
16
|
+
refreshSkewMs?: number | undefined;
|
|
17
|
+
}
|
|
18
|
+
/** One of the five activity types an app may emit; `prompt` is what people send and never ours. */
|
|
19
|
+
export type AgentActivityContent = {
|
|
20
|
+
type: 'thought';
|
|
21
|
+
body: string;
|
|
22
|
+
} | {
|
|
23
|
+
type: 'elicitation';
|
|
24
|
+
body: string;
|
|
25
|
+
} | {
|
|
26
|
+
type: 'action';
|
|
27
|
+
action: string;
|
|
28
|
+
parameter: string;
|
|
29
|
+
result?: string;
|
|
30
|
+
} | {
|
|
31
|
+
type: 'response';
|
|
32
|
+
body: string;
|
|
33
|
+
} | {
|
|
34
|
+
type: 'error';
|
|
35
|
+
body: string;
|
|
36
|
+
};
|
|
37
|
+
export interface AgentActivityInput {
|
|
38
|
+
agentSessionId: string;
|
|
39
|
+
content: AgentActivityContent;
|
|
40
|
+
/** Shown until the next activity replaces it; only `thought` and `action` may be ephemeral. */
|
|
41
|
+
ephemeral?: boolean;
|
|
42
|
+
signal?: 'auth' | 'select';
|
|
43
|
+
signalMetadata?: Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
/** The issue fields eligibility and routing read. */
|
|
46
|
+
export interface LinearIssue {
|
|
47
|
+
id: string;
|
|
48
|
+
identifier: string;
|
|
49
|
+
title: string;
|
|
50
|
+
description: string | null;
|
|
51
|
+
branchName: string;
|
|
52
|
+
url: string;
|
|
53
|
+
state: {
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
type: string;
|
|
57
|
+
};
|
|
58
|
+
team: {
|
|
59
|
+
id: string;
|
|
60
|
+
key: string;
|
|
61
|
+
};
|
|
62
|
+
labels: {
|
|
63
|
+
id: string;
|
|
64
|
+
name: string;
|
|
65
|
+
}[];
|
|
66
|
+
delegate: {
|
|
67
|
+
id: string;
|
|
68
|
+
} | null;
|
|
69
|
+
assignee: {
|
|
70
|
+
id: string;
|
|
71
|
+
name: string;
|
|
72
|
+
} | null;
|
|
73
|
+
/** ISO instant the issue was archived — Linear's delete — null while it
|
|
74
|
+
* lives. A dead ticket gets nothing from a session: there is no work. */
|
|
75
|
+
archivedAt: string | null;
|
|
76
|
+
/** Issues blocking this one; a blocker not in a finished state holds the listener back. */
|
|
77
|
+
blockedBy: {
|
|
78
|
+
id: string;
|
|
79
|
+
state: {
|
|
80
|
+
id: string;
|
|
81
|
+
name: string;
|
|
82
|
+
type: string;
|
|
83
|
+
};
|
|
84
|
+
}[];
|
|
85
|
+
/** Linear's rank — 1 Urgent through 4 Low, 0 for unranked: what sorts a
|
|
86
|
+
* board top to bottom, unranked last. */
|
|
87
|
+
priority: number;
|
|
88
|
+
/** ISO instant of creation; the tie-break under priority, oldest first. */
|
|
89
|
+
createdAt: string;
|
|
90
|
+
}
|
|
91
|
+
export interface LinearAgentSession {
|
|
92
|
+
id: string;
|
|
93
|
+
status: string;
|
|
94
|
+
issue: {
|
|
95
|
+
id: string;
|
|
96
|
+
} | null;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* What the delegate mutation answers: whether it worked, and the issue's
|
|
100
|
+
* agent sessions as Linear sees them in that very moment. Making the app
|
|
101
|
+
* the delegate creates the session itself, and it appears here — the
|
|
102
|
+
* listener starts the worker from this answer, not from a webhook.
|
|
103
|
+
*/
|
|
104
|
+
export interface DelegateAnswer {
|
|
105
|
+
success: boolean;
|
|
106
|
+
issue?: {
|
|
107
|
+
identifier: string;
|
|
108
|
+
agentSessions: {
|
|
109
|
+
nodes: {
|
|
110
|
+
id: string;
|
|
111
|
+
status: string;
|
|
112
|
+
startedAt: string | null;
|
|
113
|
+
url: string | null;
|
|
114
|
+
}[];
|
|
115
|
+
};
|
|
116
|
+
} | undefined;
|
|
117
|
+
}
|
|
118
|
+
/** A team in the workspace: the `id` is what aivi's config holds, the `key`
|
|
119
|
+
* is what Linear's URLs and issue identifiers show. States drive the lane
|
|
120
|
+
* picker; `type` completed or canceled lanes are never offered for work. */
|
|
121
|
+
export interface LinearTeam {
|
|
122
|
+
id: string;
|
|
123
|
+
key: string;
|
|
124
|
+
name: string;
|
|
125
|
+
states: {
|
|
126
|
+
id: string;
|
|
127
|
+
name: string;
|
|
128
|
+
type: string;
|
|
129
|
+
}[];
|
|
130
|
+
}
|
|
131
|
+
export declare class LinearApiError extends Error {
|
|
132
|
+
readonly status: number;
|
|
133
|
+
/** GraphQL error codes (`extensions.code`), kept apart because the HTTP
|
|
134
|
+
* status says nothing about them — Linear answers entity-not-found with a
|
|
135
|
+
* 200. Empty for transport and token errors. */
|
|
136
|
+
readonly codes: readonly string[];
|
|
137
|
+
constructor(message: string, status: number, codes?: readonly string[]);
|
|
138
|
+
/** Linear's word for "that entity is gone", whatever shape it comes in: a
|
|
139
|
+
* deleted issue takes its agent session with it, and every later call
|
|
140
|
+
* names it. The message is the stable part (`Entity not found: …`); the
|
|
141
|
+
* codes only say *how* Linear refused (`INPUT_ERROR` for a lookup of a
|
|
142
|
+
* dead session, live 2026-10-06), never *what*, so no code may stand in
|
|
143
|
+
* for the words. */
|
|
144
|
+
static isGone(error: unknown): boolean;
|
|
145
|
+
}
|
|
146
|
+
export declare class LinearClient {
|
|
147
|
+
private readonly credentials;
|
|
148
|
+
private readonly baseUrl;
|
|
149
|
+
private readonly fetchImpl;
|
|
150
|
+
private readonly log;
|
|
151
|
+
private readonly scope;
|
|
152
|
+
private readonly skew;
|
|
153
|
+
private token;
|
|
154
|
+
private refreshing;
|
|
155
|
+
constructor(credentials: LinearCredentials, options?: LinearClientOptions);
|
|
156
|
+
/** A valid token, fetching or refreshing when needed; concurrent callers share one request. */
|
|
157
|
+
accessToken(force?: boolean): Promise<string>;
|
|
158
|
+
private fetchToken;
|
|
159
|
+
/** Run one GraphQL operation; a 401 refreshes the token and retries once. */
|
|
160
|
+
graphql<T>(query: string, variables?: Record<string, unknown>, retried?: boolean): Promise<T>;
|
|
161
|
+
/** The app user's own id and its workspace in one round; also proves the
|
|
162
|
+
* credentials work. The tracker adapter learns `userId`/`orgId` here. */
|
|
163
|
+
viewer(): Promise<{
|
|
164
|
+
id: string;
|
|
165
|
+
organizationId: string;
|
|
166
|
+
}>;
|
|
167
|
+
/** The app user's own id in this workspace; also proves the credentials work. */
|
|
168
|
+
viewerId(): Promise<string>;
|
|
169
|
+
/** The teams this app can see with their workflow states, archived excluded:
|
|
170
|
+
* the answer to "which id is PEC?" and "which lanes does PEC have?".
|
|
171
|
+
* Linear returns `states` as a connection; its `nodes` are flattened here. */
|
|
172
|
+
listTeams(): Promise<LinearTeam[]>;
|
|
173
|
+
createActivity(input: AgentActivityInput): Promise<string>;
|
|
174
|
+
/** Move an issue to a workflow state by its id — how a lane move is
|
|
175
|
+
* performed. (schema: `issueUpdate(id: String!, input: IssueUpdateInput!)`,
|
|
176
|
+
* the state carried as `input.stateId`.) */
|
|
177
|
+
transitionIssue(issueId: string, stateId: string): Promise<void>;
|
|
178
|
+
/** The label id a name stands for on a team: the team's own label wins over
|
|
179
|
+
* the workspace's when both carry the name (the API applies any label to
|
|
180
|
+
* any issue, so the disambiguation lives here), and a name no label
|
|
181
|
+
* answers is **created on the team** — the human label must be applicable
|
|
182
|
+
* without a visit to settings first. (schema:
|
|
183
|
+
* `issueLabels(filter: {name: {eq}})`, `issueLabelCreate(input:)`.) */
|
|
184
|
+
labelIdForTeam(teamId: string, name: string): Promise<string>;
|
|
185
|
+
/** Put a label on an issue — how the human label marks a ticket: a stop,
|
|
186
|
+
* a failed closing, a stop the runtime would not answer. (schema:
|
|
187
|
+
* `issueAddLabel(id: String!, labelId: String!)`.) */
|
|
188
|
+
addLabel(issueId: string, labelId: string): Promise<void>;
|
|
189
|
+
/** Take a label off an issue — the lift that lets the walk work a marked
|
|
190
|
+
* ticket again. (schema: `issueRemoveLabel(id: String!, labelId: String!)`.) */
|
|
191
|
+
removeLabel(issueId: string, labelId: string): Promise<void>;
|
|
192
|
+
/** A comment on the **issue** itself, not the agent session's activity:
|
|
193
|
+
* the closing note a person reads without opening the session. Posted
|
|
194
|
+
* with the app's token, so it stands as the app's comment. */
|
|
195
|
+
createComment(issueId: string, body: string): Promise<void>;
|
|
196
|
+
/** The ticket's comment trail, oldest first — what `aivi_ticket_comments`
|
|
197
|
+
* reads back. The author is the writer's display name when Linear gives
|
|
198
|
+
* one; the app's own comments carry the app's name like any other. */
|
|
199
|
+
issueComments(issueId: string): Promise<{
|
|
200
|
+
author: string | null;
|
|
201
|
+
body: string;
|
|
202
|
+
at: string;
|
|
203
|
+
}[]>;
|
|
204
|
+
/** Rewrite the ticket's own words — what `aivi_ticket_edit` performs.
|
|
205
|
+
* Only the fields named are touched; Linear answers with the ticket as
|
|
206
|
+
* it now stands. */
|
|
207
|
+
issueUpdate(issueId: string, changes: {
|
|
208
|
+
title?: string;
|
|
209
|
+
description?: string;
|
|
210
|
+
}): Promise<LinearIssue>;
|
|
211
|
+
/** The team's label catalogue — the names a worker may add or remove. */
|
|
212
|
+
teamLabelNames(teamId: string): Promise<string[]>;
|
|
213
|
+
/** What a closing note asks Linear for in one round: the issue's comment
|
|
214
|
+
* bodies (the idempotence check — a note already standing is never
|
|
215
|
+
* posted twice) and the agent sessions with their web urls (the note's
|
|
216
|
+
* marker and the link a person clicks to read the whole trail). The
|
|
217
|
+
* comment page is deliberately wide: the check must see a note older
|
|
218
|
+
* than any retry horizon. */
|
|
219
|
+
closingNoteFacts(issueId: string): Promise<{
|
|
220
|
+
comments: {
|
|
221
|
+
body: string;
|
|
222
|
+
}[];
|
|
223
|
+
sessions: {
|
|
224
|
+
id: string;
|
|
225
|
+
url: string | null;
|
|
226
|
+
}[];
|
|
227
|
+
}>;
|
|
228
|
+
/** Whether Linear has closed this agent session: `endedAt` is its own word
|
|
229
|
+
* for "the result was shown", which is what a follower's retry asks
|
|
230
|
+
* before it would render a result twice. */
|
|
231
|
+
agentSessionEnded(agentSessionId: string): Promise<boolean>;
|
|
232
|
+
/** Replace the session's plan wholesale — Linear takes the full array
|
|
233
|
+
* every time (docs/agent-interaction, Agent Plans: partial updates are
|
|
234
|
+
* explicitly not a thing). `plan` is a JSONObject: the steps array as
|
|
235
|
+
* `{content, status}`. */
|
|
236
|
+
setPlan(agentSessionId: string, steps: {
|
|
237
|
+
content: string;
|
|
238
|
+
status: string;
|
|
239
|
+
}[]): Promise<void>;
|
|
240
|
+
/** Tickets sitting in one state of one team — the eligibility walk's
|
|
241
|
+
* top-to-bottom reading. Linear's public query cannot sort (its `sort`
|
|
242
|
+
* parameter is internal), so the board's order is computed here:
|
|
243
|
+
* Linear's own priority sort — Urgent first, unranked last — and oldest
|
|
244
|
+
* first within a rank. Archived issues never show: the connection
|
|
245
|
+
* leaves them out unless asked. `truncated` says more than 100 sit in
|
|
246
|
+
* the lane; the walk is told rather than shown a partial board in
|
|
247
|
+
* silence. */
|
|
248
|
+
issuesIn(teamId: string, stateId: string): Promise<{
|
|
249
|
+
issues: LinearIssue[];
|
|
250
|
+
truncated: boolean;
|
|
251
|
+
}>;
|
|
252
|
+
issue(id: string): Promise<LinearIssue>;
|
|
253
|
+
/** Make (or unmake, with `null`) this app the issue's delegate; the human
|
|
254
|
+
* assignee stays. Making the app the delegate is what creates the agent
|
|
255
|
+
* session — Linear returns it in this very answer (live, 2026-09-26). */
|
|
256
|
+
setDelegate(issueId: string, delegateId: string | null): Promise<DelegateAnswer>;
|
|
257
|
+
/** Create an issue in a team — the installer's throwaway ticket and the
|
|
258
|
+
* worker's escape hatch. `stateId` lands it in a named workflow state;
|
|
259
|
+
* omitted, Linear's own default (the team's triage) takes it. */
|
|
260
|
+
createIssue(input: {
|
|
261
|
+
teamId: string;
|
|
262
|
+
title: string;
|
|
263
|
+
description?: string;
|
|
264
|
+
stateId?: string;
|
|
265
|
+
}): Promise<LinearIssue>;
|
|
266
|
+
/** Archive an issue — Linear's delete, off the board. The installer tidies
|
|
267
|
+
* its own throwaway ticket with this. */
|
|
268
|
+
deleteIssue(issueId: string): Promise<void>;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Turn what an operator pastes — a team key from a Linear URL or a raw team id —
|
|
272
|
+
* into the ids the config must hold. An exact id matches first, then a key
|
|
273
|
+
* regardless of case; duplicates collapse and the given order is kept. An
|
|
274
|
+
* unknown token is an error naming it and listing the teams the app can see:
|
|
275
|
+
* a private team the app has not joined shows up here as unknown.
|
|
276
|
+
*/
|
|
277
|
+
export declare function resolveTeams(teams: LinearTeam[], tokens: string[]): string[];
|