@clipboard-health/groundcrew 4.20.2 → 4.22.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.
Files changed (35) hide show
  1. package/README.md +3 -0
  2. package/crew.config.example.ts +10 -9
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +6 -0
  5. package/dist/commands/init.js +5 -3
  6. package/dist/commands/orchestrator.d.ts.map +1 -1
  7. package/dist/commands/orchestrator.js +20 -7
  8. package/dist/commands/status.d.ts.map +1 -1
  9. package/dist/commands/status.js +2 -4
  10. package/dist/commands/task.d.ts +2 -0
  11. package/dist/commands/task.d.ts.map +1 -0
  12. package/dist/commands/task.js +488 -0
  13. package/dist/lib/adapters/linear/factory.d.ts.map +1 -1
  14. package/dist/lib/adapters/linear/factory.js +61 -34
  15. package/dist/lib/adapters/linear/fetch.d.ts +9 -0
  16. package/dist/lib/adapters/linear/fetch.d.ts.map +1 -1
  17. package/dist/lib/adapters/linear/fetch.js +11 -0
  18. package/dist/lib/adapters/shell/factory.d.ts.map +1 -1
  19. package/dist/lib/adapters/shell/factory.js +31 -22
  20. package/dist/lib/adapters/todo-txt/source.d.ts.map +1 -1
  21. package/dist/lib/adapters/todo-txt/source.js +230 -21
  22. package/dist/lib/adapters/todo-txt/writeback.d.ts +1 -0
  23. package/dist/lib/adapters/todo-txt/writeback.d.ts.map +1 -1
  24. package/dist/lib/adapters/todo-txt/writeback.js +1 -1
  25. package/dist/lib/board.d.ts +1 -1
  26. package/dist/lib/board.js +3 -3
  27. package/dist/lib/buildSources.d.ts +7 -18
  28. package/dist/lib/buildSources.d.ts.map +1 -1
  29. package/dist/lib/buildSources.js +9 -48
  30. package/dist/lib/sourceCapabilities.js +1 -1
  31. package/dist/lib/taskSource.d.ts +36 -0
  32. package/dist/lib/taskSource.d.ts.map +1 -1
  33. package/docs/commands.md +30 -0
  34. package/docs/task-sources.md +42 -5
  35. 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
@@ -2,16 +2,17 @@ import type { Config } from "@clipboard-health/groundcrew";
2
2
  // import { readFileSync } from "node:fs";
3
3
 
4
4
  export default {
5
- // Groundcrew's built-in Linear adapter is implicit and needs no config:
6
- // it picks up every Linear issue assigned to your API key's viewer that
7
- // carries an `agent-*` label. There is no project / view block. The default
8
- // Linear status names `In Progress` and `In Review` disambiguate Linear's
9
- // `started` workflow states; other statuses fall back to workflow
10
- // `state.type` (`unstarted` → todo, `started` → in progress,
11
- // `completed`/`canceled`/`duplicate` → terminal).
5
+ // Task sources at least one is required; add a `sources` array to get
6
+ // started. Two built-in options:
12
7
  //
13
- // Opt a task in: assign it to yourself and add an `agent-<model>`
14
- // label (e.g. `agent-claude`, `agent-any`).
8
+ // Zero credentials: use a local todo.txt file. No API key needed.
9
+ // sources: [{ kind: "todo-txt" }]
10
+ //
11
+ // Linear: picks up issues assigned to your API key's viewer that carry
12
+ // an `agent-*` label. Requires GROUNDCREW_LINEAR_API_KEY.
13
+ // sources: [{ kind: "linear" }]
14
+ //
15
+ // Running `crew run` without a sources array will print this guidance and exit.
15
16
  workspace: {
16
17
  // Parent directory under which groundcrew clones repositories and (by
17
18
  // default) creates per-task worktrees.
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>]",
@@ -207,9 +207,11 @@ function writeInitGuidance(destination, options) {
207
207
  writeOutput(" - Set workspace.projectDir and workspace.knownRepositories");
208
208
  }
209
209
  writeCloneGuidance(options);
210
- writeOutput(" - If using Linear, export your API key:");
211
- writeOutput(' export GROUNDCREW_LINEAR_API_KEY="lin_api_..."');
212
- writeOutput(" - In Linear, assign tasks to yourself and add an agent-* label to opt them in");
210
+ writeOutput(" - Add a task source to your config (required):");
211
+ writeOutput(" # Zero credentials — uses a local todo.txt file:");
212
+ writeOutput(' sources: [{ kind: "todo-txt" }]');
213
+ writeOutput(" # Or use Linear (requires GROUNDCREW_LINEAR_API_KEY):");
214
+ writeOutput(' sources: [{ kind: "linear" }]');
213
215
  writeOutput(" - Validate and start:");
214
216
  writeOutput(" crew doctor");
215
217
  writeOutput(" crew run --watch");
@@ -1 +1 @@
1
- {"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../src/commands/orchestrator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA2DH,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;CACjB;AAiBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgD7E"}
1
+ {"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../src/commands/orchestrator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA2DH,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;CACjB;AAiBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgE7E"}
@@ -6,11 +6,11 @@
6
6
  */
7
7
  import { createBoard } from "../lib/board.js";
8
8
  import { buildSources, sourcesFromConfig } from "../lib/buildSources.js";
9
- import { loadConfig } from "../lib/config.js";
9
+ import { loadConfigWithSource } from "../lib/config.js";
10
10
  import { findPullRequestsForBranch } from "../lib/pullRequests.js";
11
11
  import { RepositoryResolutionError } from "../lib/taskSource.js";
12
12
  import { getUsageByModel } from "../lib/usage.js";
13
- import { errorMessage, log, sleep } from "../lib/util.js";
13
+ import { errorMessage, log, sleep, writeOutput } from "../lib/util.js";
14
14
  import { worktrees } from "../lib/worktrees.js";
15
15
  import { createCleaner } from "./cleaner.js";
16
16
  import { createDispatcher } from "./dispatcher.js";
@@ -66,11 +66,24 @@ async function fetchUsageOrEmpty(config, signal) {
66
66
  }
67
67
  }
68
68
  export async function orchestrate(options) {
69
- const config = await loadConfig();
70
- // Build all sources (Linear implicit + any user-declared shell adapters).
71
- // sourcesFromConfig synthesizes the implicit linear source from the config;
72
- // this ensures the Linear adapter's markInProgress is reachable via board.
73
- const allSources = await buildSources(sourcesFromConfig(config), { globalConfig: config });
69
+ const { config, source: configSource } = await loadConfigWithSource();
70
+ const rawSources = sourcesFromConfig(config);
71
+ if (rawSources.length === 0) {
72
+ writeOutput([
73
+ "No task sources configured. Add a sources array to your config:",
74
+ "",
75
+ ` Path: ${configSource.filepath}`,
76
+ "",
77
+ " # Zero credentials — uses a local todo.txt file:",
78
+ ' sources: [{ kind: "todo-txt" }]',
79
+ "",
80
+ " # Or use Linear (requires GROUNDCREW_LINEAR_API_KEY):",
81
+ ' sources: [{ kind: "linear" }]',
82
+ ].join("\n"));
83
+ process.exitCode = 1;
84
+ return;
85
+ }
86
+ const allSources = await buildSources(rawSources, { globalConfig: config });
74
87
  const board = createBoard(allSources);
75
88
  await board.verify();
76
89
  const cleaner = createCleaner({ config });
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAIA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAcnE,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAoqBD,wBAAsB,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/F;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAI7D"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAIA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAcnE,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAkqBD,wBAAsB,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/F;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAI7D"}
@@ -434,10 +434,8 @@ async function buildBoardForStatus(config) {
434
434
  }
435
435
  /**
436
436
  * Single board fetch used by both the slot count header and the
437
- * Queue/Blocked sections. `sourcesFromConfig` prepends an implicit Linear
438
- * source when none are configured, so we always attempt; failures
439
- * (e.g., missing API key) are captured and rendered later as
440
- * `unavailable: ...` in the Queue section.
437
+ * Queue/Blocked sections. Failures (e.g., missing API key) are captured
438
+ * and rendered later as `unavailable: ...` in the Queue section.
441
439
  */
442
440
  async function fetchBoardForStatus(config) {
443
441
  try {
@@ -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"}