@clipboard-health/groundcrew 4.20.2 → 4.21.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 CHANGED
@@ -93,6 +93,9 @@ crew init [--global | --local] [--force] [--dry-run] # create a crew.config.
93
93
  [--project-dir <dir>] [--repo <repo>]...
94
94
  [--runner <auto|safehouse|sdx|none>] [--model <claude|codex>]
95
95
  crew doctor # check setup
96
+ crew task list [--source <name>] # list tasks across sources
97
+ crew task get <TASK> [--source <name>] [--prompt] # inspect one task or its prompt
98
+ crew task create "Title" --source <name> --agent <name> # create a source task
96
99
  crew status [<TASK>] # inspect current state or one task
97
100
  crew run [--watch] # one-shot or --watch forever
98
101
  crew start <TASK> # provision + launch one task now
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AA+QA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAwCvD"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAqRA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAwCvD"}
package/dist/cli.js CHANGED
@@ -8,6 +8,7 @@ import { resumeWorkspaceCli } from "./commands/resumeWorkspace.js";
8
8
  import { setupWorkspaceCli } from "./commands/setupWorkspace.js";
9
9
  import { sourceCli } from "./commands/source.js";
10
10
  import { statusCli } from "./commands/status.js";
11
+ import { taskCli } from "./commands/task.js";
11
12
  import { createDefaultUpgradeCliOptions, upgradeCli } from "./commands/upgrade.js";
12
13
  import { errorMessage, parseDryRunPositionals, readEnvironmentVariable, readTaskArgument, setVerbose, writeError, writeOutput, } from "./lib/util.js";
13
14
  const REMOVED_SANDBOX_COMMAND_MESSAGE = [
@@ -134,6 +135,11 @@ const SUBCOMMANDS = {
134
135
  usage: "<list|verify> [...]",
135
136
  invoke: sourceCli,
136
137
  },
138
+ task: {
139
+ summary: "List, get, and create tasks across configured sources",
140
+ usage: "<list|get|create> [...]",
141
+ invoke: taskCli,
142
+ },
137
143
  status: {
138
144
  summary: "Print read-only groundcrew state, or one task's local/Linear status",
139
145
  usage: "[<task>]",
@@ -0,0 +1,2 @@
1
+ export declare function taskCli(argv: string[]): Promise<void>;
2
+ //# sourceMappingURL=task.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task.d.ts","sourceRoot":"","sources":["../../src/commands/task.ts"],"names":[],"mappings":"AAmmBA,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAe3D"}
@@ -0,0 +1,488 @@
1
+ import { buildSources, sourcesFromConfig } from "../lib/buildSources.js";
2
+ import { loadConfig } from "../lib/config.js";
3
+ import { naturalIdFromCanonical, } from "../lib/taskSource.js";
4
+ import { writeOutput } from "../lib/util.js";
5
+ const TASK_USAGE = `Usage: crew task <subcommand>
6
+
7
+ Subcommands:
8
+ list [options] List tasks across configured sources
9
+ get <task-id> [options] Get one task
10
+ create "Short title" [options] Create one task`;
11
+ const LIST_USAGE = `Usage: crew task list [options]
12
+
13
+ Options:
14
+ --source <name> Limit to one source.
15
+ --status <status> Filter by status. Repeatable.
16
+ --agent <name> Filter by agent/model.
17
+ --repo <owner/repo> Filter by repository.
18
+ --blocked Show only blocked tasks.
19
+ --unblocked Show only unblocked tasks.
20
+ --json Print normalized task JSON.
21
+ --limit <n> Limit output.`;
22
+ const GET_USAGE = `Usage: crew task get <task-id> [options]
23
+
24
+ Options:
25
+ --source <name> Resolve a source-native ID against a specific source.
26
+ --json Print normalized task JSON.
27
+ --prompt Print only the task description/prompt.`;
28
+ const CREATE_USAGE = `Usage: crew task create "Short title" --source <source> --agent <agent> [options]`;
29
+ const CANONICAL_STATUSES = [
30
+ "todo",
31
+ "in-progress",
32
+ "in-review",
33
+ "done",
34
+ "other",
35
+ ];
36
+ const CANONICAL_STATUS_SET = new Set(CANONICAL_STATUSES);
37
+ function isCanonicalStatus(value) {
38
+ return CANONICAL_STATUS_SET.has(value);
39
+ }
40
+ function readOptionValue(argv, index, option, usage) {
41
+ const value = argv[index + 1];
42
+ if (value === undefined) {
43
+ throw new Error(`crew task: ${option} requires a value\n${usage}`);
44
+ }
45
+ return value;
46
+ }
47
+ const CREATE_VALUE_HANDLERS = {
48
+ "--source": (state, value) => {
49
+ state.sourceName = value;
50
+ },
51
+ "--agent": (state, value) => {
52
+ state.agent = value;
53
+ },
54
+ "--repo": (state, value) => {
55
+ state.repository = value;
56
+ },
57
+ "--id": (state, value) => {
58
+ state.id = value;
59
+ },
60
+ "--priority": (state, value) => {
61
+ state.priority = value;
62
+ },
63
+ "--project": (state, value) => {
64
+ state.projects.push(value);
65
+ },
66
+ "--context": (state, value) => {
67
+ state.contexts.push(value);
68
+ },
69
+ "--dep": (state, value) => {
70
+ state.dependencies.push(value);
71
+ },
72
+ "--due": (state, value) => {
73
+ state.due = value;
74
+ },
75
+ "--rec": (state, value) => {
76
+ state.recurrence = value;
77
+ },
78
+ "--prompt-file": (state, value) => {
79
+ state.promptFile = value;
80
+ },
81
+ "--description": (state, value) => {
82
+ state.description = value;
83
+ },
84
+ };
85
+ function parseLimit(raw) {
86
+ const limit = Number.parseInt(raw, 10);
87
+ if (!Number.isInteger(limit) || limit < 1 || String(limit) !== raw) {
88
+ throw new Error("crew task list: --limit must be a positive integer");
89
+ }
90
+ return limit;
91
+ }
92
+ function parseListOptions(argv) {
93
+ const options = {
94
+ statuses: [],
95
+ blocked: false,
96
+ unblocked: false,
97
+ json: false,
98
+ };
99
+ for (let index = 0; index < argv.length; index += 1) {
100
+ const argument = argv[index];
101
+ /* v8 ignore next 3 @preserve -- index is bounded by argv.length; guard exists for noUncheckedIndexedAccess */
102
+ if (argument === undefined) {
103
+ continue;
104
+ }
105
+ if (argument === "--source") {
106
+ options.sourceName = readOptionValue(argv, index, argument, LIST_USAGE);
107
+ index += 1;
108
+ continue;
109
+ }
110
+ if (argument === "--status") {
111
+ const status = readOptionValue(argv, index, argument, LIST_USAGE);
112
+ if (!isCanonicalStatus(status)) {
113
+ throw new Error(`crew task list: unknown status "${status}" (expected ${CANONICAL_STATUSES.join(", ")})`);
114
+ }
115
+ options.statuses.push(status);
116
+ index += 1;
117
+ continue;
118
+ }
119
+ if (argument === "--agent") {
120
+ options.agent = readOptionValue(argv, index, argument, LIST_USAGE);
121
+ index += 1;
122
+ continue;
123
+ }
124
+ if (argument === "--repo") {
125
+ options.repository = readOptionValue(argv, index, argument, LIST_USAGE);
126
+ index += 1;
127
+ continue;
128
+ }
129
+ if (argument === "--blocked") {
130
+ options.blocked = true;
131
+ continue;
132
+ }
133
+ if (argument === "--unblocked") {
134
+ options.unblocked = true;
135
+ continue;
136
+ }
137
+ if (argument === "--json") {
138
+ options.json = true;
139
+ continue;
140
+ }
141
+ if (argument === "--limit") {
142
+ options.limit = parseLimit(readOptionValue(argv, index, argument, LIST_USAGE));
143
+ index += 1;
144
+ continue;
145
+ }
146
+ throw new Error(`crew task list: unknown argument: ${argument}\n${LIST_USAGE}`);
147
+ }
148
+ if (options.blocked && options.unblocked) {
149
+ throw new Error("crew task list: --blocked and --unblocked are mutually exclusive");
150
+ }
151
+ return options;
152
+ }
153
+ function parseGetOptions(argv) {
154
+ const positionals = [];
155
+ let sourceName;
156
+ let json = false;
157
+ let prompt = false;
158
+ for (let index = 0; index < argv.length; index += 1) {
159
+ const argument = argv[index];
160
+ /* v8 ignore next 3 @preserve -- index is bounded by argv.length; guard exists for noUncheckedIndexedAccess */
161
+ if (argument === undefined) {
162
+ continue;
163
+ }
164
+ if (argument === "--source") {
165
+ sourceName = readOptionValue(argv, index, argument, GET_USAGE);
166
+ index += 1;
167
+ continue;
168
+ }
169
+ if (argument === "--json") {
170
+ json = true;
171
+ continue;
172
+ }
173
+ if (argument === "--prompt") {
174
+ prompt = true;
175
+ continue;
176
+ }
177
+ if (argument.startsWith("-")) {
178
+ throw new Error(`crew task get: unknown option: ${argument}\n${GET_USAGE}`);
179
+ }
180
+ positionals.push(argument);
181
+ }
182
+ const [taskId, ...extras] = positionals;
183
+ if (taskId === undefined || extras.length > 0) {
184
+ throw new Error(GET_USAGE);
185
+ }
186
+ if (json && prompt) {
187
+ throw new Error("crew task get: --json and --prompt are mutually exclusive");
188
+ }
189
+ return { taskId, ...(sourceName === undefined ? {} : { sourceName }), json, prompt };
190
+ }
191
+ function parseCreateOptions(argv) {
192
+ const state = {
193
+ positionals: [],
194
+ projects: [],
195
+ contexts: [],
196
+ dependencies: [],
197
+ edit: false,
198
+ json: false,
199
+ };
200
+ for (let index = 0; index < argv.length; index += 1) {
201
+ const argument = argv[index];
202
+ /* v8 ignore next 3 @preserve -- index is bounded by argv.length; guard exists for noUncheckedIndexedAccess */
203
+ if (argument === undefined) {
204
+ continue;
205
+ }
206
+ const valueHandler = CREATE_VALUE_HANDLERS[argument];
207
+ if (valueHandler !== undefined) {
208
+ valueHandler(state, readOptionValue(argv, index, argument, CREATE_USAGE));
209
+ index += 1;
210
+ continue;
211
+ }
212
+ if (argument === "--edit") {
213
+ state.edit = true;
214
+ continue;
215
+ }
216
+ if (argument === "--json") {
217
+ state.json = true;
218
+ continue;
219
+ }
220
+ if (argument.startsWith("-")) {
221
+ throw new Error(`crew task create: unknown option: ${argument}\n${CREATE_USAGE}`);
222
+ }
223
+ state.positionals.push(argument);
224
+ }
225
+ const [title, ...extras] = state.positionals;
226
+ if (title === undefined || extras.length > 0) {
227
+ throw new Error(`${CREATE_USAGE}\nQuote multi-word titles as one argument.`);
228
+ }
229
+ if (state.sourceName === undefined) {
230
+ throw new Error("crew task create: --source is required");
231
+ }
232
+ if (state.agent === undefined) {
233
+ throw new Error("crew task create: --agent is required");
234
+ }
235
+ const input = {
236
+ title,
237
+ agent: state.agent,
238
+ projects: state.projects,
239
+ contexts: state.contexts,
240
+ dependencies: state.dependencies,
241
+ edit: state.edit,
242
+ ...(state.repository === undefined ? {} : { repository: state.repository }),
243
+ ...(state.id === undefined ? {} : { id: state.id }),
244
+ ...(state.priority === undefined ? {} : { priority: state.priority }),
245
+ ...(state.due === undefined ? {} : { due: state.due }),
246
+ ...(state.recurrence === undefined ? {} : { recurrence: state.recurrence }),
247
+ ...(state.promptFile === undefined ? {} : { promptFile: state.promptFile }),
248
+ ...(state.description === undefined ? {} : { description: state.description }),
249
+ };
250
+ return { title, sourceName: state.sourceName, input, json: state.json };
251
+ }
252
+ async function loadTaskSources() {
253
+ const config = await loadConfig();
254
+ return await buildSources(sourcesFromConfig(config), { globalConfig: config });
255
+ }
256
+ function findSource(sources, sourceName) {
257
+ const source = sources.find((candidate) => candidate.name === sourceName);
258
+ if (source === undefined) {
259
+ throw new Error(`crew task: no source named "${sourceName}"`);
260
+ }
261
+ return source;
262
+ }
263
+ function taskIsBlocked(task) {
264
+ return task.hasMoreBlockers || task.blockers.some((blocker) => blocker.status !== "done");
265
+ }
266
+ function filterTasks(tasks, options) {
267
+ let filtered = [...tasks];
268
+ if (options.statuses.length > 0) {
269
+ filtered = filtered.filter((task) => options.statuses.includes(task.status));
270
+ }
271
+ if (options.agent !== undefined) {
272
+ filtered = filtered.filter((task) => task.model === options.agent);
273
+ }
274
+ if (options.repository !== undefined) {
275
+ filtered = filtered.filter((task) => task.repository === options.repository);
276
+ }
277
+ if (options.blocked) {
278
+ filtered = filtered.filter(taskIsBlocked);
279
+ }
280
+ if (options.unblocked) {
281
+ filtered = filtered.filter((task) => !taskIsBlocked(task));
282
+ }
283
+ if (options.limit !== undefined) {
284
+ filtered = filtered.slice(0, options.limit);
285
+ }
286
+ return filtered;
287
+ }
288
+ function printableTask(task) {
289
+ return {
290
+ id: task.id,
291
+ source: task.source,
292
+ title: task.title,
293
+ description: task.description,
294
+ status: task.status,
295
+ repository: task.repository,
296
+ model: task.model,
297
+ assignee: task.assignee,
298
+ updatedAt: task.updatedAt,
299
+ blockers: task.blockers,
300
+ hasMoreBlockers: task.hasMoreBlockers,
301
+ ...(task.url === undefined ? {} : { url: task.url }),
302
+ ...(task.priority === undefined ? {} : { priority: task.priority }),
303
+ };
304
+ }
305
+ function writeJson(tasks) {
306
+ writeOutput(JSON.stringify(tasks.map(printableTask), null, 2));
307
+ }
308
+ function writeTaskJson(task) {
309
+ writeOutput(JSON.stringify(printableTask(task), null, 2));
310
+ }
311
+ function writeTaskTable(tasks) {
312
+ if (tasks.length === 0) {
313
+ writeOutput("(no tasks)");
314
+ return;
315
+ }
316
+ const rows = tasks.map((task) => ({
317
+ id: task.id,
318
+ status: task.status,
319
+ agent: task.model ?? "-",
320
+ repository: task.repository ?? "-",
321
+ blocked: taskIsBlocked(task) ? "yes" : "no",
322
+ title: task.title,
323
+ }));
324
+ const idWidth = Math.max(2, ...rows.map((row) => row.id.length));
325
+ const statusWidth = Math.max(6, ...rows.map((row) => row.status.length));
326
+ const agentWidth = Math.max(5, ...rows.map((row) => row.agent.length));
327
+ const repositoryWidth = Math.max(4, ...rows.map((row) => row.repository.length));
328
+ writeOutput([
329
+ "ID".padEnd(idWidth),
330
+ "STATUS".padEnd(statusWidth),
331
+ "AGENT".padEnd(agentWidth),
332
+ "REPO".padEnd(repositoryWidth),
333
+ "BLOCKED",
334
+ "TITLE",
335
+ ].join(" "));
336
+ for (const row of rows) {
337
+ writeOutput([
338
+ row.id.padEnd(idWidth),
339
+ row.status.padEnd(statusWidth),
340
+ row.agent.padEnd(agentWidth),
341
+ row.repository.padEnd(repositoryWidth),
342
+ row.blocked.padEnd(7),
343
+ row.title,
344
+ ].join(" "));
345
+ }
346
+ }
347
+ function canonicalParts(taskId) {
348
+ const colonIndex = taskId.indexOf(":");
349
+ if (colonIndex === -1) {
350
+ return undefined;
351
+ }
352
+ const sourceName = taskId.slice(0, colonIndex);
353
+ const naturalId = taskId.slice(colonIndex + 1);
354
+ if (sourceName.length === 0 || naturalId.length === 0) {
355
+ throw new Error(`crew task get: invalid canonical task id "${taskId}"`);
356
+ }
357
+ return { sourceName, naturalId };
358
+ }
359
+ async function taskFromSource(source, naturalId) {
360
+ return await source.getTask(naturalId);
361
+ }
362
+ async function resolveTask(sources, taskId, sourceName) {
363
+ const canonical = canonicalParts(taskId);
364
+ if (canonical !== undefined) {
365
+ if (sourceName !== undefined && sourceName !== canonical.sourceName) {
366
+ throw new Error(`crew task get: canonical id "${taskId}" already names source "${canonical.sourceName}"`);
367
+ }
368
+ const source = findSource(sources, canonical.sourceName);
369
+ const task = await taskFromSource(source, canonical.naturalId);
370
+ if (task === null) {
371
+ throw new Error(`Task ${taskId} not found in source "${source.name}".`);
372
+ }
373
+ return task;
374
+ }
375
+ if (sourceName !== undefined) {
376
+ const source = findSource(sources, sourceName);
377
+ const task = await taskFromSource(source, taskId);
378
+ if (task === null) {
379
+ throw new Error(`Task ${taskId} not found in source "${source.name}".`);
380
+ }
381
+ return task;
382
+ }
383
+ const results = await Promise.allSettled(sources.map(async (source) => await taskFromSource(source, taskId)));
384
+ const matches = [];
385
+ const rejections = [];
386
+ for (const result of results) {
387
+ if (result.status === "fulfilled") {
388
+ if (result.value !== null) {
389
+ matches.push(result.value);
390
+ }
391
+ continue;
392
+ }
393
+ rejections.push(result.reason);
394
+ }
395
+ if (matches.length === 0) {
396
+ if (rejections.length > 0) {
397
+ throw rejections[0];
398
+ }
399
+ throw new Error(`Task ${taskId} not found across configured sources.`);
400
+ }
401
+ if (matches.length > 1) {
402
+ throw new Error(`Task id "${taskId}" matched multiple sources: ${matches.map((task) => task.id).join(", ")}. Re-run with a canonical id or --source <name>.`);
403
+ }
404
+ const [match] = matches;
405
+ /* v8 ignore next 3 @preserve -- matches.length was checked above; guard exists for noUncheckedIndexedAccess */
406
+ if (match === undefined) {
407
+ throw new Error(`Task ${taskId} not found across configured sources.`);
408
+ }
409
+ return match;
410
+ }
411
+ function writeTaskDetails(task) {
412
+ writeOutput(task.id);
413
+ writeOutput(`title: ${task.title}`);
414
+ writeOutput(`status: ${task.status}`);
415
+ writeOutput(`source: ${task.source}`);
416
+ if (task.repository !== undefined) {
417
+ writeOutput(`repo: ${task.repository}`);
418
+ }
419
+ if (task.model !== undefined) {
420
+ writeOutput(`agent: ${task.model}`);
421
+ }
422
+ if (task.url !== undefined) {
423
+ writeOutput(`url: ${task.url}`);
424
+ }
425
+ if (task.blockers.length > 0) {
426
+ writeOutput(`blockers: ${task.blockers.map((blocker) => `${naturalIdFromCanonical(blocker.id)}(${blocker.status})`).join(", ")}`);
427
+ }
428
+ if (task.description.trim().length > 0) {
429
+ writeOutput("");
430
+ writeOutput(task.description);
431
+ }
432
+ }
433
+ async function taskListCli(argv) {
434
+ const options = parseListOptions(argv);
435
+ const sources = await loadTaskSources();
436
+ const selectedSources = options.sourceName === undefined ? sources : [findSource(sources, options.sourceName)];
437
+ const sourceTasks = await Promise.all(selectedSources.map(async (source) => await source.listTasks()));
438
+ const tasks = filterTasks(sourceTasks.flat(), options);
439
+ if (options.json) {
440
+ writeJson(tasks);
441
+ return;
442
+ }
443
+ writeTaskTable(tasks);
444
+ }
445
+ async function taskGetCli(argv) {
446
+ const options = parseGetOptions(argv);
447
+ const sources = await loadTaskSources();
448
+ const task = await resolveTask(sources, options.taskId, options.sourceName);
449
+ if (options.prompt) {
450
+ writeOutput(task.description);
451
+ return;
452
+ }
453
+ if (options.json) {
454
+ writeTaskJson(task);
455
+ return;
456
+ }
457
+ writeTaskDetails(task);
458
+ }
459
+ async function taskCreateCli(argv) {
460
+ const options = parseCreateOptions(argv);
461
+ const sources = await loadTaskSources();
462
+ const source = findSource(sources, options.sourceName);
463
+ if (source.createTask === undefined) {
464
+ throw new Error(`crew task create: source "${source.name}" does not support task creation`);
465
+ }
466
+ const created = await source.createTask(options.input);
467
+ if (options.json) {
468
+ writeTaskJson(created);
469
+ return;
470
+ }
471
+ writeOutput(created.id);
472
+ }
473
+ export async function taskCli(argv) {
474
+ const [verb, ...rest] = argv;
475
+ if (verb === "list") {
476
+ await taskListCli(rest);
477
+ return;
478
+ }
479
+ if (verb === "get") {
480
+ await taskGetCli(rest);
481
+ return;
482
+ }
483
+ if (verb === "create") {
484
+ await taskCreateCli(rest);
485
+ return;
486
+ }
487
+ throw new Error(TASK_USAGE);
488
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/linear/factory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAIL,KAAK,KAAK,IAAI,cAAc,EAG5B,KAAK,UAAU,EAChB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,EAIL,KAAK,KAAK,IAAI,WAAW,EAE1B,MAAM,YAAY,CAAC;AACpB,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,kBAAkB,CAAC;AAG1B;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,iGAAiG;IACjG,YAAY,EAAE,MAAM,CAAC;CACtB;AAmDD,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,WAAW,EACxB,UAAU,EAAE,MAAM,EAClB,WAAW,GAAE,iBAA+C,GAC3D,cAAc,CA4BhB;AAED,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,mBAAmB,EAC3B,OAAO,EAAE,cAAc,GACtB,UAAU,CAiGZ"}
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/linear/factory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAIL,KAAK,KAAK,IAAI,cAAc,EAG5B,KAAK,UAAU,EAChB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,EAIL,KAAK,KAAK,IAAI,WAAW,EAE1B,MAAM,YAAY,CAAC;AACpB,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,kBAAkB,CAAC;AAG1B;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,iGAAiG;IACjG,YAAY,EAAE,MAAM,CAAC;CACtB;AA2DD,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,WAAW,EACxB,UAAU,EAAE,MAAM,EAClB,WAAW,GAAE,iBAA+C,GAC3D,cAAc,CA4BhB;AAED,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,mBAAmB,EAC3B,OAAO,EAAE,cAAc,GACtB,UAAU,CAwHZ"}
@@ -51,6 +51,11 @@ function toCanonicalParentSkip(skip, sourceName) {
51
51
  childCount: skip.childCount,
52
52
  };
53
53
  }
54
+ function isLinearNotFoundError(error, naturalId) {
55
+ return (error instanceof Error &&
56
+ error.message.startsWith(`Task ${naturalId.toUpperCase()} `) &&
57
+ error.message.includes("not found"));
58
+ }
54
59
  export function toCanonicalIssue(linearIssue, sourceName, statusNames = DEFAULT_LINEAR_STATUS_NAMES) {
55
60
  const sourceRef = {
56
61
  uuid: linearIssue.uuid,
@@ -106,51 +111,73 @@ export function createLinearTaskSource(config, context) {
106
111
  return cachedIssueStatusUpdater;
107
112
  }
108
113
  let lastParentSkips = [];
114
+ async function listTasks() {
115
+ const state = await getBoardSource().fetch();
116
+ lastParentSkips = state.parentSkips.map((skip) => toCanonicalParentSkip(skip, sourceName));
117
+ return state.issues.map((linearIssue) => toCanonicalIssue(linearIssue, sourceName, statusNames));
118
+ }
119
+ async function getTask(naturalId) {
120
+ let resolved;
121
+ try {
122
+ resolved = await fetchResolvedIssue({
123
+ client: getClient(),
124
+ config: globalConfig,
125
+ task: naturalId,
126
+ });
127
+ }
128
+ catch (error) {
129
+ if (isLinearNotFoundError(error, naturalId)) {
130
+ return null;
131
+ }
132
+ throw error;
133
+ }
134
+ const sourceRef = {
135
+ uuid: resolved.uuid,
136
+ statusId: resolved.statusId,
137
+ teamId: resolved.teamId,
138
+ stateType: resolved.stateType,
139
+ nativeStatus: resolved.status,
140
+ };
141
+ return {
142
+ id: toCanonicalId(sourceName, naturalId),
143
+ source: sourceName,
144
+ title: resolved.title,
145
+ description: resolved.description,
146
+ status: canonicalStatusFromLinearState({
147
+ nativeStatus: resolved.status,
148
+ stateType: resolved.stateType,
149
+ statusNames,
150
+ }),
151
+ repository: resolved.repository,
152
+ model: resolved.model,
153
+ assignee: resolved.assignee,
154
+ updatedAt: resolved.updatedAt,
155
+ blockers: resolved.blockers.map((b) => toCanonicalBlocker(b, sourceName, statusNames)),
156
+ hasMoreBlockers: resolved.hasMoreBlockers,
157
+ url: resolved.url,
158
+ ...(resolved.priority === 0 ? {} : { priority: resolved.priority }),
159
+ sourceRef,
160
+ };
161
+ }
109
162
  return {
110
163
  name: sourceName,
111
164
  async verify() {
112
165
  await getBoardSource().verify();
113
166
  },
167
+ async listTasks() {
168
+ return await listTasks();
169
+ },
170
+ async getTask(naturalId) {
171
+ return await getTask(naturalId);
172
+ },
114
173
  async fetch() {
115
- const state = await getBoardSource().fetch();
116
- lastParentSkips = state.parentSkips.map((skip) => toCanonicalParentSkip(skip, sourceName));
117
- return state.issues.map((linearIssue) => toCanonicalIssue(linearIssue, sourceName, statusNames));
174
+ return await listTasks();
118
175
  },
119
176
  async fetchParentSkips() {
120
177
  return lastParentSkips;
121
178
  },
122
179
  async resolveOne(naturalId) {
123
- const resolved = await fetchResolvedIssue({
124
- client: getClient(),
125
- config: globalConfig,
126
- task: naturalId,
127
- });
128
- const sourceRef = {
129
- uuid: resolved.uuid,
130
- statusId: resolved.statusId,
131
- teamId: resolved.teamId,
132
- stateType: resolved.stateType,
133
- nativeStatus: resolved.status,
134
- };
135
- return {
136
- id: toCanonicalId(sourceName, naturalId),
137
- source: sourceName,
138
- title: resolved.title,
139
- description: resolved.description,
140
- status: canonicalStatusFromLinearState({
141
- nativeStatus: resolved.status,
142
- stateType: resolved.stateType,
143
- statusNames,
144
- }),
145
- repository: resolved.repository,
146
- model: resolved.model,
147
- assignee: "Unassigned",
148
- updatedAt: new Date().toISOString(),
149
- blockers: [],
150
- hasMoreBlockers: false,
151
- url: resolved.url,
152
- sourceRef,
153
- };
180
+ return (await getTask(naturalId)) ?? undefined;
154
181
  },
155
182
  async markInProgress(issue) {
156
183
  // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- by the Linear adapter's contract, every Issue it produces carries a LinearSourceRef in sourceRef
@@ -116,7 +116,12 @@ interface ResolvedIssue {
116
116
  stateType: string;
117
117
  status: string;
118
118
  statusId: string;
119
+ assignee: string;
120
+ updatedAt: string;
121
+ blockers: Blocker[];
122
+ hasMoreBlockers: boolean;
119
123
  url: string;
124
+ priority: number;
120
125
  }
121
126
  export interface RawLinearIssue {
122
127
  uuid: string;
@@ -130,6 +135,8 @@ export interface RawLinearIssue {
130
135
  stateName: string;
131
136
  stateType: string;
132
137
  stateId: string;
138
+ assignee: string;
139
+ updatedAt: string;
133
140
  blockers: Blocker[];
134
141
  hasMoreBlockers: boolean;
135
142
  /**
@@ -141,6 +148,8 @@ export interface RawLinearIssue {
141
148
  hasChildren: boolean;
142
149
  /** Linear `Issue.url` — direct web link to the task. */
143
150
  url: string;
151
+ /** Linear priority: 1=Urgent, 2=High, 3=Medium, 4=Low, 0=No priority. */
152
+ priority: number;
144
153
  }
145
154
  export declare function fetchBlockersForTask(arguments_: {
146
155
  client: LinearClient;