@clipboard-health/groundcrew 4.20.1 → 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 +3 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -0
- package/dist/commands/task.d.ts +2 -0
- package/dist/commands/task.d.ts.map +1 -0
- package/dist/commands/task.js +488 -0
- package/dist/lib/adapters/linear/factory.d.ts.map +1 -1
- package/dist/lib/adapters/linear/factory.js +61 -34
- package/dist/lib/adapters/linear/fetch.d.ts +9 -0
- package/dist/lib/adapters/linear/fetch.d.ts.map +1 -1
- package/dist/lib/adapters/linear/fetch.js +11 -0
- package/dist/lib/adapters/shell/factory.d.ts.map +1 -1
- package/dist/lib/adapters/shell/factory.js +31 -22
- package/dist/lib/adapters/todo-txt/source.d.ts.map +1 -1
- package/dist/lib/adapters/todo-txt/source.js +239 -29
- package/dist/lib/adapters/todo-txt/writeback.d.ts +1 -0
- package/dist/lib/adapters/todo-txt/writeback.d.ts.map +1 -1
- package/dist/lib/adapters/todo-txt/writeback.js +1 -1
- package/dist/lib/board.d.ts +1 -1
- package/dist/lib/board.js +3 -3
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +24 -11
- package/dist/lib/paths.d.ts +2 -0
- package/dist/lib/paths.d.ts.map +1 -0
- package/dist/lib/paths.js +11 -0
- package/dist/lib/sourceCapabilities.js +1 -1
- package/dist/lib/taskSource.d.ts +36 -0
- package/dist/lib/taskSource.d.ts.map +1 -1
- package/docs/commands.md +30 -0
- package/docs/task-sources.md +42 -5
- package/package.json +1 -1
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":"
|
|
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 @@
|
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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;
|