@clipboard-health/groundcrew 4.14.0 → 4.16.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 (40) hide show
  1. package/crew.config.example.ts +14 -4
  2. package/dist/commands/orchestrator.d.ts +2 -2
  3. package/dist/commands/orchestrator.d.ts.map +1 -1
  4. package/dist/commands/orchestrator.js +9 -2
  5. package/dist/commands/reviewer.d.ts +51 -0
  6. package/dist/commands/reviewer.d.ts.map +1 -0
  7. package/dist/commands/reviewer.js +121 -0
  8. package/dist/lib/adapters/linear/factory.d.ts +5 -4
  9. package/dist/lib/adapters/linear/factory.d.ts.map +1 -1
  10. package/dist/lib/adapters/linear/factory.js +41 -58
  11. package/dist/lib/adapters/linear/fetch.d.ts +8 -9
  12. package/dist/lib/adapters/linear/fetch.d.ts.map +1 -1
  13. package/dist/lib/adapters/linear/fetch.js +5 -6
  14. package/dist/lib/adapters/linear/schema.d.ts +6 -1
  15. package/dist/lib/adapters/linear/schema.d.ts.map +1 -1
  16. package/dist/lib/adapters/linear/schema.js +9 -1
  17. package/dist/lib/adapters/linear/statusNames.d.ts +25 -0
  18. package/dist/lib/adapters/linear/statusNames.d.ts.map +1 -0
  19. package/dist/lib/adapters/linear/statusNames.js +60 -0
  20. package/dist/lib/adapters/linear/writeback.d.ts +4 -0
  21. package/dist/lib/adapters/linear/writeback.d.ts.map +1 -1
  22. package/dist/lib/adapters/linear/writeback.js +43 -10
  23. package/dist/lib/adapters/shell/factory.d.ts +1 -0
  24. package/dist/lib/adapters/shell/factory.d.ts.map +1 -1
  25. package/dist/lib/adapters/shell/factory.js +37 -19
  26. package/dist/lib/adapters/shell/schema.d.ts +2 -0
  27. package/dist/lib/adapters/shell/schema.d.ts.map +1 -1
  28. package/dist/lib/adapters/shell/schema.js +2 -0
  29. package/dist/lib/board.d.ts +11 -5
  30. package/dist/lib/board.d.ts.map +1 -1
  31. package/dist/lib/board.js +20 -9
  32. package/dist/lib/config.d.ts +3 -3
  33. package/dist/lib/config.d.ts.map +1 -1
  34. package/dist/lib/config.js +2 -1
  35. package/dist/lib/ticketSource.d.ts +23 -8
  36. package/dist/lib/ticketSource.d.ts.map +1 -1
  37. package/docs/configuration.md +21 -3
  38. package/docs/ticket-sources.md +11 -2
  39. package/docs/troubleshooting.md +1 -1
  40. package/package.json +1 -1
@@ -4,10 +4,11 @@ import type { Config } from "@clipboard-health/groundcrew";
4
4
  export default {
5
5
  // Groundcrew's built-in Linear adapter is implicit and needs no config:
6
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 / status
8
- // block Linear's workflow `state.type` (`unstarted` todo,
9
- // `started` in progress, `completed`/`canceled`/`duplicate`
10
- // terminal) is the single source of truth, so renamed columns Just Work.
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).
11
12
  //
12
13
  // Opt a ticket in: assign it to yourself and add an `agent-<model>`
13
14
  // label (e.g. `agent-claude`, `agent-any`).
@@ -53,6 +54,15 @@ export default {
53
54
  // // See the shell adapter's ShellIssue schema for the JSON contract
54
55
  // // `fetch` / `resolveOne` must emit.
55
56
  // sources: [
57
+ // // Optional: explicitly declare Linear only when you need custom status
58
+ // // names. Omitted fields keep their defaults.
59
+ // {
60
+ // kind: "linear",
61
+ // statuses: {
62
+ // inProgress: ["Doing"],
63
+ // inReview: ["Code Review"],
64
+ // },
65
+ // },
56
66
  // {
57
67
  // kind: "shell",
58
68
  // name: "jira",
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * groundcrew orchestrator — polls Linear projects and spins up workspace +
3
3
  * git-worktree pairs for ready tickets. Each tick fetches the board, runs
4
- * the cleaner, and runs the dispatcher; logging from those modules is the
5
- * orchestrator's user-facing output.
4
+ * the cleaner, the reviewer, and the dispatcher; logging from those modules is
5
+ * the orchestrator's user-facing output.
6
6
  */
7
7
  export interface OrchestratorOptions {
8
8
  watch: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../src/commands/orchestrator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAyDH,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,CA0C7E"}
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,18 +1,20 @@
1
1
  /**
2
2
  * groundcrew orchestrator — polls Linear projects and spins up workspace +
3
3
  * git-worktree pairs for ready tickets. Each tick fetches the board, runs
4
- * the cleaner, and runs the dispatcher; logging from those modules is the
5
- * orchestrator's user-facing output.
4
+ * the cleaner, the reviewer, and the dispatcher; logging from those modules is
5
+ * the orchestrator's user-facing output.
6
6
  */
7
7
  import { createBoard } from "../lib/board.js";
8
8
  import { buildSources, sourcesFromConfig } from "../lib/buildSources.js";
9
9
  import { loadConfig } from "../lib/config.js";
10
+ import { findPullRequestsForBranch } from "../lib/pullRequests.js";
10
11
  import { RepositoryResolutionError } from "../lib/ticketSource.js";
11
12
  import { getUsageByModel } from "../lib/usage.js";
12
13
  import { errorMessage, log, sleep } from "../lib/util.js";
13
14
  import { worktrees } from "../lib/worktrees.js";
14
15
  import { createCleaner } from "./cleaner.js";
15
16
  import { createDispatcher } from "./dispatcher.js";
17
+ import { createReviewer } from "./reviewer.js";
16
18
  const RATE_LIMIT_DELAY_MS = 60_000;
17
19
  const RETRY_BASE_DELAY_MS = 1000;
18
20
  const RETRY_MAX_ATTEMPTS = 3;
@@ -72,6 +74,10 @@ export async function orchestrate(options) {
72
74
  const board = createBoard(allSources);
73
75
  await board.verify();
74
76
  const cleaner = createCleaner({ config });
77
+ const reviewer = createReviewer({
78
+ board,
79
+ findPullRequests: findPullRequestsForBranch,
80
+ });
75
81
  const dispatcher = createDispatcher({ config, board });
76
82
  // Folded into the dispatcher's idle log lines in watch mode so each idle
77
83
  // tick prints one combined line instead of "<reason>" + "Next poll in Xs".
@@ -88,6 +94,7 @@ export async function orchestrate(options) {
88
94
  ...(signal === undefined ? {} : { signal }),
89
95
  };
90
96
  await cleaner.runOnce(tickArguments);
97
+ await reviewer.runOnce(tickArguments);
91
98
  await dispatcher.runOnce({
92
99
  ...tickArguments,
93
100
  // Lazy: dispatcher only invokes this after its own early-returns, so
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Per-iteration scanner that advances in-progress tickets to in-review once
3
+ * their worktree has an open (or merged) pull request. Sits between the cleaner
4
+ * and the dispatcher in each `orchestrate()` tick. A successfully-applied
5
+ * transition frees a dispatch slot (the slot math counts only in-progress)
6
+ * while leaving the worktree intact for review, since the cleaner only tears
7
+ * down `done` tickets.
8
+ *
9
+ * The write-back lands in the ticket source, not the in-memory `BoardState`, so
10
+ * the dispatcher in the SAME tick still sees the ticket as in-progress; the slot
11
+ * frees on the NEXT tick's `board.fetch()`. That one-tick latency is deliberate
12
+ * — it keeps the reviewer from mutating shared `BoardState` mid-tick. One per
13
+ * `orchestrate()`; stateless across iterations. Mirrors `Cleaner`.
14
+ *
15
+ * "Worktree has an open PR" is a v1 proxy for "the implementation is finished
16
+ * and up for review". The detection + the open/merged condition live here in
17
+ * the core reviewer (a push model) rather than inside any adapter, so a future
18
+ * per-adapter `shouldAdvanceToReview` predicate is a clean additive change.
19
+ */
20
+ import type { Board } from "../lib/board.ts";
21
+ import type { PullRequestSummary } from "../lib/pullRequests.ts";
22
+ import { type BoardState } from "../lib/ticketSource.ts";
23
+ import type { WorktreeEntry } from "../lib/worktrees.ts";
24
+ /**
25
+ * Injected PR lookup. Matches `findPullRequestsForBranch`'s shape: best-effort,
26
+ * never rejects — a failed lookup (gh missing, unauthenticated, non-GitHub
27
+ * remote, network error) resolves to an empty list, indistinguishable from
28
+ * "no PR yet". Both outcomes mean "skip this issue, retry next tick".
29
+ */
30
+ export type FindPullRequests = (arguments_: {
31
+ cwd: string;
32
+ branchName: string;
33
+ signal?: AbortSignal;
34
+ }) => Promise<readonly PullRequestSummary[]>;
35
+ interface ReviewerDeps {
36
+ board: Board;
37
+ findPullRequests: FindPullRequests;
38
+ }
39
+ /** Per-tick inputs, mirroring the other orchestrator steps' shape. */
40
+ interface ReviewArguments {
41
+ state: BoardState;
42
+ worktreeEntries: readonly WorktreeEntry[];
43
+ dryRun: boolean;
44
+ signal?: AbortSignal;
45
+ }
46
+ export interface Reviewer {
47
+ runOnce(arguments_: ReviewArguments): Promise<void>;
48
+ }
49
+ export declare function createReviewer(deps: ReviewerDeps): Reviewer;
50
+ export {};
51
+ //# sourceMappingURL=reviewer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reviewer.d.ts","sourceRoot":"","sources":["../../src/commands/reviewer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,KAAK,UAAU,EAAsC,MAAM,wBAAwB,CAAC;AAE7F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,UAAU,EAAE;IAC1C,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,KAAK,OAAO,CAAC,SAAS,kBAAkB,EAAE,CAAC,CAAC;AAE7C,UAAU,YAAY;IACpB,KAAK,EAAE,KAAK,CAAC;IACb,gBAAgB,EAAE,gBAAgB,CAAC;CACpC;AAED,sEAAsE;AACtE,UAAU,eAAe;IACvB,KAAK,EAAE,UAAU,CAAC;IAClB,eAAe,EAAE,SAAS,aAAa,EAAE,CAAC;IAC1C,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,QAAQ;IACvB,OAAO,CAAC,UAAU,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrD;AAsBD,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,QAAQ,CAqG3D"}
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Per-iteration scanner that advances in-progress tickets to in-review once
3
+ * their worktree has an open (or merged) pull request. Sits between the cleaner
4
+ * and the dispatcher in each `orchestrate()` tick. A successfully-applied
5
+ * transition frees a dispatch slot (the slot math counts only in-progress)
6
+ * while leaving the worktree intact for review, since the cleaner only tears
7
+ * down `done` tickets.
8
+ *
9
+ * The write-back lands in the ticket source, not the in-memory `BoardState`, so
10
+ * the dispatcher in the SAME tick still sees the ticket as in-progress; the slot
11
+ * frees on the NEXT tick's `board.fetch()`. That one-tick latency is deliberate
12
+ * — it keeps the reviewer from mutating shared `BoardState` mid-tick. One per
13
+ * `orchestrate()`; stateless across iterations. Mirrors `Cleaner`.
14
+ *
15
+ * "Worktree has an open PR" is a v1 proxy for "the implementation is finished
16
+ * and up for review". The detection + the open/merged condition live here in
17
+ * the core reviewer (a push model) rather than inside any adapter, so a future
18
+ * per-adapter `shouldAdvanceToReview` predicate is a clean additive change.
19
+ */
20
+ import { naturalIdFromCanonical } from "../lib/ticketSource.js";
21
+ import { debug, errorMessage, log, logEvent } from "../lib/util.js";
22
+ // A PR whose lifecycle means the work is up for (or past) review. `merged`
23
+ // catches a PR that merged between ticks, so we still free the slot.
24
+ function isReviewablePullRequest(pr) {
25
+ return pr.state === "open" || pr.state === "merged";
26
+ }
27
+ function matchingWorktreeEntries(arguments_) {
28
+ const { issue, worktreeEntries, ticket } = arguments_;
29
+ if (issue.repository === undefined) {
30
+ return [];
31
+ }
32
+ return worktreeEntries.filter((entry) => entry.ticket === ticket && entry.repository === issue.repository);
33
+ }
34
+ export function createReviewer(deps) {
35
+ const { board, findPullRequests } = deps;
36
+ async function runOnce(arguments_) {
37
+ const { state, worktreeEntries, dryRun, signal } = arguments_;
38
+ const inProgress = state.issues.filter((issue) => issue.status === "in-progress");
39
+ if (inProgress.length === 0) {
40
+ return;
41
+ }
42
+ for (const issue of inProgress) {
43
+ // oxlint-disable-next-line no-await-in-loop -- at most maximumInProgress (1-5) issues per tick; sequential keeps gh load low.
44
+ await advanceIfReviewable({
45
+ issue,
46
+ worktreeEntries,
47
+ dryRun,
48
+ ...(signal === undefined ? {} : { signal }),
49
+ });
50
+ }
51
+ }
52
+ // Idempotent after an applied transition: once advanced, the issue leaves
53
+ // `in-progress`, so it never reaches this scan again. Unsupported writebacks
54
+ // are skipped without claiming success and may retry on later ticks.
55
+ async function advanceIfReviewable(arguments_) {
56
+ const { issue, worktreeEntries, dryRun, signal } = arguments_;
57
+ const ticket = naturalIdFromCanonical(issue.id);
58
+ const entries = matchingWorktreeEntries({ issue, worktreeEntries, ticket });
59
+ for (const entry of entries) {
60
+ // The injected lookup is contracted never to reject (failures resolve to
61
+ // []), but we still guard it so one bad lookup can never abort the tick
62
+ // and starve the other in-progress issues. A failure means "can't tell
63
+ // yet" → skip this worktree and retry next tick.
64
+ let pullRequests;
65
+ try {
66
+ // oxlint-disable-next-line no-await-in-loop -- a ticket almost always has one worktree; sequential lookups are fine.
67
+ pullRequests = await findPullRequests({
68
+ cwd: entry.dir,
69
+ branchName: entry.branchName,
70
+ ...(signal === undefined ? {} : { signal }),
71
+ });
72
+ }
73
+ catch (error) {
74
+ debug(`PR lookup failed for ${ticket} (${entry.branchName}): ${errorMessage(error)}`);
75
+ continue;
76
+ }
77
+ const reviewable = pullRequests.find(isReviewablePullRequest);
78
+ if (reviewable === undefined) {
79
+ continue;
80
+ }
81
+ if (dryRun) {
82
+ log(`[dry-run] Would advance ${ticket} to in-review (PR ${reviewable.url})`);
83
+ logEvent("review", { outcome: "skipped", reason: "dry_run", ticket, pr: reviewable.url });
84
+ return;
85
+ }
86
+ // oxlint-disable-next-line no-await-in-loop -- single write-back then return; never iterates past the first reviewable worktree.
87
+ await advance({ issue, ticket, pullRequest: reviewable });
88
+ return;
89
+ }
90
+ }
91
+ // A writeback failure (shell/Linear error) is logged and swallowed: the
92
+ // ticket stays in-progress and is retried next tick, exactly like a failed
93
+ // lookup. We never let one ticket's writeback abort the others' reviews.
94
+ async function advance(arguments_) {
95
+ const { issue, ticket, pullRequest } = arguments_;
96
+ try {
97
+ const result = await board.markInReview(issue);
98
+ if (result.outcome === "unsupported") {
99
+ log(`Skipped advancing ${ticket} to in-review: ${result.reason}`);
100
+ logEvent("review", {
101
+ outcome: "skipped",
102
+ reason: "unsupported",
103
+ ticket,
104
+ });
105
+ return;
106
+ }
107
+ log(`Advanced ${ticket} to in-review (PR ${pullRequest.url})`);
108
+ logEvent("review", {
109
+ outcome: "advanced",
110
+ ticket,
111
+ pr: pullRequest.url,
112
+ state: pullRequest.state,
113
+ });
114
+ }
115
+ catch (error) {
116
+ log(`Failed to advance ${ticket} to in-review: ${errorMessage(error)}`);
117
+ logEvent("review", { outcome: "failed", reason: "writeback_failed", ticket });
118
+ }
119
+ }
120
+ return { runOnce };
121
+ }
@@ -5,9 +5,9 @@
5
5
  * ./client.ts) and converts Linear-specific shapes into the canonical
6
6
  * Issue/Blocker types consumers (via Board) speak.
7
7
  *
8
- * State classification is driven by Linear's workflow `state.type` never
9
- * by status name so workspaces with renamed columns Just Work without
10
- * per-team config.
8
+ * Status names disambiguate Linear's multiple `started` states ("In Progress"
9
+ * vs "In Review"). Unmatched statuses fall back to workflow `state.type` so
10
+ * renamed or custom columns still preserve the broad lifecycle behavior.
11
11
  *
12
12
  * Description is populated on both `fetch()` Issues and `resolveOne()` Issues.
13
13
  */
@@ -15,6 +15,7 @@ import type { AdapterContext } from "../../adapterDefinition.ts";
15
15
  import { type Issue as CanonicalIssue, type TicketSource } from "../../ticketSource.ts";
16
16
  import type { LinearAdapterConfig } from "./schema.ts";
17
17
  import { type Issue as LinearIssue } from "./fetch.ts";
18
+ import { type LinearStatusNames } from "./statusNames.ts";
18
19
  /**
19
20
  * Adapter-private payload threaded through `Issue.sourceRef`. Consumers
20
21
  * MUST NOT inspect; only the Linear adapter reads it.
@@ -28,6 +29,6 @@ export interface LinearSourceRef {
28
29
  /** Human-readable native status name, e.g. "In Progress", "Shipped". Diagnostic display only. */
29
30
  nativeStatus: string;
30
31
  }
31
- export declare function toCanonicalIssue(linearIssue: LinearIssue, sourceName: string): CanonicalIssue;
32
+ export declare function toCanonicalIssue(linearIssue: LinearIssue, sourceName: string, statusNames?: LinearStatusNames): CanonicalIssue;
32
33
  export declare function createLinearTicketSource(config: LinearAdapterConfig, context: AdapterContext): TicketSource;
33
34
  //# sourceMappingURL=factory.d.ts.map
@@ -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,EAE5B,KAAK,YAAY,EAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,EAKL,KAAK,KAAK,IAAI,WAAW,EAE1B,MAAM,YAAY,CAAC;AAGpB;;;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;AAkFD,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,GAAG,cAAc,CAuB7F;AAED,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,mBAAmB,EAC3B,OAAO,EAAE,cAAc,GACtB,YAAY,CAgFd"}
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,YAAY,EAClB,MAAM,uBAAuB,CAAC;AAC/B,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,CA2BhB;AAED,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,mBAAmB,EAC3B,OAAO,EAAE,cAAc,GACtB,YAAY,CAiGd"}
@@ -5,73 +5,37 @@
5
5
  * ./client.ts) and converts Linear-specific shapes into the canonical
6
6
  * Issue/Blocker types consumers (via Board) speak.
7
7
  *
8
- * State classification is driven by Linear's workflow `state.type` never
9
- * by status name so workspaces with renamed columns Just Work without
10
- * per-team config.
8
+ * Status names disambiguate Linear's multiple `started` states ("In Progress"
9
+ * vs "In Review"). Unmatched statuses fall back to workflow `state.type` so
10
+ * renamed or custom columns still preserve the broad lifecycle behavior.
11
11
  *
12
12
  * Description is populated on both `fetch()` Issues and `resolveOne()` Issues.
13
13
  */
14
14
  import { toCanonicalId, } from "../../ticketSource.js";
15
15
  import { getLinearClient, lazyLinearClient } from "./client.js";
16
- import { createBoardSource, fetchResolvedIssue, isTerminalStateType, } from "./fetch.js";
16
+ import { createBoardSource, fetchResolvedIssue, } from "./fetch.js";
17
+ import { canonicalStatusFromLinearState, DEFAULT_LINEAR_STATUS_NAMES, resolveLinearStatusNames, } from "./statusNames.js";
17
18
  import { createLinearIssueStatusUpdater } from "./writeback.js";
18
- function canonicalStatusFromStateType(stateType) {
19
- /* v8 ignore next 3 @preserve -- LinearIssue.stateType is non-optional; this guard is defensive for the resolveOne path */
20
- if (stateType === undefined) {
21
- return "other";
22
- }
23
- switch (stateType) {
24
- case "unstarted": {
25
- return "todo";
26
- }
27
- case "started": {
28
- return "in-progress";
29
- }
30
- case "completed":
31
- case "canceled":
32
- case "duplicate": {
33
- return "done";
34
- }
35
- default: {
36
- return "other";
37
- }
38
- }
39
- }
40
- function canonicalBlockerStatus(blocker) {
41
- if (blocker.stateType === undefined) {
42
- return {
43
- status: "other",
44
- statusReason: "missing",
45
- ...(blocker.status !== undefined && { nativeStatus: blocker.status }),
46
- };
47
- }
48
- if (isTerminalStateType(blocker.stateType)) {
49
- return {
50
- status: "done",
51
- ...(blocker.status !== undefined && { nativeStatus: blocker.status }),
52
- };
53
- }
54
- if (blocker.stateType === "started") {
19
+ function canonicalBlockerStatus(blocker, statusNames) {
20
+ const status = canonicalStatusFromLinearState({
21
+ nativeStatus: blocker.status,
22
+ stateType: blocker.stateType,
23
+ statusNames,
24
+ });
25
+ if (status !== "other") {
55
26
  return {
56
- status: "in-progress",
27
+ status,
57
28
  ...(blocker.status !== undefined && { nativeStatus: blocker.status }),
58
29
  };
59
30
  }
60
- if (blocker.stateType === "unstarted") {
61
- return {
62
- status: "todo",
63
- ...(blocker.status !== undefined && { nativeStatus: blocker.status }),
64
- };
65
- }
66
- // backlog / triage / anything else falls through as "other"
67
31
  return {
68
- status: "other",
69
- statusReason: "unmapped",
32
+ status,
33
+ statusReason: blocker.stateType === undefined ? "missing" : "unmapped",
70
34
  ...(blocker.status !== undefined && { nativeStatus: blocker.status }),
71
35
  };
72
36
  }
73
- function toCanonicalBlocker(blocker, sourceName) {
74
- const { status, statusReason, nativeStatus } = canonicalBlockerStatus(blocker);
37
+ function toCanonicalBlocker(blocker, sourceName, statusNames) {
38
+ const { status, statusReason, nativeStatus } = canonicalBlockerStatus(blocker, statusNames);
75
39
  return {
76
40
  id: toCanonicalId(sourceName, blocker.id),
77
41
  title: blocker.title,
@@ -87,7 +51,7 @@ function toCanonicalParentSkip(skip, sourceName) {
87
51
  childCount: skip.childCount,
88
52
  };
89
53
  }
90
- export function toCanonicalIssue(linearIssue, sourceName) {
54
+ export function toCanonicalIssue(linearIssue, sourceName, statusNames = DEFAULT_LINEAR_STATUS_NAMES) {
91
55
  const sourceRef = {
92
56
  uuid: linearIssue.uuid,
93
57
  statusId: linearIssue.statusId,
@@ -100,12 +64,16 @@ export function toCanonicalIssue(linearIssue, sourceName) {
100
64
  source: sourceName,
101
65
  title: linearIssue.title,
102
66
  description: linearIssue.description,
103
- status: canonicalStatusFromStateType(linearIssue.stateType),
67
+ status: canonicalStatusFromLinearState({
68
+ nativeStatus: linearIssue.status,
69
+ stateType: linearIssue.stateType,
70
+ statusNames,
71
+ }),
104
72
  repository: linearIssue.repository,
105
73
  model: linearIssue.model,
106
74
  assignee: linearIssue.assignee,
107
75
  updatedAt: linearIssue.updatedAt,
108
- blockers: linearIssue.blockers.map((b) => toCanonicalBlocker(b, sourceName)),
76
+ blockers: linearIssue.blockers.map((b) => toCanonicalBlocker(b, sourceName, statusNames)),
109
77
  hasMoreBlockers: linearIssue.hasMoreBlockers,
110
78
  url: linearIssue.url,
111
79
  sourceRef,
@@ -113,6 +81,7 @@ export function toCanonicalIssue(linearIssue, sourceName) {
113
81
  }
114
82
  export function createLinearTicketSource(config, context) {
115
83
  const sourceName = config.name ?? "linear";
84
+ const statusNames = resolveLinearStatusNames(config.statuses);
116
85
  const { globalConfig } = context;
117
86
  // Lazy: deferring `getLinearClient()` (and the sub-modules that depend on
118
87
  // it) until first method use means `createLinearTicketSource` can be
@@ -131,6 +100,7 @@ export function createLinearTicketSource(config, context) {
131
100
  function getIssueStatusUpdater() {
132
101
  cachedIssueStatusUpdater ??= createLinearIssueStatusUpdater({
133
102
  client: getClient(),
103
+ statusNames,
134
104
  });
135
105
  return cachedIssueStatusUpdater;
136
106
  }
@@ -143,7 +113,7 @@ export function createLinearTicketSource(config, context) {
143
113
  async fetch() {
144
114
  const state = await getBoardSource().fetch();
145
115
  lastParentSkips = state.parentSkips.map((skip) => toCanonicalParentSkip(skip, sourceName));
146
- return state.issues.map((linearIssue) => toCanonicalIssue(linearIssue, sourceName));
116
+ return state.issues.map((linearIssue) => toCanonicalIssue(linearIssue, sourceName, statusNames));
147
117
  },
148
118
  async fetchParentSkips() {
149
119
  return lastParentSkips;
@@ -166,7 +136,11 @@ export function createLinearTicketSource(config, context) {
166
136
  source: sourceName,
167
137
  title: resolved.title,
168
138
  description: resolved.description,
169
- status: canonicalStatusFromStateType(resolved.stateType),
139
+ status: canonicalStatusFromLinearState({
140
+ nativeStatus: resolved.status,
141
+ stateType: resolved.stateType,
142
+ statusNames,
143
+ }),
170
144
  repository: resolved.repository,
171
145
  model: resolved.model,
172
146
  assignee: "Unassigned",
@@ -186,5 +160,14 @@ export function createLinearTicketSource(config, context) {
186
160
  teamId: ref.teamId,
187
161
  });
188
162
  },
163
+ async markInReview(issue) {
164
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- by the Linear adapter's contract, every Issue it produces carries a LinearSourceRef in sourceRef
165
+ const ref = issue.sourceRef;
166
+ return await getIssueStatusUpdater().markInReview({
167
+ id: issue.id,
168
+ uuid: ref.uuid,
169
+ teamId: ref.teamId,
170
+ });
171
+ },
189
172
  };
190
173
  }
@@ -1,12 +1,11 @@
1
1
  /**
2
2
  * Linear adapter — GraphQL fetch helpers for board/issue data.
3
3
  *
4
- * There is no project / view / status configuration: the only server-side
5
- * filter is "assigned to the API key's viewer AND carries an `agent-*`
6
- * label." State classification is driven by Linear's workflow `state.type`
7
- * (`unstarted` | `started` | `completed` | `canceled` | `duplicate`) —
8
- * never by status name — so workspaces with renamed columns (Todo -> To Do,
9
- * Done -> Shipped, etc.) Just Work without per-team config.
4
+ * There is no project or view configuration: the only server-side filter is
5
+ * "assigned to the API key's viewer AND carries an `agent-*` label." This
6
+ * module returns Linear's native status name plus workflow `state.type`; the
7
+ * ticket-source factory applies status-name disambiguation and state-type
8
+ * fallback when building canonical issues.
10
9
  */
11
10
  import type { LinearClient } from "@linear/sdk";
12
11
  import type { ResolvedConfig } from "../../config.ts";
@@ -18,8 +17,8 @@ export interface Blocker {
18
17
  status: string | undefined;
19
18
  /**
20
19
  * Linear workflow `state.type` for the blocker (`unstarted` | `started` |
21
- * `completed` | `canceled` | `duplicate` | `backlog` | `triage`). All
22
- * canonical classification todo / in-progress / terminal keys off this.
20
+ * `completed` | `canceled` | `duplicate` | `backlog` | `triage`). Canonical
21
+ * classification uses this as a fallback after configured status names.
23
22
  */
24
23
  stateType: string | undefined;
25
24
  }
@@ -30,7 +29,7 @@ export interface Issue {
30
29
  description: string;
31
30
  status: string;
32
31
  statusId: string;
33
- /** Linear workflow `state.type` — the source of truth for canonical classification. */
32
+ /** Linear workflow `state.type` — canonical classification fallback after status-name matching. */
34
33
  stateType: string;
35
34
  assignee: string;
36
35
  updatedAt: string;
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/linear/fetch.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/linear/fetch.ts"],"names":[],"mappings":"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"}
@@ -1,12 +1,11 @@
1
1
  /**
2
2
  * Linear adapter — GraphQL fetch helpers for board/issue data.
3
3
  *
4
- * There is no project / view / status configuration: the only server-side
5
- * filter is "assigned to the API key's viewer AND carries an `agent-*`
6
- * label." State classification is driven by Linear's workflow `state.type`
7
- * (`unstarted` | `started` | `completed` | `canceled` | `duplicate`) —
8
- * never by status name — so workspaces with renamed columns (Todo -> To Do,
9
- * Done -> Shipped, etc.) Just Work without per-team config.
4
+ * There is no project or view configuration: the only server-side filter is
5
+ * "assigned to the API key's viewer AND carries an `agent-*` label." This
6
+ * module returns Linear's native status name plus workflow `state.type`; the
7
+ * ticket-source factory applies status-name disambiguation and state-type
8
+ * fallback when building canonical issues.
10
9
  */
11
10
  import { RepositoryResolutionError } from "../../ticketSource.js";
12
11
  import { log, styleWarning } from "../../util.js";
@@ -1,12 +1,17 @@
1
1
  /**
2
2
  * Zod schema for the Linear adapter's per-source config block. The built-in
3
3
  * Linear adapter is implicit and derives scope from the API key's viewer plus
4
- * `agent-*` labels, so the source config only needs an optional display name.
4
+ * `agent-*` labels. Source config is only needed to override display name or
5
+ * Linear status names that disambiguate multiple `started` workflow states.
5
6
  */
6
7
  import { z } from "zod";
7
8
  export declare const linearAdapterConfigSchema: z.ZodObject<{
8
9
  kind: z.ZodLiteral<"linear">;
9
10
  name: z.ZodOptional<z.ZodString>;
11
+ statuses: z.ZodOptional<z.ZodObject<{
12
+ inProgress: z.ZodOptional<z.ZodArray<z.ZodString>>;
13
+ inReview: z.ZodOptional<z.ZodArray<z.ZodString>>;
14
+ }, z.core.$strip>>;
10
15
  }, z.core.$strip>;
11
16
  export type LinearAdapterConfig = z.infer<typeof linearAdapterConfigSchema>;
12
17
  //# sourceMappingURL=schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/linear/schema.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,yBAAyB;;;iBAMpC,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/linear/schema.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,yBAAyB;;;;;;;iBAYpC,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC"}
@@ -1,13 +1,21 @@
1
1
  /**
2
2
  * Zod schema for the Linear adapter's per-source config block. The built-in
3
3
  * Linear adapter is implicit and derives scope from the API key's viewer plus
4
- * `agent-*` labels, so the source config only needs an optional display name.
4
+ * `agent-*` labels. Source config is only needed to override display name or
5
+ * Linear status names that disambiguate multiple `started` workflow states.
5
6
  */
6
7
  import { z } from "zod";
8
+ const statusNamesSchema = z.array(z.string().trim().min(1)).min(1);
7
9
  export const linearAdapterConfigSchema = z.object({
8
10
  kind: z.literal("linear"),
9
11
  name: z
10
12
  .string()
11
13
  .regex(/^[a-z][a-z0-9-]*$/, "name must be kebab-case (lowercase letters, digits, hyphens)")
12
14
  .optional(),
15
+ statuses: z
16
+ .object({
17
+ inProgress: statusNamesSchema.optional(),
18
+ inReview: statusNamesSchema.optional(),
19
+ })
20
+ .optional(),
13
21
  });
@@ -0,0 +1,25 @@
1
+ import type { CanonicalStatus } from "../../ticketSource.ts";
2
+ import type { LinearAdapterConfig } from "./schema.ts";
3
+ export interface LinearStatusNames {
4
+ inProgress: readonly string[];
5
+ inReview: readonly string[];
6
+ }
7
+ export interface LinearWorkflowState {
8
+ id: string;
9
+ name: string;
10
+ type: string;
11
+ position: number;
12
+ }
13
+ export declare const DEFAULT_LINEAR_STATUS_NAMES: {
14
+ readonly inProgress: readonly ["In Progress"];
15
+ readonly inReview: readonly ["In Review"];
16
+ };
17
+ export declare function resolveLinearStatusNames(config: LinearAdapterConfig["statuses"] | undefined): LinearStatusNames;
18
+ export declare function canonicalStatusFromLinearState(arguments_: {
19
+ nativeStatus: string | undefined;
20
+ stateType: string | undefined;
21
+ statusNames: LinearStatusNames;
22
+ }): CanonicalStatus;
23
+ export declare function findLinearWorkflowStateByName(states: readonly LinearWorkflowState[], names: readonly string[]): LinearWorkflowState | undefined;
24
+ export declare function formatLinearStatusNames(names: readonly string[]): string;
25
+ //# sourceMappingURL=statusNames.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"statusNames.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/linear/statusNames.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,eAAO,MAAM,2BAA2B;;;CAGF,CAAC;AAEvC,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,mBAAmB,CAAC,UAAU,CAAC,GAAG,SAAS,GAClD,iBAAiB,CAKnB;AAED,wBAAgB,8BAA8B,CAAC,UAAU,EAAE;IACzD,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,WAAW,EAAE,iBAAiB,CAAC;CAChC,GAAG,eAAe,CAWlB;AAED,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,SAAS,mBAAmB,EAAE,EACtC,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,mBAAmB,GAAG,SAAS,CAEjC;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAExE"}