@clipboard-health/groundcrew 4.2.0 → 4.2.1

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 (69) hide show
  1. package/dist/commands/cleaner.d.ts +1 -1
  2. package/dist/commands/cleaner.d.ts.map +1 -1
  3. package/dist/commands/cleaner.js +4 -2
  4. package/dist/commands/dispatcher.d.ts +6 -6
  5. package/dist/commands/dispatcher.d.ts.map +1 -1
  6. package/dist/commands/dispatcher.js +43 -27
  7. package/dist/commands/doctor.d.ts.map +1 -1
  8. package/dist/commands/doctor.js +18 -22
  9. package/dist/commands/eligibility.d.ts +1 -1
  10. package/dist/commands/eligibility.d.ts.map +1 -1
  11. package/dist/commands/eligibility.js +7 -6
  12. package/dist/commands/orchestrator.d.ts.map +1 -1
  13. package/dist/commands/orchestrator.js +18 -14
  14. package/dist/commands/resumeWorkspace.d.ts.map +1 -1
  15. package/dist/commands/resumeWorkspace.js +3 -2
  16. package/dist/commands/setupWorkspace.d.ts +2 -4
  17. package/dist/commands/setupWorkspace.d.ts.map +1 -1
  18. package/dist/commands/setupWorkspace.js +27 -27
  19. package/dist/commands/status.d.ts.map +1 -1
  20. package/dist/commands/status.js +6 -3
  21. package/dist/index.d.ts +3 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +2 -2
  24. package/dist/lib/adapters/linear/client.d.ts +22 -0
  25. package/dist/lib/adapters/linear/client.d.ts.map +1 -0
  26. package/dist/lib/adapters/linear/client.js +36 -0
  27. package/dist/lib/adapters/linear/factory.d.ts +24 -14
  28. package/dist/lib/adapters/linear/factory.d.ts.map +1 -1
  29. package/dist/lib/adapters/linear/factory.js +113 -46
  30. package/dist/lib/{boardSource.d.ts → adapters/linear/fetch.d.ts} +19 -71
  31. package/dist/lib/adapters/linear/fetch.d.ts.map +1 -0
  32. package/dist/lib/{boardSource.js → adapters/linear/fetch.js} +21 -133
  33. package/dist/lib/adapters/linear/index.d.ts +1 -0
  34. package/dist/lib/adapters/linear/index.d.ts.map +1 -1
  35. package/dist/lib/adapters/linear/parsing.d.ts +44 -0
  36. package/dist/lib/adapters/linear/parsing.d.ts.map +1 -0
  37. package/dist/lib/adapters/linear/parsing.js +144 -0
  38. package/dist/lib/{linearIssueStatus.d.ts → adapters/linear/writeback.d.ts} +1 -2
  39. package/dist/lib/adapters/linear/writeback.d.ts.map +1 -0
  40. package/dist/lib/{linearIssueStatus.js → adapters/linear/writeback.js} +16 -17
  41. package/dist/lib/adapters/shell/factory.d.ts +1 -1
  42. package/dist/lib/adapters/shell/factory.d.ts.map +1 -1
  43. package/dist/lib/adapters/shell/factory.js +8 -4
  44. package/dist/lib/adapters/shell/invoke.d.ts +4 -7
  45. package/dist/lib/adapters/shell/invoke.d.ts.map +1 -1
  46. package/dist/lib/adapters/shell/invoke.js +46 -75
  47. package/dist/lib/adapters/shell/schema.d.ts +10 -0
  48. package/dist/lib/adapters/shell/schema.d.ts.map +1 -1
  49. package/dist/lib/adapters/shell/schema.js +9 -5
  50. package/dist/lib/board.d.ts.map +1 -1
  51. package/dist/lib/board.js +43 -4
  52. package/dist/lib/buildSources.d.ts +11 -0
  53. package/dist/lib/buildSources.d.ts.map +1 -1
  54. package/dist/lib/buildSources.js +41 -0
  55. package/dist/lib/repositoryValidation.d.ts +13 -0
  56. package/dist/lib/repositoryValidation.d.ts.map +1 -0
  57. package/dist/lib/repositoryValidation.js +20 -0
  58. package/dist/lib/testing/canonicalFixtures.d.ts +19 -0
  59. package/dist/lib/testing/canonicalFixtures.d.ts.map +1 -0
  60. package/dist/lib/testing/canonicalFixtures.js +62 -0
  61. package/dist/lib/ticketSource.d.ts +71 -1
  62. package/dist/lib/ticketSource.d.ts.map +1 -1
  63. package/dist/lib/ticketSource.js +31 -0
  64. package/dist/lib/util.d.ts +0 -20
  65. package/dist/lib/util.d.ts.map +1 -1
  66. package/dist/lib/util.js +0 -35
  67. package/package.json +1 -1
  68. package/dist/lib/boardSource.d.ts.map +0 -1
  69. 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
- 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,14 +140,14 @@ 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 resolveTodoAgentMetadata(arguments_) {
169
146
  const { ticket, description, modelResolution, config, isTodo } = arguments_;
170
147
  let repository;
171
148
  let model;
172
149
  if (modelResolution.kind !== "no-label" && isTodo) {
173
- const resolution = resolveRepositoryFor({ description, config, ticket });
150
+ const resolution = resolveRepositoryFor({ description, config });
174
151
  if (resolution.kind === "ok") {
175
152
  ({ repository } = resolution);
176
153
  model = modelForResolution(modelResolution);
@@ -186,6 +163,7 @@ function buildLinearIssue(input) {
186
163
  id: input.identifier.toLowerCase(),
187
164
  uuid: input.uuid,
188
165
  title: input.title,
166
+ description: input.description,
189
167
  status: input.status,
190
168
  statusId: input.statusId,
191
169
  stateType: input.stateType,
@@ -202,11 +180,6 @@ function buildLinearIssue(input) {
202
180
  function issueFromNode(node, config) {
203
181
  const modelResolution = resolveModelFor({ labels: node.labels.nodes, config });
204
182
  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
183
  const { repository, model } = resolveTodoAgentMetadata({
211
184
  ticket: node.identifier,
212
185
  /* v8 ignore next @preserve -- BoardIssues query selects description; the ?? guard normalises a null vs undefined edge */
@@ -219,6 +192,8 @@ function issueFromNode(node, config) {
219
192
  identifier: node.identifier,
220
193
  uuid: node.id,
221
194
  title: node.title,
195
+ /* v8 ignore next @preserve -- BoardIssues query always selects description; this `?? ""` is a defensive null vs undefined edge */
196
+ description: node.description ?? "",
222
197
  /* v8 ignore next @preserve -- BoardIssues query always returns state */
223
198
  status: node.state?.name ?? "Unknown",
224
199
  /* v8 ignore next @preserve -- BoardIssues query always returns state */
@@ -233,23 +208,6 @@ function issueFromNode(node, config) {
233
208
  inverseRelations: node.inverseRelations,
234
209
  });
235
210
  }
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
211
  const ISSUE_LABEL_PAGE_SIZE = 50;
254
212
  const ISSUE_RELATION_PAGE_SIZE = 50;
255
213
  export async function fetchBlockersForTicket(arguments_) {
@@ -294,7 +252,7 @@ export async function fetchRawLinearIssue(arguments_) {
294
252
  title
295
253
  description
296
254
  team { id }
297
- state { name type }
255
+ state { id name type }
298
256
  children { nodes { id } }
299
257
  labels(first: ${ISSUE_LABEL_PAGE_SIZE}) {
300
258
  nodes { name }
@@ -328,6 +286,8 @@ export async function fetchRawLinearIssue(arguments_) {
328
286
  stateName: issue.state?.name ?? "",
329
287
  /* v8 ignore next @preserve -- ResolveIssue query selects state; null only if Linear genuinely returns a stateless ticket */
330
288
  stateType: issue.state?.type ?? "",
289
+ /* v8 ignore next @preserve -- ResolveIssue query selects state; null only if Linear genuinely returns a stateless ticket */
290
+ stateId: issue.state?.id ?? "",
331
291
  blockers: blockersFromRelations(issue.inverseRelations?.nodes ?? []),
332
292
  hasMoreBlockers: issue.inverseRelations?.pageInfo.hasNextPage ?? false,
333
293
  hasChildren: (issue.children?.nodes.length ?? 0) > 0,
@@ -374,53 +334,6 @@ export async function fetchInProgressIssueCount(arguments_) {
374
334
  after = page.pageInfo.endCursor;
375
335
  }
376
336
  }
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
337
  export async function fetchResolvedIssue(arguments_) {
425
338
  const { client, config, ticket } = arguments_;
426
339
  const upper = ticket.toUpperCase();
@@ -428,7 +341,6 @@ export async function fetchResolvedIssue(arguments_) {
428
341
  const repositoryResolution = resolveRepositoryFor({
429
342
  description: raw.description,
430
343
  config,
431
- ticket: upper,
432
344
  });
433
345
  if (repositoryResolution.kind === "missing") {
434
346
  throw new RepositoryResolutionError({
@@ -452,35 +364,11 @@ export async function fetchResolvedIssue(arguments_) {
452
364
  repository: repositoryResolution.repository,
453
365
  model,
454
366
  teamId: raw.teamId,
367
+ stateType: raw.stateType,
368
+ status: raw.stateName,
369
+ statusId: raw.stateId,
455
370
  };
456
371
  }
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
372
  export function warnIfDisabledFallback(ticket, modelResolution, config) {
485
373
  if (modelResolution.kind !== "disabled-fallback") {
486
374
  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"}
@@ -1,12 +1,18 @@
1
- import { log } from "./util.js";
1
+ import { log } from "../../util.js";
2
2
  export function createLinearIssueStatusUpdater(arguments_) {
3
3
  const { client } = arguments_;
4
- // The first matching workflow state per team is cached by teamId so the
5
- // status writeback doesn't re-query Linear on every dispatch. Teams that
6
- // genuinely lack an `started` workflow state stay in the negative cache
7
- // until the dispatcher resets it at the top of each tick.
4
+ // Positive cache only. Keyed by teamId because the workflow `state.type ===
5
+ // "started"` lookup yields a single stateId per team independent of which
6
+ // project the ticket belongs to. State ids don't change for misconfig
7
+ // reasons, so caching successful resolutions is safe across the process.
8
+ //
9
+ // No negative cache: a missing "started" workflow state is a Linear-side
10
+ // config issue the operator can correct mid-session, and a negative cache
11
+ // would mask that recovery until process restart. Slot count caps
12
+ // markInProgress calls per tick at 1-5, so re-fetching team states on
13
+ // every failing attempt costs at most a handful of extra Linear API calls
14
+ // per tick.
8
15
  const inProgressStateByTeam = new Map();
9
- let teamsMissingInProgress = new Set();
10
16
  async function getInProgressStateId(teamId) {
11
17
  if (teamId.length === 0) {
12
18
  return undefined;
@@ -15,17 +21,13 @@ export function createLinearIssueStatusUpdater(arguments_) {
15
21
  if (cached !== undefined) {
16
22
  return cached;
17
23
  }
18
- if (teamsMissingInProgress.has(teamId)) {
19
- return undefined;
20
- }
21
24
  const team = await client.team(teamId);
22
25
  const states = await team.states();
23
- // Use the workflow state's *type* — Linear standardises on
24
- // `started` for in-progress columns regardless of how the user renames
25
- // them, so this works without any per-team status-name configuration.
26
+ // Use the workflow state's `type` — Linear standardises on `started` for
27
+ // in-progress columns regardless of how the user renames them, so this
28
+ // works without any per-team status-name configuration.
26
29
  const inProgress = states.nodes.find((state) => state.type === "started");
27
30
  if (inProgress?.id === undefined) {
28
- teamsMissingInProgress.add(teamId);
29
31
  return undefined;
30
32
  }
31
33
  inProgressStateByTeam.set(teamId, inProgress.id);
@@ -39,8 +41,5 @@ export function createLinearIssueStatusUpdater(arguments_) {
39
41
  await client.updateIssue(issue.uuid, { stateId });
40
42
  log(`Marked ${issue.id} as in progress`);
41
43
  }
42
- function resetMissingInProgressCache() {
43
- teamsMissingInProgress = new Set();
44
- }
45
- return { markInProgress, resetMissingInProgressCache };
44
+ return { markInProgress };
46
45
  }
@@ -14,7 +14,7 @@
14
14
  * - `fetch` is required by the Zod schema.
15
15
  */
16
16
  import type { AdapterContext } from "../../adapterDefinition.ts";
17
- import type { Issue as CanonicalIssue, TicketSource } from "../../ticketSource.ts";
17
+ import { type Issue as CanonicalIssue, type TicketSource } from "../../ticketSource.ts";
18
18
  import { type ShellAdapterConfig, type ShellIssue } from "./schema.ts";
19
19
  export declare function toCanonicalIssue(shellIssue: ShellIssue, sourceName: string): CanonicalIssue;
20
20
  export declare function createShellTicketSource(config: ShellAdapterConfig, _context: AdapterContext): TicketSource;
@@ -1 +1 @@
1
- {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/shell/factory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAEV,KAAK,IAAI,cAAc,EACvB,YAAY,EACb,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EACL,KAAK,kBAAkB,EAEvB,KAAK,UAAU,EAEhB,MAAM,aAAa,CAAC;AAyBrB,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,cAAc,CAoB3F;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,cAAc,GACvB,YAAY,CA+Ed"}
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/shell/factory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAGL,KAAK,KAAK,IAAI,cAAc,EAC5B,KAAK,YAAY,EAClB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EACL,KAAK,kBAAkB,EAEvB,KAAK,UAAU,EAEhB,MAAM,aAAa,CAAC;AAyBrB,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,cAAc,CAsB3F;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,cAAc,GACvB,YAAY,CAgFd"}
@@ -13,6 +13,7 @@
13
13
  * - `markInProgress` absent → silent no-op.
14
14
  * - `fetch` is required by the Zod schema.
15
15
  */
16
+ import { toCanonicalId, } from "../../ticketSource.js";
16
17
  import { invokeShellCommand } from "./invoke.js";
17
18
  import { shellFetchOutputSchema, shellIssueSchema, } from "./schema.js";
18
19
  const DEFAULT_TIMEOUTS = {
@@ -31,12 +32,14 @@ function mergeTimeouts(overrides) {
31
32
  }
32
33
  export function toCanonicalIssue(shellIssue, sourceName) {
33
34
  const blockers = shellIssue.blockers.map((b) => ({
34
- id: `${sourceName}:${b.id}`,
35
+ id: toCanonicalId(sourceName, b.id),
35
36
  title: b.title,
36
37
  status: b.status,
38
+ ...(b.statusReason !== undefined && { statusReason: b.statusReason }),
39
+ ...(b.nativeStatus !== undefined && { nativeStatus: b.nativeStatus }),
37
40
  }));
38
41
  return {
39
- id: `${sourceName}:${shellIssue.id}`,
42
+ id: toCanonicalId(sourceName, shellIssue.id),
40
43
  source: sourceName,
41
44
  title: shellIssue.title,
42
45
  description: shellIssue.description,
@@ -81,10 +84,11 @@ export function createShellTicketSource(config, _context) {
81
84
  },
82
85
  fetch: runFetch,
83
86
  async resolveOne(naturalId) {
87
+ const canonicalId = toCanonicalId(sourceName, naturalId);
84
88
  const resolveCommand = config.commands.resolveOne;
85
89
  if (resolveCommand === undefined) {
86
90
  const all = await runFetch();
87
- return all.find((i) => i.id === `${sourceName}:${naturalId}`);
91
+ return all.find((i) => i.id === canonicalId);
88
92
  }
89
93
  const result = await invokeShellCommand({
90
94
  command: resolveCommand,
@@ -93,7 +97,7 @@ export function createShellTicketSource(config, _context) {
93
97
  env: config.env,
94
98
  substitutions: {
95
99
  id: naturalId,
96
- canonicalId: `${sourceName}:${naturalId}`,
100
+ canonicalId,
97
101
  name: sourceName,
98
102
  },
99
103
  sourceName,
@@ -11,19 +11,12 @@
11
11
  * Exit code 0 = success; exit code 3 = "not found" (caller decides how to
12
12
  * interpret); any other nonzero exit throws.
13
13
  */
14
- export declare const SHELL_COMMAND_MAX_BUFFER_BYTES: number;
15
14
  export declare class ShellAdapterTimeoutError extends Error {
16
15
  constructor(arguments_: {
17
16
  command: string;
18
17
  timeoutMs: number;
19
18
  });
20
19
  }
21
- export declare class ShellAdapterOutputLimitError extends Error {
22
- constructor(arguments_: {
23
- command: string;
24
- maxBytes: number;
25
- });
26
- }
27
20
  interface InvokeArgs {
28
21
  command: string;
29
22
  timeoutMs: number;
@@ -33,11 +26,15 @@ interface InvokeArgs {
33
26
  substitutions?: Record<string, string> | undefined;
34
27
  /** Source name for log prefixing. */
35
28
  sourceName: string;
29
+ /** Override the default per-stream stdout/stderr cap (10 MB). Used by tests. */
30
+ maxOutputBytes?: number;
36
31
  }
37
32
  interface InvokeResult {
38
33
  stdout: string;
39
34
  stderr: string;
40
35
  exitCode: number;
36
+ /** True if either stream hit the byte cap and the rest was discarded. */
37
+ truncated: boolean;
41
38
  }
42
39
  export declare function applySubstitutions(command: string, subs: Record<string, string>): string;
43
40
  export declare function invokeShellCommand(args: InvokeArgs): Promise<InvokeResult>;
@@ -1 +1 @@
1
- {"version":3,"file":"invoke.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/shell/invoke.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,eAAO,MAAM,8BAA8B,QAAmB,CAAC;AAG/D,qBAAa,wBAAyB,SAAQ,KAAK;IACjD,YAAmB,UAAU,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,EAGpE;CACF;AAED,qBAAa,4BAA6B,SAAQ,KAAK;IACrD,YAAmB,UAAU,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAKnE;CACF;AAED,UAAU,UAAU;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IACzC,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IACnD,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,YAAY;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAmBD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAMxF;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,CA8HhF"}
1
+ {"version":3,"file":"invoke.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/shell/invoke.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAcH,qBAAa,wBAAyB,SAAQ,KAAK;IACjD,YAAmB,UAAU,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,EAGpE;CACF;AAED,UAAU,UAAU;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IACzC,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IACnD,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,YAAY;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,SAAS,EAAE,OAAO,CAAC;CACpB;AAMD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAMxF;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,CAkGhF"}