@clipboard-health/groundcrew 4.2.0 → 4.2.2

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 (70) hide show
  1. package/README.md +15 -25
  2. package/dist/commands/cleaner.d.ts +1 -1
  3. package/dist/commands/cleaner.d.ts.map +1 -1
  4. package/dist/commands/cleaner.js +4 -2
  5. package/dist/commands/dispatcher.d.ts +7 -6
  6. package/dist/commands/dispatcher.d.ts.map +1 -1
  7. package/dist/commands/dispatcher.js +56 -28
  8. package/dist/commands/doctor.d.ts.map +1 -1
  9. package/dist/commands/doctor.js +18 -22
  10. package/dist/commands/eligibility.d.ts +1 -1
  11. package/dist/commands/eligibility.d.ts.map +1 -1
  12. package/dist/commands/eligibility.js +7 -6
  13. package/dist/commands/orchestrator.d.ts.map +1 -1
  14. package/dist/commands/orchestrator.js +18 -14
  15. package/dist/commands/resumeWorkspace.d.ts.map +1 -1
  16. package/dist/commands/resumeWorkspace.js +3 -2
  17. package/dist/commands/setupWorkspace.d.ts +2 -4
  18. package/dist/commands/setupWorkspace.d.ts.map +1 -1
  19. package/dist/commands/setupWorkspace.js +27 -27
  20. package/dist/commands/status.d.ts.map +1 -1
  21. package/dist/commands/status.js +6 -3
  22. package/dist/index.d.ts +3 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2 -2
  25. package/dist/lib/adapters/linear/client.d.ts +22 -0
  26. package/dist/lib/adapters/linear/client.d.ts.map +1 -0
  27. package/dist/lib/adapters/linear/client.js +36 -0
  28. package/dist/lib/adapters/linear/factory.d.ts +24 -14
  29. package/dist/lib/adapters/linear/factory.d.ts.map +1 -1
  30. package/dist/lib/adapters/linear/factory.js +113 -46
  31. package/dist/lib/{boardSource.d.ts → adapters/linear/fetch.d.ts} +22 -74
  32. package/dist/lib/adapters/linear/fetch.d.ts.map +1 -0
  33. package/dist/lib/{boardSource.js → adapters/linear/fetch.js} +28 -136
  34. package/dist/lib/adapters/linear/index.d.ts +1 -0
  35. package/dist/lib/adapters/linear/index.d.ts.map +1 -1
  36. package/dist/lib/adapters/linear/parsing.d.ts +44 -0
  37. package/dist/lib/adapters/linear/parsing.d.ts.map +1 -0
  38. package/dist/lib/adapters/linear/parsing.js +144 -0
  39. package/dist/lib/{linearIssueStatus.d.ts → adapters/linear/writeback.d.ts} +1 -2
  40. package/dist/lib/adapters/linear/writeback.d.ts.map +1 -0
  41. package/dist/lib/{linearIssueStatus.js → adapters/linear/writeback.js} +16 -17
  42. package/dist/lib/adapters/shell/factory.d.ts +1 -1
  43. package/dist/lib/adapters/shell/factory.d.ts.map +1 -1
  44. package/dist/lib/adapters/shell/factory.js +8 -4
  45. package/dist/lib/adapters/shell/invoke.d.ts +4 -7
  46. package/dist/lib/adapters/shell/invoke.d.ts.map +1 -1
  47. package/dist/lib/adapters/shell/invoke.js +46 -75
  48. package/dist/lib/adapters/shell/schema.d.ts +10 -0
  49. package/dist/lib/adapters/shell/schema.d.ts.map +1 -1
  50. package/dist/lib/adapters/shell/schema.js +9 -5
  51. package/dist/lib/board.d.ts.map +1 -1
  52. package/dist/lib/board.js +43 -4
  53. package/dist/lib/buildSources.d.ts +11 -0
  54. package/dist/lib/buildSources.d.ts.map +1 -1
  55. package/dist/lib/buildSources.js +41 -0
  56. package/dist/lib/repositoryValidation.d.ts +13 -0
  57. package/dist/lib/repositoryValidation.d.ts.map +1 -0
  58. package/dist/lib/repositoryValidation.js +20 -0
  59. package/dist/lib/testing/canonicalFixtures.d.ts +19 -0
  60. package/dist/lib/testing/canonicalFixtures.d.ts.map +1 -0
  61. package/dist/lib/testing/canonicalFixtures.js +62 -0
  62. package/dist/lib/ticketSource.d.ts +73 -3
  63. package/dist/lib/ticketSource.d.ts.map +1 -1
  64. package/dist/lib/ticketSource.js +31 -0
  65. package/dist/lib/util.d.ts +0 -20
  66. package/dist/lib/util.d.ts.map +1 -1
  67. package/dist/lib/util.js +0 -35
  68. package/package.json +1 -1
  69. package/dist/lib/boardSource.d.ts.map +0 -1
  70. package/dist/lib/linearIssueStatus.d.ts.map +0 -1
@@ -1,19 +1,16 @@
1
1
  /**
2
- * Linear adapter — turns the viewer's GraphQL state into a `BoardState`
3
- * snapshot. Owns the GraphQL queries and shape parsing so callers consume a
4
- * typed `BoardState` instead of raw nodes.
2
+ * Linear adapter — GraphQL fetch helpers for board/issue data.
5
3
  *
6
- * There is no project / view / status configuration: the only filter is
7
- * "assigned to the API key's viewer AND carries an `agent-*` label."
8
- * State classification is driven by Linear's workflow `state.type`
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`
9
7
  * (`unstarted` | `started` | `completed` | `canceled` | `duplicate`) —
10
- * never by status name — so workspaces with renamed columns (Todo To Do,
11
- * Done Shipped, etc.) Just Work.
8
+ * never by status name — so workspaces with renamed columns (Todo -> To Do,
9
+ * Done -> Shipped, etc.) Just Work without per-team config.
12
10
  */
13
11
  import type { LinearClient } from "@linear/sdk";
14
- import { type ResolvedConfig } from "./config.ts";
15
- import { RepositoryResolutionError } from "./ticketSource.ts";
16
- export declare const AGENT_LABEL_PREFIX = "agent-";
12
+ import type { ResolvedConfig } from "../../config.ts";
13
+ import { type ModelResolution } from "./parsing.ts";
17
14
  export declare const ISSUES_PAGE_SIZE = 250;
18
15
  export interface Blocker {
19
16
  id: string;
@@ -30,6 +27,7 @@ export interface Issue {
30
27
  id: string;
31
28
  uuid: string;
32
29
  title: string;
30
+ description: string;
33
31
  status: string;
34
32
  statusId: string;
35
33
  /** Linear workflow `state.type` — the source of truth for canonical classification. */
@@ -39,33 +37,28 @@ export interface Issue {
39
37
  /**
40
38
  * `undefined` unless the ticket is in Todo with a parseable `agent-*` label
41
39
  * and a known-repo reference in its description — i.e. the dispatcher would
42
- * actually pick it up. Resolving on non-Todo statuses would just invite
43
- * tick-spam warnings on already-finished work.
40
+ * actually pick it up. Non-Todo tickets do not resolve repositories because
41
+ * that would invite tick-spam warnings on already-finished work.
44
42
  */
45
43
  repository: string | undefined;
46
- /** `undefined` whenever `repository` is the two are populated together. */
44
+ /** Parsed from the `agent-*` label when present, including non-Todo tickets for slot logs. */
47
45
  model: string | undefined;
48
46
  teamId: string;
49
47
  blockers: Blocker[];
50
48
  hasMoreBlockers: boolean;
51
49
  }
52
50
  /**
53
- * `Issue` narrowed to "this ticket is for groundcrew" produced by filtering
54
- * through `isGroundcrewIssue`. Use this type wherever downstream code reads
55
- * `model`/`repository` and the issue has already been through that filter.
51
+ * `Issue` narrowed to "this ticket is for groundcrew". Consumers operate on
52
+ * the canonical `GroundcrewIssue` from `ticketSource.ts`; this internal
53
+ * variant just shapes the adapter's local Linear type.
56
54
  */
57
55
  export type GroundcrewIssue = Issue & {
58
56
  model: string;
59
57
  repository: string;
60
58
  };
61
- export declare function isGroundcrewIssue(issue: Issue): issue is GroundcrewIssue;
62
59
  /**
63
60
  * Linear ticket that was silently dropped from `issues` because it has at
64
61
  * least one sub-issue and groundcrew works sub-issues rather than parents.
65
- * The dispatcher logs each one per tick so operators see WHY a Todo ticket
66
- * isn't being picked up instead of just "No Todo tickets to pick up." Only
67
- * Todo+agent-labelled parents qualify — non-actionable parents (e.g. Done
68
- * epics) would be noise.
69
62
  */
70
63
  export interface ParentSkip {
71
64
  id: string;
@@ -77,14 +70,8 @@ export interface BoardState {
77
70
  issues: Issue[];
78
71
  parentSkips: ParentSkip[];
79
72
  }
80
- export { RepositoryResolutionError };
81
73
  export interface BoardSource {
82
- /**
83
- * Verify the Linear API key resolves to a viewer. Run once at startup so
84
- * misconfiguration surfaces before the first tick.
85
- */
86
74
  verify(): Promise<void>;
87
- /** Fetch the current board snapshot. Paginates internally. */
88
75
  fetch(): Promise<BoardState>;
89
76
  }
90
77
  interface BoardSourceDeps {
@@ -116,16 +103,6 @@ export interface IssueRelationNode {
116
103
  export declare function modelForResolution(resolution: Exclude<ModelResolution, {
117
104
  kind: "no-label";
118
105
  }>): string;
119
- export declare function resolveTodoAgentMetadata(arguments_: {
120
- ticket: string;
121
- description: string | undefined;
122
- modelResolution: ModelResolution;
123
- config: ResolvedConfig;
124
- isTodo: boolean;
125
- }): {
126
- repository: string | undefined;
127
- model: string | undefined;
128
- };
129
106
  interface ResolvedIssue {
130
107
  uuid: string;
131
108
  title: string;
@@ -133,6 +110,9 @@ interface ResolvedIssue {
133
110
  repository: string;
134
111
  model: string;
135
112
  teamId: string;
113
+ stateType: string;
114
+ status: string;
115
+ statusId: string;
136
116
  }
137
117
  export interface RawLinearIssue {
138
118
  uuid: string;
@@ -144,7 +124,8 @@ export interface RawLinearIssue {
144
124
  }[];
145
125
  /** Linear workflow state name, e.g. "Todo", "In Review". May be "" if state was null. */
146
126
  stateName: string;
147
- stateType?: string;
127
+ stateType: string;
128
+ stateId: string;
148
129
  blockers: Blocker[];
149
130
  hasMoreBlockers: boolean;
150
131
  /**
@@ -167,40 +148,6 @@ export declare function fetchRawLinearIssue(arguments_: {
167
148
  export declare function fetchInProgressIssueCount(arguments_: {
168
149
  client: LinearClient;
169
150
  }): Promise<number>;
170
- export type RepositoryResolution = {
171
- kind: "ok";
172
- repository: string;
173
- } | {
174
- kind: "missing";
175
- };
176
- export declare function resolveRepositoryFor(arguments_: {
177
- description: string | undefined;
178
- config: ResolvedConfig;
179
- ticket: string;
180
- }): RepositoryResolution;
181
- export type ModelResolution = {
182
- kind: "matched";
183
- model: string;
184
- } | {
185
- kind: "no-label";
186
- } | {
187
- kind: "agent-any";
188
- } | {
189
- kind: "disabled-fallback";
190
- requestedModel: string;
191
- fallbackModel: string;
192
- };
193
- export declare function resolveModelFor(arguments_: {
194
- labels: {
195
- name: string;
196
- }[];
197
- config: ResolvedConfig;
198
- }): ModelResolution;
199
- /**
200
- * `agent-any` collapses to `models.default` here — manual setup doesn't run
201
- * the usage-gated `any` resolver, so the caller gets a concrete model name
202
- * instead of a sentinel that downstream code can't interpret.
203
- */
204
151
  export declare function fetchResolvedIssue(arguments_: {
205
152
  client: LinearClient;
206
153
  config: ResolvedConfig;
@@ -208,4 +155,5 @@ export declare function fetchResolvedIssue(arguments_: {
208
155
  }): Promise<ResolvedIssue>;
209
156
  export declare function warnIfDisabledFallback(ticket: string, modelResolution: ModelResolution, config: ResolvedConfig): void;
210
157
  export declare function blockersFromRelations(relations: IssueRelationNode[]): Blocker[];
211
- //# sourceMappingURL=boardSource.d.ts.map
158
+ export {};
159
+ //# sourceMappingURL=fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/linear/fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGtD,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,cAAc,CAAC;AAEtB,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAYpC,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B;;;;OAIG;IACH,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,uFAAuF;IACvF,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,8FAA8F;IAC9F,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;CAC9B;AAED,UAAU,eAAe;IACvB,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,GAAG,WAAW,CAUpE;AAkBD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,WAAW,CAAC,GAAG,OAAO,CAE1E;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,WAAW,CAAC,GAAG,OAAO,CAEpE;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAE1E;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,WAAW,CAAC,GAAG,OAAO,CAEjF;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAEpE;AAwBD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE;QACN,UAAU,EAAE,MAAM,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAC;KAChD,GAAG,IAAI,CAAC;CACV;AAmFD,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,OAAO,CAAC,eAAe,EAAE;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC,GACzD,MAAM,CAQR;AAiGD,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAKD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC3B,yFAAyF;IACzF,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,eAAe,EAAE,OAAO,CAAC;IACzB;;;;;OAKG;IACH,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,wBAAsB,sBAAsB,CAAC,UAAU,EAAE;IACvD,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC,SAAS,OAAO,EAAE,CAAC,CA8C9B;AAED,wBAAsB,mBAAmB,CAAC,UAAU,EAAE;IACpD,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,cAAc,CAAC,CAiE1B;AAUD,wBAAsB,yBAAyB,CAAC,UAAU,EAAE;IAC1D,MAAM,EAAE,YAAY,CAAC;CACtB,GAAG,OAAO,CAAC,MAAM,CAAC,CA2ClB;AAED,wBAAsB,kBAAkB,CAAC,UAAU,EAAE;IACnD,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,aAAa,CAAC,CAiCzB;AAED,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,eAAe,EAChC,MAAM,EAAE,cAAc,GACrB,IAAI,CAON;AAED,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,iBAAiB,EAAE,GAAG,OAAO,EAAE,CAS/E"}
@@ -1,19 +1,16 @@
1
1
  /**
2
- * Linear adapter — turns the viewer's GraphQL state into a `BoardState`
3
- * snapshot. Owns the GraphQL queries and shape parsing so callers consume a
4
- * typed `BoardState` instead of raw nodes.
2
+ * Linear adapter — GraphQL fetch helpers for board/issue data.
5
3
  *
6
- * There is no project / view / status configuration: the only filter is
7
- * "assigned to the API key's viewer AND carries an `agent-*` label."
8
- * State classification is driven by Linear's workflow `state.type`
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`
9
7
  * (`unstarted` | `started` | `completed` | `canceled` | `duplicate`) —
10
- * never by status name — so workspaces with renamed columns (Todo To Do,
11
- * Done Shipped, etc.) Just Work.
8
+ * never by status name — so workspaces with renamed columns (Todo -> To Do,
9
+ * Done -> Shipped, etc.) Just Work without per-team config.
12
10
  */
13
- import { AGENT_ANY_MODEL, isShippedDefaultDisabled } from "./config.js";
14
- import { RepositoryResolutionError } from "./ticketSource.js";
15
- import { log } from "./util.js";
16
- export const AGENT_LABEL_PREFIX = "agent-";
11
+ import { RepositoryResolutionError } from "../../ticketSource.js";
12
+ import { log } from "../../util.js";
13
+ import { AGENT_LABEL_PREFIX, resolveModelFor, resolveRepositoryFor, } from "./parsing.js";
17
14
  export const ISSUES_PAGE_SIZE = 250;
18
15
  // `state.type` values surfaced by `fetch()`. `backlog` / `triage` are dropped
19
16
  // at the GraphQL filter; everything else is post-classified by these names.
@@ -24,14 +21,6 @@ const ACTIONABLE_STATE_TYPES = [
24
21
  "canceled",
25
22
  "duplicate",
26
23
  ];
27
- export function isGroundcrewIssue(issue) {
28
- return issue.model !== undefined && issue.repository !== undefined;
29
- }
30
- // Canonical RepositoryResolutionError lives in ./ticketSource.ts (imported at
31
- // the top of this file). Re-exported here so existing consumers of
32
- // boardSource.ts keep compiling until a follow-up PR completes the consumer
33
- // refactor and deletes this file.
34
- export { RepositoryResolutionError };
35
24
  export function createBoardSource(deps) {
36
25
  const { config, client } = deps;
37
26
  return {
@@ -75,18 +64,6 @@ export function isTerminalStatusForBlocker(blocker) {
75
64
  async function fetchBoard(client, config) {
76
65
  const nodes = [];
77
66
  let after = null;
78
- // Three server-side filters narrow the response to tickets the orchestrator
79
- // can actually act on:
80
- // 1. Assignee: the API key's own viewer. groundcrew is a single-user
81
- // orchestrator — every ticket it dispatches is "this user's work."
82
- // 2. Label: at least one `agent-*` label — i.e. the user opted the
83
- // ticket in to groundcrew. Without this, every human-owned ticket
84
- // would round-trip back just to be filtered out client-side.
85
- // 3. State type: scoped to actionable values (`unstarted`, `started`,
86
- // `completed`, `canceled`, `duplicate`) so backlog/triage tickets never
87
- // make it into the page.
88
- // The client-side `isGroundcrewIssue` guard in dispatcher.ts is
89
- // belt-and-suspenders against query drift, not the load-bearing filter.
90
67
  const stateTypes = [...ACTIONABLE_STATE_TYPES];
91
68
  for (;;) {
92
69
  // oxlint-disable-next-line no-await-in-loop -- pagination cursor depends on the previous response
@@ -163,19 +140,23 @@ export function modelForResolution(resolution) {
163
140
  if (resolution.kind === "disabled-fallback") {
164
141
  return resolution.fallbackModel;
165
142
  }
166
- return AGENT_ANY_MODEL;
143
+ return "any";
167
144
  }
168
- export function resolveTodoAgentMetadata(arguments_) {
145
+ function resolveAgentMetadata(arguments_) {
169
146
  const { ticket, description, modelResolution, config, isTodo } = arguments_;
170
147
  let repository;
171
148
  let model;
172
- if (modelResolution.kind !== "no-label" && isTodo) {
173
- const resolution = resolveRepositoryFor({ description, config, ticket });
149
+ if (modelResolution.kind === "no-label") {
150
+ return { repository, model };
151
+ }
152
+ model = modelForResolution(modelResolution);
153
+ if (isTodo) {
154
+ const resolution = resolveRepositoryFor({ description, config });
174
155
  if (resolution.kind === "ok") {
175
156
  ({ repository } = resolution);
176
- model = modelForResolution(modelResolution);
177
157
  }
178
158
  else {
159
+ model = undefined;
179
160
  log(`WARNING: ${ticket} has an ${AGENT_LABEL_PREFIX}* label but no known repository in its description; skipping dispatch. Add one of workspace.knownRepositories to the description, or remove the ${AGENT_LABEL_PREFIX}* label: ${config.workspace.knownRepositories.join(", ")}`);
180
161
  }
181
162
  }
@@ -186,6 +167,7 @@ function buildLinearIssue(input) {
186
167
  id: input.identifier.toLowerCase(),
187
168
  uuid: input.uuid,
188
169
  title: input.title,
170
+ description: input.description,
189
171
  status: input.status,
190
172
  statusId: input.statusId,
191
173
  stateType: input.stateType,
@@ -202,12 +184,7 @@ function buildLinearIssue(input) {
202
184
  function issueFromNode(node, config) {
203
185
  const modelResolution = resolveModelFor({ labels: node.labels.nodes, config });
204
186
  warnIfDisabledFallback(node.identifier, modelResolution, config);
205
- // Only the dispatcher reads `Issue.repository` / `Issue.model`, and only on
206
- // tickets in the Todo column it's about to pick up. Resolving them for In
207
- // Progress (already running) or Done (cleaner only needs the id) would just
208
- // invite tick-spam warnings on already-finished tickets — e.g. when a
209
- // description was edited or knownRepositories changed after dispatch.
210
- const { repository, model } = resolveTodoAgentMetadata({
187
+ const { repository, model } = resolveAgentMetadata({
211
188
  ticket: node.identifier,
212
189
  /* v8 ignore next @preserve -- BoardIssues query selects description; the ?? guard normalises a null vs undefined edge */
213
190
  description: node.description ?? undefined,
@@ -219,6 +196,8 @@ function issueFromNode(node, config) {
219
196
  identifier: node.identifier,
220
197
  uuid: node.id,
221
198
  title: node.title,
199
+ /* v8 ignore next @preserve -- BoardIssues query always selects description; this `?? ""` is a defensive null vs undefined edge */
200
+ description: node.description ?? "",
222
201
  /* v8 ignore next @preserve -- BoardIssues query always returns state */
223
202
  status: node.state?.name ?? "Unknown",
224
203
  /* v8 ignore next @preserve -- BoardIssues query always returns state */
@@ -233,23 +212,6 @@ function issueFromNode(node, config) {
233
212
  inverseRelations: node.inverseRelations,
234
213
  });
235
214
  }
236
- function escapeRegex(value) {
237
- return value.replaceAll(/[$()*+.?[\\\]^{|}]/g, String.raw `\$&`);
238
- }
239
- // Sort by descending length so longer names match first — `api-admin`
240
- // must beat `api` when both are configured. `\b` treats `-` as a word
241
- // boundary, so without this ordering `api` would win on `api-admin`.
242
- function buildRepositoryRegex(config) {
243
- const candidates = config.workspace.knownRepositories.flatMap((repo) => {
244
- const slashIndex = repo.indexOf("/");
245
- return slashIndex === -1 ? [repo] : [repo, repo.slice(slashIndex + 1)];
246
- });
247
- const alternation = candidates
248
- .toSorted((a, b) => b.length - a.length)
249
- .map(escapeRegex)
250
- .join("|");
251
- return new RegExp(String.raw `\b(${alternation})\b`);
252
- }
253
215
  const ISSUE_LABEL_PAGE_SIZE = 50;
254
216
  const ISSUE_RELATION_PAGE_SIZE = 50;
255
217
  export async function fetchBlockersForTicket(arguments_) {
@@ -294,7 +256,7 @@ export async function fetchRawLinearIssue(arguments_) {
294
256
  title
295
257
  description
296
258
  team { id }
297
- state { name type }
259
+ state { id name type }
298
260
  children { nodes { id } }
299
261
  labels(first: ${ISSUE_LABEL_PAGE_SIZE}) {
300
262
  nodes { name }
@@ -328,6 +290,8 @@ export async function fetchRawLinearIssue(arguments_) {
328
290
  stateName: issue.state?.name ?? "",
329
291
  /* v8 ignore next @preserve -- ResolveIssue query selects state; null only if Linear genuinely returns a stateless ticket */
330
292
  stateType: issue.state?.type ?? "",
293
+ /* v8 ignore next @preserve -- ResolveIssue query selects state; null only if Linear genuinely returns a stateless ticket */
294
+ stateId: issue.state?.id ?? "",
331
295
  blockers: blockersFromRelations(issue.inverseRelations?.nodes ?? []),
332
296
  hasMoreBlockers: issue.inverseRelations?.pageInfo.hasNextPage ?? false,
333
297
  hasChildren: (issue.children?.nodes.length ?? 0) > 0,
@@ -374,53 +338,6 @@ export async function fetchInProgressIssueCount(arguments_) {
374
338
  after = page.pageInfo.endCursor;
375
339
  }
376
340
  }
377
- export function resolveRepositoryFor(arguments_) {
378
- const { description, config } = arguments_;
379
- if (description === undefined || description.length === 0) {
380
- return { kind: "missing" };
381
- }
382
- const match = buildRepositoryRegex(config).exec(description)?.[1];
383
- if (match === undefined) {
384
- return { kind: "missing" };
385
- }
386
- // `buildRepositoryRegex` matches both the full `owner/repo` entry and its bare
387
- // suffix, so the captured value can be either form. Downstream code composes
388
- // the resolved value with `workspace.projectDir` and needs the exact
389
- // `knownRepositories` entry, so resolve back to that form here.
390
- const candidates = config.workspace.knownRepositories.filter((entry) => entry === match || entry.endsWith(`/${match}`));
391
- if (candidates.length !== 1) {
392
- return { kind: "missing" };
393
- }
394
- const [canonical] = candidates;
395
- /* v8 ignore next 3 @preserve -- candidates.length === 1 guarantees [0] is defined */
396
- if (canonical === undefined) {
397
- return { kind: "missing" };
398
- }
399
- return { kind: "ok", repository: canonical };
400
- }
401
- export function resolveModelFor(arguments_) {
402
- const { labels, config } = arguments_;
403
- const parsed = parseAgentLabels(labels, config);
404
- if (parsed === undefined) {
405
- return { kind: "no-label" };
406
- }
407
- if (parsed.model === AGENT_ANY_MODEL) {
408
- return { kind: "agent-any" };
409
- }
410
- if (parsed.disabledFallback !== undefined) {
411
- return {
412
- kind: "disabled-fallback",
413
- requestedModel: parsed.disabledFallback,
414
- fallbackModel: parsed.model,
415
- };
416
- }
417
- return { kind: "matched", model: parsed.model };
418
- }
419
- /**
420
- * `agent-any` collapses to `models.default` here — manual setup doesn't run
421
- * the usage-gated `any` resolver, so the caller gets a concrete model name
422
- * instead of a sentinel that downstream code can't interpret.
423
- */
424
341
  export async function fetchResolvedIssue(arguments_) {
425
342
  const { client, config, ticket } = arguments_;
426
343
  const upper = ticket.toUpperCase();
@@ -428,7 +345,6 @@ export async function fetchResolvedIssue(arguments_) {
428
345
  const repositoryResolution = resolveRepositoryFor({
429
346
  description: raw.description,
430
347
  config,
431
- ticket: upper,
432
348
  });
433
349
  if (repositoryResolution.kind === "missing") {
434
350
  throw new RepositoryResolutionError({
@@ -452,35 +368,11 @@ export async function fetchResolvedIssue(arguments_) {
452
368
  repository: repositoryResolution.repository,
453
369
  model,
454
370
  teamId: raw.teamId,
371
+ stateType: raw.stateType,
372
+ status: raw.stateName,
373
+ statusId: raw.stateId,
455
374
  };
456
375
  }
457
- function parseAgentLabels(labels, config) {
458
- const agentLabels = labels.filter((label) => label.name.startsWith(AGENT_LABEL_PREFIX));
459
- if (agentLabels.length === 0) {
460
- return undefined;
461
- }
462
- let disabledFallback;
463
- for (const label of agentLabels) {
464
- const name = label.name.slice(AGENT_LABEL_PREFIX.length);
465
- if (name === AGENT_ANY_MODEL) {
466
- return { model: AGENT_ANY_MODEL };
467
- }
468
- // Own-property check, not `in`: a label like `agent-toString` or
469
- // `agent-__proto__` would otherwise resolve through the prototype chain
470
- // instead of falling back to `models.default`.
471
- if (Object.hasOwn(config.models.definitions, name)) {
472
- return { model: name };
473
- }
474
- if (disabledFallback === undefined && isShippedDefaultDisabled(config, name)) {
475
- disabledFallback = name;
476
- }
477
- }
478
- const fallback = { model: config.models.default };
479
- if (disabledFallback !== undefined) {
480
- fallback.disabledFallback = disabledFallback;
481
- }
482
- return fallback;
483
- }
484
376
  export function warnIfDisabledFallback(ticket, modelResolution, config) {
485
377
  if (modelResolution.kind !== "disabled-fallback") {
486
378
  return;
@@ -2,4 +2,5 @@ import type { AdapterDefinition } from "../../adapterDefinition.ts";
2
2
  import { linearAdapterConfigSchema } from "./schema.ts";
3
3
  declare const definition: AdapterDefinition<typeof linearAdapterConfigSchema>;
4
4
  export default definition;
5
+ export type { LinearSourceRef } from "./factory.ts";
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/linear/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAGpE,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAExD,QAAA,MAAM,UAAU,EAAE,iBAAiB,CAAC,OAAO,yBAAyB,CAInE,CAAC;eAEa,UAAU"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/linear/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAGpE,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAExD,QAAA,MAAM,UAAU,EAAE,iBAAiB,CAAC,OAAO,yBAAyB,CAInE,CAAC;eAEa,UAAU;AAEzB,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Linear adapter — parsing helpers for model/repository resolution from
3
+ * issue labels and descriptions. Extracted from boardSource.ts (Task 10).
4
+ */
5
+ import { type ResolvedConfig } from "../../config.ts";
6
+ export declare const AGENT_LABEL_PREFIX = "agent-";
7
+ export type RepositoryResolution = {
8
+ kind: "ok";
9
+ repository: string;
10
+ } | {
11
+ kind: "missing";
12
+ };
13
+ export type ModelResolution = {
14
+ kind: "matched";
15
+ model: string;
16
+ } | {
17
+ kind: "no-label";
18
+ } | {
19
+ kind: "agent-any";
20
+ } | {
21
+ kind: "disabled-fallback";
22
+ requestedModel: string;
23
+ fallbackModel: string;
24
+ };
25
+ export declare function buildRepositoryRegex(config: ResolvedConfig): RegExp;
26
+ export declare function resolveRepositoryFor(arguments_: {
27
+ description: string | undefined;
28
+ config: ResolvedConfig;
29
+ }): RepositoryResolution;
30
+ interface ParseRepositoryArguments {
31
+ description: string | undefined;
32
+ config: ResolvedConfig;
33
+ repositoryRegex: RegExp;
34
+ ticket: string;
35
+ }
36
+ export declare function parseRepository(arguments_: ParseRepositoryArguments): string;
37
+ export declare function resolveModelFor(arguments_: {
38
+ labels: {
39
+ name: string;
40
+ }[];
41
+ config: ResolvedConfig;
42
+ }): ModelResolution;
43
+ export {};
44
+ //# sourceMappingURL=parsing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parsing.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/linear/parsing.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAA6C,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGjG,eAAO,MAAM,kBAAkB,WAAW,CAAC;AAE3C,MAAM,MAAM,oBAAoB,GAAG;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAE5F,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GACpB;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,mBAAmB,CAAC;IAAC,cAAc,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAAC;AASjF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAUnE;AAiDD,wBAAgB,oBAAoB,CAAC,UAAU,EAAE;IAC/C,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,MAAM,EAAE,cAAc,CAAC;CACxB,GAAG,oBAAoB,CAuBvB;AAED,UAAU,wBAAwB;IAChC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,MAAM,EAAE,cAAc,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,eAAe,CAAC,UAAU,EAAE,wBAAwB,GAAG,MAAM,CA2B5E;AAkDD,wBAAgB,eAAe,CAAC,UAAU,EAAE;IAC1C,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC3B,MAAM,EAAE,cAAc,CAAC;CACxB,GAAG,eAAe,CAiBlB"}
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Linear adapter — parsing helpers for model/repository resolution from
3
+ * issue labels and descriptions. Extracted from boardSource.ts (Task 10).
4
+ */
5
+ import { AGENT_ANY_MODEL, isShippedDefaultDisabled } from "../../config.js";
6
+ import { RepositoryResolutionError } from "../../ticketSource.js";
7
+ export const AGENT_LABEL_PREFIX = "agent-";
8
+ function escapeRegex(value) {
9
+ return value.replaceAll(/[$()*+.?[\\\]^{|}]/g, String.raw `\$&`);
10
+ }
11
+ // Sort by descending length so longer names match first — `api-admin`
12
+ // must beat `api` when both are configured. `\b` treats `-` as a word
13
+ // boundary, so without this ordering `api` would win on `api-admin`.
14
+ export function buildRepositoryRegex(config) {
15
+ const candidates = config.workspace.knownRepositories.flatMap((repo) => {
16
+ const slashIndex = repo.indexOf("/");
17
+ return slashIndex === -1 ? [repo] : [repo, repo.slice(slashIndex + 1)];
18
+ });
19
+ const alternation = candidates
20
+ .toSorted((a, b) => b.length - a.length)
21
+ .map(escapeRegex)
22
+ .join("|");
23
+ return new RegExp(String.raw `\b(${alternation})\b`);
24
+ }
25
+ function canonicalizeRepositoryMatch(description, config, repositoryRegex) {
26
+ if (description === undefined || description.length === 0) {
27
+ return { kind: "missing" };
28
+ }
29
+ // Guard against an empty knownRepositories config: buildRepositoryRegex
30
+ // would produce /\b()\b/, which matches the empty string at any word
31
+ // boundary and returns a bogus "" match. Treat that as "no repo could
32
+ // be resolved" so neither the dispatch path nor the doctor path emits
33
+ // a spurious empty-string repository.
34
+ if (config.workspace.knownRepositories.length === 0) {
35
+ return { kind: "missing" };
36
+ }
37
+ const matched = repositoryRegex.exec(description)?.[1];
38
+ if (matched === undefined) {
39
+ return { kind: "missing" };
40
+ }
41
+ const candidates = config.workspace.knownRepositories.filter((r) => r === matched || r.endsWith(`/${matched}`));
42
+ if (candidates.length > 1) {
43
+ return { kind: "ambiguous" };
44
+ }
45
+ if (candidates.length === 1) {
46
+ /* v8 ignore next @preserve -- length-1 guarantees [0] defined */
47
+ // oxlint-disable-next-line typescript/no-non-null-assertion -- length-1 guarantees [0] is defined
48
+ return { kind: "canonical", repository: candidates[0] };
49
+ }
50
+ return { kind: "unknown", repository: matched };
51
+ }
52
+ export function resolveRepositoryFor(arguments_) {
53
+ const { description, config } = arguments_;
54
+ const match = canonicalizeRepositoryMatch(description, config, buildRepositoryRegex(config));
55
+ switch (match.kind) {
56
+ case "missing":
57
+ case "ambiguous": {
58
+ // Ambiguous matches surface as "missing" so fetchResolvedIssue throws
59
+ // RepositoryResolutionError — same conflation parseRepository uses,
60
+ // and the right call for single-ticket flows: the launcher can't
61
+ // disambiguate "matched N known repos" any more than the dispatcher can.
62
+ return { kind: "missing" };
63
+ }
64
+ case "canonical":
65
+ case "unknown": {
66
+ return { kind: "ok", repository: match.repository };
67
+ }
68
+ /* v8 ignore next 5 @preserve -- exhaustive over CanonicalizedRepositoryMatch.kind */
69
+ default: {
70
+ throw new Error(`resolveRepositoryFor: unexpected match kind ${match.kind}`);
71
+ }
72
+ }
73
+ }
74
+ export function parseRepository(arguments_) {
75
+ const { description, config, repositoryRegex, ticket } = arguments_;
76
+ const match = canonicalizeRepositoryMatch(description, config, repositoryRegex);
77
+ switch (match.kind) {
78
+ case "missing":
79
+ case "ambiguous": {
80
+ throw new RepositoryResolutionError({
81
+ ticket,
82
+ repositories: config.workspace.knownRepositories,
83
+ });
84
+ }
85
+ case "canonical": {
86
+ return match.repository;
87
+ }
88
+ case "unknown": {
89
+ // No match in knownRepositories — return the asserted name as-is. The
90
+ // dispatcher's dispatchableRepository helper WARN-logs and skips at
91
+ // the host layer, uniformly across all sources.
92
+ return match.repository;
93
+ }
94
+ /* v8 ignore next 5 @preserve -- exhaustive over CanonicalizedRepositoryMatch.kind */
95
+ default: {
96
+ throw new Error(`parseRepository: unexpected match kind ${match.kind}`);
97
+ }
98
+ }
99
+ }
100
+ function parseAgentLabels(labels, config) {
101
+ const agentLabels = labels.filter((label) => label.name.startsWith(AGENT_LABEL_PREFIX));
102
+ if (agentLabels.length === 0) {
103
+ return undefined;
104
+ }
105
+ let disabledFallback;
106
+ for (const label of agentLabels) {
107
+ const name = label.name.slice(AGENT_LABEL_PREFIX.length);
108
+ if (name === AGENT_ANY_MODEL) {
109
+ return { model: AGENT_ANY_MODEL };
110
+ }
111
+ // Own-property check, not `in`: a label like `agent-toString` or
112
+ // `agent-__proto__` would otherwise resolve through the prototype chain
113
+ // instead of falling back to `models.default`.
114
+ if (Object.hasOwn(config.models.definitions, name)) {
115
+ return { model: name };
116
+ }
117
+ if (disabledFallback === undefined && isShippedDefaultDisabled(config, name)) {
118
+ disabledFallback = name;
119
+ }
120
+ }
121
+ const fallback = { model: config.models.default };
122
+ if (disabledFallback !== undefined) {
123
+ fallback.disabledFallback = disabledFallback;
124
+ }
125
+ return fallback;
126
+ }
127
+ export function resolveModelFor(arguments_) {
128
+ const { labels, config } = arguments_;
129
+ const parsed = parseAgentLabels(labels, config);
130
+ if (parsed === undefined) {
131
+ return { kind: "no-label" };
132
+ }
133
+ if (parsed.model === AGENT_ANY_MODEL) {
134
+ return { kind: "agent-any" };
135
+ }
136
+ if (parsed.disabledFallback !== undefined) {
137
+ return {
138
+ kind: "disabled-fallback",
139
+ requestedModel: parsed.disabledFallback,
140
+ fallbackModel: parsed.model,
141
+ };
142
+ }
143
+ return { kind: "matched", model: parsed.model };
144
+ }
@@ -6,10 +6,9 @@ interface LinearIssueReference {
6
6
  }
7
7
  interface LinearIssueStatusUpdater {
8
8
  markInProgress(issue: LinearIssueReference): Promise<void>;
9
- resetMissingInProgressCache(): void;
10
9
  }
11
10
  export declare function createLinearIssueStatusUpdater(arguments_: {
12
11
  client: LinearClient;
13
12
  }): LinearIssueStatusUpdater;
14
13
  export {};
15
- //# sourceMappingURL=linearIssueStatus.d.ts.map
14
+ //# sourceMappingURL=writeback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writeback.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/linear/writeback.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIhD,UAAU,oBAAoB;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,wBAAwB;IAChC,cAAc,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5D;AAED,wBAAgB,8BAA8B,CAAC,UAAU,EAAE;IACzD,MAAM,EAAE,YAAY,CAAC;CACtB,GAAG,wBAAwB,CAgD3B"}