@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
@@ -13,31 +13,22 @@
13
13
  */
14
14
  import { spawn } from "node:child_process";
15
15
  import { log } from "../../util.js";
16
- export const SHELL_COMMAND_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
17
- const TIMEOUT_SIGNAL = "SIGKILL";
16
+ /**
17
+ * Hard cap on captured stdout/stderr per stream. Misbehaving scripts that
18
+ * `yes | head -c <huge>` would otherwise exhaust memory. 10 MB is enough for
19
+ * any realistic JSON ticket payload; tests can override via InvokeArgs.maxOutputBytes
20
+ * to exercise the truncation path with a smaller fixture.
21
+ */
22
+ const DEFAULT_MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
18
23
  export class ShellAdapterTimeoutError extends Error {
19
24
  constructor(arguments_) {
20
25
  super(`Shell command timed out after ${arguments_.timeoutMs}ms: ${arguments_.command}`);
21
26
  this.name = "ShellAdapterTimeoutError";
22
27
  }
23
28
  }
24
- export class ShellAdapterOutputLimitError extends Error {
25
- constructor(arguments_) {
26
- super(`Shell command exceeded combined stdout/stderr maxBuffer of ${arguments_.maxBytes} bytes: ${arguments_.command}`);
27
- this.name = "ShellAdapterOutputLimitError";
28
- }
29
- }
30
29
  function shellQuote(value) {
31
30
  return `'${value.replaceAll("'", String.raw `'\''`)}'`;
32
31
  }
33
- function killChildProcess(child, signal, shouldUseProcessGroup) {
34
- /* v8 ignore next 4 @preserve -- fallback path is for Windows or a spawn failure before pid assignment */
35
- if (!shouldUseProcessGroup || child.pid === undefined) {
36
- child.kill(signal);
37
- return;
38
- }
39
- process.kill(-child.pid, signal);
40
- }
41
32
  export function applySubstitutions(command, subs) {
42
33
  let result = command;
43
34
  for (const [key, value] of Object.entries(subs)) {
@@ -49,92 +40,72 @@ export async function invokeShellCommand(args) {
49
40
  const command = args.substitutions === undefined
50
41
  ? args.command
51
42
  : applySubstitutions(args.command, args.substitutions);
43
+ const maxBytes = args.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
52
44
  return await new Promise((resolve, reject) => {
53
- const shouldUseProcessGroup = process.platform !== "win32";
54
45
  const child = spawn("sh", ["-c", command], {
55
46
  cwd: args.cwd,
56
- detached: shouldUseProcessGroup,
57
47
  // oxlint-disable-next-line node/no-process-env -- subprocess inherits the parent's full env by design; user-supplied vars layer on top
58
48
  env: { ...process.env, ...args.env },
59
49
  stdio: ["pipe", "pipe", "pipe"],
60
50
  });
61
- const stdoutChunks = [];
62
- const stderrChunks = [];
63
- let stdoutLength = 0;
64
- let stderrLength = 0;
51
+ let stdout = Buffer.alloc(0);
52
+ let stderr = Buffer.alloc(0);
53
+ let truncated = false;
65
54
  let settled = false;
66
- function cleanup() {
67
- clearTimeout(timer);
68
- }
69
- function killChild(signal) {
70
- try {
71
- killChildProcess(child, signal, shouldUseProcessGroup);
72
- }
73
- catch {
74
- // The child may have exited between timeout/output-limit handling and the kill request.
75
- }
76
- }
77
- function failAndKill(error) {
78
- /* v8 ignore next 3 @preserve -- timeout/output-limit races can call this after another terminal event; deterministic tests cover the first terminal path */
55
+ const timer = setTimeout(() => {
56
+ /* v8 ignore next 3 @preserve -- timer/close race: clearTimeout in the close handler should prevent this branch, but the guard is kept as defense-in-depth */
79
57
  if (settled) {
80
58
  return;
81
59
  }
82
60
  settled = true;
83
- cleanup();
84
- killChild(TIMEOUT_SIGNAL);
85
- reject(error);
86
- }
87
- function appendOutput(input) {
88
- /* v8 ignore next 3 @preserve -- streams may emit after timeout/output-limit settlement; this race guard is intentionally defensive */
89
- if (settled) {
90
- return input.currentLength;
61
+ child.kill("SIGKILL");
62
+ reject(new ShellAdapterTimeoutError({ command, timeoutMs: args.timeoutMs }));
63
+ }, args.timeoutMs);
64
+ // Buffer accumulators so the byte cap matches its name. A string-based
65
+ // comparison would measure UTF-16 code units against a byte budget,
66
+ // letting multibyte UTF-8 sneak past the cap and risking a mid-
67
+ // surrogate-pair slice on truncation.
68
+ const appendCapped = (current, chunk) => {
69
+ if (current.byteLength >= maxBytes) {
70
+ truncated = true;
71
+ return current;
91
72
  }
92
- const nextCombinedLength = stdoutLength + stderrLength + input.chunk.length;
93
- if (nextCombinedLength > SHELL_COMMAND_MAX_BUFFER_BYTES) {
94
- failAndKill(new ShellAdapterOutputLimitError({
95
- command,
96
- maxBytes: SHELL_COMMAND_MAX_BUFFER_BYTES,
97
- }));
98
- return input.currentLength;
73
+ const next = Buffer.concat([current, chunk]);
74
+ if (next.byteLength <= maxBytes) {
75
+ return next;
99
76
  }
100
- input.chunks.push(input.chunk);
101
- return input.currentLength + input.chunk.length;
102
- }
103
- const timer = setTimeout(() => {
104
- failAndKill(new ShellAdapterTimeoutError({ command, timeoutMs: args.timeoutMs }));
105
- }, args.timeoutMs);
77
+ truncated = true;
78
+ const clipped = next.subarray(0, maxBytes);
79
+ return Buffer.concat([
80
+ clipped,
81
+ Buffer.from(`\n[truncated: stream exceeded ${maxBytes} bytes]`),
82
+ ]);
83
+ };
106
84
  child.stdout.on("data", (chunk) => {
107
- stdoutLength = appendOutput({
108
- chunks: stdoutChunks,
109
- currentLength: stdoutLength,
110
- chunk,
111
- });
85
+ stdout = appendCapped(stdout, chunk);
112
86
  });
113
87
  child.stderr.on("data", (chunk) => {
114
- stderrLength = appendOutput({
115
- chunks: stderrChunks,
116
- currentLength: stderrLength,
117
- chunk,
118
- });
88
+ stderr = appendCapped(stderr, chunk);
119
89
  });
120
90
  child.on("close", (code) => {
91
+ /* v8 ignore next 3 @preserve -- timer/close race: when the timeout fires first it SIGKILLs and sets settled=true; the 'close' event still arrives and must no-op. No deterministic test exists — an orphaned grandchild (`sh -c "sleep N; ..."`) keeps the stdout pipe open, so 'close' doesn't arrive until the real timeout elapses; mirrors the ignored timer/error settle guards above. */
121
92
  if (settled) {
122
93
  return;
123
94
  }
124
95
  settled = true;
125
- cleanup();
126
- const stdout = Buffer.concat(stdoutChunks, stdoutLength).toString("utf8");
127
- const stderr = Buffer.concat(stderrChunks, stderrLength).toString("utf8");
128
- if (stderr.length > 0) {
129
- log(`[shell:${args.sourceName}] ${command}\n${stderr.trimEnd()}`);
96
+ clearTimeout(timer);
97
+ const stderrText = stderr.toString("utf8");
98
+ if (stderrText.length > 0) {
99
+ log(`[shell:${args.sourceName}] ${command}\n${stderrText.trimEnd()}`);
130
100
  }
131
- /* v8 ignore next @preserve -- `code` is null only when the process was killed by signal; timeout/output-limit paths settle before 'close' */
101
+ /* v8 ignore next @preserve -- `code` is null only when the process was killed by signal; the timeout path SIGKILLs but settles via the timer rather than 'close' */
132
102
  const exitCode = code ?? 1;
103
+ const stdoutText = stdout.toString("utf8");
133
104
  if (exitCode === 0 || exitCode === 3) {
134
- resolve({ stdout, stderr, exitCode });
105
+ resolve({ stdout: stdoutText, stderr: stderrText, exitCode, truncated });
135
106
  return;
136
107
  }
137
- reject(new Error(`Shell command for source "${args.sourceName}" failed with exit ${exitCode}: ${stderr.trim().length > 0 ? stderr.trim() : command}`));
108
+ reject(new Error(`Shell command for source "${args.sourceName}" failed with exit ${exitCode}: ${stderrText.trim().length > 0 ? stderrText.trim() : command}`));
138
109
  });
139
110
  /* v8 ignore next 8 @preserve -- spawn 'error' event fires only on exec failures (PATH miss, EACCES) which are hard to simulate in tests without polluting host PATH */
140
111
  child.on("error", (error) => {
@@ -142,7 +113,7 @@ export async function invokeShellCommand(args) {
142
113
  return;
143
114
  }
144
115
  settled = true;
145
- cleanup();
116
+ clearTimeout(timer);
146
117
  reject(error);
147
118
  });
148
119
  if (args.stdin !== undefined) {
@@ -34,6 +34,11 @@ export declare const shellIssueSchema: z.ZodObject<{
34
34
  other: "other";
35
35
  todo: "todo";
36
36
  }>;
37
+ statusReason: z.ZodOptional<z.ZodEnum<{
38
+ missing: "missing";
39
+ unmapped: "unmapped";
40
+ }>>;
41
+ nativeStatus: z.ZodOptional<z.ZodString>;
37
42
  }, z.core.$strip>>;
38
43
  hasMoreBlockers: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
39
44
  sourceRef: z.ZodUnknown;
@@ -64,6 +69,11 @@ export declare const shellFetchOutputSchema: z.ZodArray<z.ZodObject<{
64
69
  other: "other";
65
70
  todo: "todo";
66
71
  }>;
72
+ statusReason: z.ZodOptional<z.ZodEnum<{
73
+ missing: "missing";
74
+ unmapped: "unmapped";
75
+ }>>;
76
+ nativeStatus: z.ZodOptional<z.ZodString>;
67
77
  }, z.core.$strip>>;
68
78
  hasMoreBlockers: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
69
79
  sourceRef: z.ZodUnknown;
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/shell/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAWxB,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAY3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAA4B,CAAC;AAEhE,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;iBAqBnC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/shell/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAYxB,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAY3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAA4B,CAAC;AAEhE,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;iBAwBnC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC"}
@@ -10,11 +10,12 @@
10
10
  */
11
11
  import { z } from "zod";
12
12
  const canonicalStatusSchema = z.enum(["todo", "in-progress", "in-review", "done", "other"]);
13
- const timeoutSchema = z.number().int().positive();
14
13
  const shellBlockerSchema = z.object({
15
14
  id: z.string(),
16
15
  title: z.string(),
17
16
  status: canonicalStatusSchema,
17
+ statusReason: z.enum(["missing", "unmapped"]).optional(),
18
+ nativeStatus: z.string().optional(),
18
19
  });
19
20
  export const shellIssueSchema = z.object({
20
21
  id: z.string(),
@@ -44,10 +45,13 @@ export const shellAdapterConfigSchema = z.object({
44
45
  cwd: z.string().optional(),
45
46
  timeouts: z
46
47
  .object({
47
- verify: timeoutSchema.optional(),
48
- fetch: timeoutSchema.optional(),
49
- resolveOne: timeoutSchema.optional(),
50
- markInProgress: timeoutSchema.optional(),
48
+ // Per-method timeout in milliseconds. Must be a positive integer —
49
+ // zero, negative, and fractional values would either deadlock or
50
+ // misbehave inside setTimeout.
51
+ verify: z.number().int().positive().optional(),
52
+ fetch: z.number().int().positive().optional(),
53
+ resolveOne: z.number().int().positive().optional(),
54
+ markInProgress: z.number().int().positive().optional(),
51
55
  })
52
56
  .optional(),
53
57
  env: z.record(z.string(), z.string()).optional(),
@@ -1 +1 @@
1
- {"version":3,"file":"board.d.ts","sourceRoot":"","sources":["../../src/lib/board.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,KAAK,EACV,KAAK,YAAY,EAClB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,WAAW,KAAK;IACpB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;IAC7B;;;OAGG;IACH,UAAU,CAAC,oBAAoB,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC;IACrE,wFAAwF;IACxF,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7C;AAcD,wBAAgB,WAAW,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,KAAK,CAqEnE"}
1
+ {"version":3,"file":"board.d.ts","sourceRoot":"","sources":["../../src/lib/board.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,KAAK,EAEV,KAAK,YAAY,EAClB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,WAAW,KAAK;IACpB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;IAC7B;;;OAGG;IACH,UAAU,CAAC,oBAAoB,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC;IACrE,wFAAwF;IACxF,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7C;AAqBD,wBAAgB,WAAW,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,KAAK,CAwGnE"}
package/dist/lib/board.js CHANGED
@@ -11,6 +11,12 @@ async function callVerify(source) {
11
11
  async function callFetch(source) {
12
12
  return await source.fetch();
13
13
  }
14
+ async function callFetchParentSkips(source) {
15
+ if (source.fetchParentSkips !== undefined) {
16
+ return await source.fetchParentSkips();
17
+ }
18
+ return [];
19
+ }
14
20
  async function callResolveOne(source, naturalId) {
15
21
  return await source.resolveOne(naturalId);
16
22
  }
@@ -38,8 +44,21 @@ export function createBoard(sources) {
38
44
  }
39
45
  },
40
46
  async fetch() {
41
- const issuesPerSource = await Promise.all(sources.map(callFetch));
42
- return { timestamp: new Date().toISOString(), issues: issuesPerSource.flat() };
47
+ // Per-source serialization: each source's callFetch must complete
48
+ // before its callFetchParentSkips so adapters that cache parent skips
49
+ // as a side effect of fetch() (e.g. Linear, which stores them on
50
+ // `lastParentSkips`) don't serve stale or empty data. Outer Promise.all
51
+ // keeps cross-source fan-out concurrent.
52
+ const perSource = await Promise.all(sources.map(async (source) => {
53
+ const issues = await callFetch(source);
54
+ const parentSkips = await callFetchParentSkips(source);
55
+ return { issues, parentSkips };
56
+ }));
57
+ return {
58
+ timestamp: new Date().toISOString(),
59
+ issues: perSource.flatMap((entry) => entry.issues),
60
+ parentSkips: perSource.flatMap((entry) => entry.parentSkips),
61
+ };
43
62
  },
44
63
  async resolveOne(idArgument) {
45
64
  const colonIndex = idArgument.indexOf(":");
@@ -52,9 +71,29 @@ export function createBoard(sources) {
52
71
  }
53
72
  return await callResolveOne(source, naturalId);
54
73
  }
55
- const results = await Promise.all(sources.map(async (s) => await callResolveOne(s, idArgument)));
56
- const matches = results.filter((r) => r !== undefined);
74
+ // Per-source resolveOne errors must not poison sibling resolutions.
75
+ // A source that rejects on a natural-id lookup is treated as "I don't
76
+ // have this ticket" (or "I can't say"). If any source resolved we use
77
+ // it; only when none resolved AND at least one rejected do we surface
78
+ // the rejection — so the user sees a real Linear/network error when
79
+ // there's no fallback, but a stray "not found" from one source doesn't
80
+ // mask a successful match from another.
81
+ const results = await Promise.allSettled(sources.map(async (s) => await callResolveOne(s, idArgument)));
82
+ const matches = [];
83
+ const rejections = [];
84
+ for (const result of results) {
85
+ if (result.status === "rejected") {
86
+ rejections.push(result.reason);
87
+ continue;
88
+ }
89
+ if (result.value !== undefined) {
90
+ matches.push(result.value);
91
+ }
92
+ }
57
93
  if (matches.length === 0) {
94
+ if (rejections.length > 0) {
95
+ throw rejections[0];
96
+ }
58
97
  return undefined;
59
98
  }
60
99
  if (matches.length === 1) {
@@ -6,6 +6,7 @@
6
6
  * directory-scanned `adapterRegistry`.
7
7
  */
8
8
  import type { AdapterContext, AdapterDefinition } from "./adapterDefinition.ts";
9
+ import type { ResolvedConfig } from "./config.ts";
9
10
  import type { TicketSource } from "./ticketSource.ts";
10
11
  /**
11
12
  * Production entry point. Awaits the directory-scanned registry, then dispatches.
@@ -16,4 +17,14 @@ export declare function buildSources(rawConfigs: readonly unknown[], context: Ad
16
17
  * import side effects.
17
18
  */
18
19
  export declare function buildSourcesWith(registry: Record<string, AdapterDefinition>, rawConfigs: readonly unknown[], context: AdapterContext): TicketSource[];
20
+ /**
21
+ * Build the runtime source list from a ResolvedConfig: synthesizes the
22
+ * implicit Linear source (Linear is always active under the post-#110
23
+ * model — viewer + agent-* label filtering happens at the GraphQL layer)
24
+ * and appends any user-declared `sources`. The implicit source is omitted
25
+ * when the user already declared a Linear source (by `kind` or by runtime
26
+ * name "linear") so they can override its `name` / construction without
27
+ * spawning a duplicate adapter.
28
+ */
29
+ export declare function sourcesFromConfig(config: ResolvedConfig): readonly unknown[];
19
30
  //# sourceMappingURL=buildSources.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"buildSources.d.ts","sourceRoot":"","sources":["../../src/lib/buildSources.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAItD;;GAEG;AACH,wBAAsB,YAAY,CAChC,UAAU,EAAE,SAAS,OAAO,EAAE,EAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,YAAY,EAAE,CAAC,CAGzB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,EAC3C,UAAU,EAAE,SAAS,OAAO,EAAE,EAC9B,OAAO,EAAE,cAAc,GACtB,YAAY,EAAE,CAchB"}
1
+ {"version":3,"file":"buildSources.d.ts","sourceRoot":"","sources":["../../src/lib/buildSources.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAItD;;GAEG;AACH,wBAAsB,YAAY,CAChC,UAAU,EAAE,SAAS,OAAO,EAAE,EAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,YAAY,EAAE,CAAC,CAGzB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,EAC3C,UAAU,EAAE,SAAS,OAAO,EAAE,EAC9B,OAAO,EAAE,cAAc,GACtB,YAAY,EAAE,CAchB;AA6BD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,OAAO,EAAE,CAM5E"}
@@ -32,3 +32,44 @@ export function buildSourcesWith(registry, rawConfigs, context) {
32
32
  return adapter.create(config, context);
33
33
  });
34
34
  }
35
+ const sourceShape = z.looseObject({
36
+ name: z.string().optional(),
37
+ kind: z.string().optional(),
38
+ });
39
+ /**
40
+ * True when `raw` is an explicitly-declared Linear source. Matches either a
41
+ * `kind: "linear"` entry — regardless of any `name` override — or any entry
42
+ * whose resolved runtime name (explicit `name`, else `kind`) is "linear".
43
+ * The latter catches a non-Linear adapter the user named "linear", which
44
+ * would otherwise collide with the implicit Linear source.
45
+ *
46
+ * Used to suppress the synthesized implicit Linear source so a renamed Linear
47
+ * entry like `{ kind: "linear", name: "custom" }` doesn't spawn a duplicate
48
+ * adapter pointed at the same viewer. Returns false for malformed entries
49
+ * (no `kind`/`name`) — those get rejected by the per-adapter Zod schema
50
+ * downstream.
51
+ */
52
+ function isExplicitLinearSource(raw) {
53
+ const parsed = sourceShape.safeParse(raw);
54
+ /* v8 ignore next 3 @preserve -- looseObject() with all-optional fields only fails to parse non-object inputs (null, primitives); the same input would be rejected by the per-adapter Zod schema in buildSourcesWith, so this guard never fires in practice. */
55
+ if (!parsed.success) {
56
+ return false;
57
+ }
58
+ return parsed.data.kind === "linear" || (parsed.data.name ?? parsed.data.kind) === "linear";
59
+ }
60
+ /**
61
+ * Build the runtime source list from a ResolvedConfig: synthesizes the
62
+ * implicit Linear source (Linear is always active under the post-#110
63
+ * model — viewer + agent-* label filtering happens at the GraphQL layer)
64
+ * and appends any user-declared `sources`. The implicit source is omitted
65
+ * when the user already declared a Linear source (by `kind` or by runtime
66
+ * name "linear") so they can override its `name` / construction without
67
+ * spawning a duplicate adapter.
68
+ */
69
+ export function sourcesFromConfig(config) {
70
+ const hasExplicitLinear = config.sources.some(isExplicitLinearSource);
71
+ if (hasExplicitLinear) {
72
+ return [...config.sources];
73
+ }
74
+ return [{ kind: "linear" }, ...config.sources];
75
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Host-level repository validation for canonical Issues. Adapters produce
3
+ * Issue.repository based on their own signal (Linear: agent-* label parse;
4
+ * shell: script JSON output). The host (dispatcher) decides whether that
5
+ * repository is configured for this crew via workspace.knownRepositories.
6
+ *
7
+ * WARN+skip on unknown repo is a deliberate behavior choice (P-refined in
8
+ * the MVP-2 plan): one badly-labelled ticket should not throw and abort
9
+ * the tick across N sources.
10
+ */
11
+ import type { Issue } from "./ticketSource.ts";
12
+ export declare function dispatchableRepository(issue: Issue, knownRepositories: readonly string[], log: (message: string) => void): string | undefined;
13
+ //# sourceMappingURL=repositoryValidation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repositoryValidation.d.ts","sourceRoot":"","sources":["../../src/lib/repositoryValidation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE/C,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,KAAK,EACZ,iBAAiB,EAAE,SAAS,MAAM,EAAE,EACpC,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAC7B,MAAM,GAAG,SAAS,CAWpB"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Host-level repository validation for canonical Issues. Adapters produce
3
+ * Issue.repository based on their own signal (Linear: agent-* label parse;
4
+ * shell: script JSON output). The host (dispatcher) decides whether that
5
+ * repository is configured for this crew via workspace.knownRepositories.
6
+ *
7
+ * WARN+skip on unknown repo is a deliberate behavior choice (P-refined in
8
+ * the MVP-2 plan): one badly-labelled ticket should not throw and abort
9
+ * the tick across N sources.
10
+ */
11
+ export function dispatchableRepository(issue, knownRepositories, log) {
12
+ if (issue.repository === undefined) {
13
+ return undefined;
14
+ }
15
+ if (!knownRepositories.includes(issue.repository)) {
16
+ log(`issue ${issue.id} references unknown repository ${issue.repository}; configured workspace.knownRepositories: ${knownRepositories.join(", ") || "(none)"}`);
17
+ return undefined;
18
+ }
19
+ return issue.repository;
20
+ }
@@ -0,0 +1,19 @@
1
+ import { type Blocker, type Issue } from "../ticketSource.ts";
2
+ export declare function canonicalLinearIssue(overrides: Partial<Issue> & {
3
+ naturalId: string;
4
+ }): Issue;
5
+ export declare function canonicalBlocker(overrides: Partial<Blocker> & {
6
+ naturalId: string;
7
+ }): Blocker;
8
+ /**
9
+ * Canonical Issue fixture for a non-Linear source. Default source name is
10
+ * "shell-test"; override via `sourceName`. Mirrors `canonicalLinearIssue`'s
11
+ * defaults except `sourceRef` is an empty opaque object (no LinearSourceRef
12
+ * shape, since this is meant to stand in for any non-Linear adapter — the
13
+ * shell adapter, future Jira adapter, etc.).
14
+ */
15
+ export declare function canonicalShellIssue(overrides: Partial<Issue> & {
16
+ naturalId: string;
17
+ sourceName?: string;
18
+ }): Issue;
19
+ //# sourceMappingURL=canonicalFixtures.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonicalFixtures.d.ts","sourceRoot":"","sources":["../../../src/lib/testing/canonicalFixtures.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,KAAK,EAAiB,MAAM,oBAAoB,CAAC;AAE7E,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,KAAK,CA0B7F;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAQ7F;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GACrE,KAAK,CAiBP"}
@@ -0,0 +1,62 @@
1
+ import { toCanonicalId } from "../ticketSource.js";
2
+ export function canonicalLinearIssue(overrides) {
3
+ const { naturalId, sourceRef: refOverride, ...rest } = overrides;
4
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- test fixture; sourceRef is opaque unknown in the Issue contract; we reinterpret it here only for fixture construction
5
+ const refPartial = refOverride ?? {};
6
+ const sourceRef = {
7
+ uuid: refPartial.uuid ?? `uuid-${naturalId}`,
8
+ statusId: refPartial.statusId ?? "statusId-default",
9
+ teamId: refPartial.teamId ?? "team-default",
10
+ stateType: refPartial.stateType ?? "unstarted",
11
+ nativeStatus: refPartial.nativeStatus ?? "Todo",
12
+ };
13
+ return {
14
+ id: toCanonicalId("linear", naturalId),
15
+ source: "linear",
16
+ title: `Title for ${naturalId}`,
17
+ description: "",
18
+ status: "todo",
19
+ repository: undefined,
20
+ model: undefined,
21
+ assignee: "Unassigned",
22
+ updatedAt: "2026-01-01T00:00:00.000Z",
23
+ blockers: [],
24
+ hasMoreBlockers: false,
25
+ ...rest,
26
+ sourceRef,
27
+ };
28
+ }
29
+ export function canonicalBlocker(overrides) {
30
+ const { naturalId, ...rest } = overrides;
31
+ return {
32
+ id: toCanonicalId("linear", naturalId),
33
+ title: `Title for ${naturalId}`,
34
+ status: "todo",
35
+ ...rest,
36
+ };
37
+ }
38
+ /**
39
+ * Canonical Issue fixture for a non-Linear source. Default source name is
40
+ * "shell-test"; override via `sourceName`. Mirrors `canonicalLinearIssue`'s
41
+ * defaults except `sourceRef` is an empty opaque object (no LinearSourceRef
42
+ * shape, since this is meant to stand in for any non-Linear adapter — the
43
+ * shell adapter, future Jira adapter, etc.).
44
+ */
45
+ export function canonicalShellIssue(overrides) {
46
+ const { naturalId, sourceName = "shell-test", sourceRef, ...rest } = overrides;
47
+ return {
48
+ id: toCanonicalId(sourceName, naturalId),
49
+ source: sourceName,
50
+ title: `Title for ${naturalId}`,
51
+ description: "",
52
+ status: "todo",
53
+ repository: undefined,
54
+ model: undefined,
55
+ assignee: "Unassigned",
56
+ updatedAt: "2026-01-01T00:00:00.000Z",
57
+ blockers: [],
58
+ hasMoreBlockers: false,
59
+ sourceRef: sourceRef ?? {},
60
+ ...rest,
61
+ };
62
+ }
@@ -26,9 +26,33 @@ export interface Blocker {
26
26
  id: string;
27
27
  title: string;
28
28
  status: CanonicalStatus;
29
+ /**
30
+ * When `status === "other"`, adapters MUST set this to explain why
31
+ * they couldn't classify. Consumers (specifically `ticketDoctor`) render
32
+ * this verbatim to give users an actionable next step.
33
+ *
34
+ * - `"missing"`: the source returned no status for this blocker
35
+ * (e.g., Linear had no state on the blocker; shell script omitted
36
+ * the field).
37
+ * - `"unmapped"`: the source returned a status that isn't in the
38
+ * source's known mapping (e.g., a Linear column not in
39
+ * `linear.projects[*].statuses`, or an unrecognized shell value).
40
+ *
41
+ * MUST be undefined when `status !== "other"`.
42
+ */
43
+ statusReason?: "missing" | "unmapped";
44
+ /**
45
+ * Human-readable native status from the source, when available.
46
+ * Used for diagnostic display only — never branched on. Adapters SHOULD
47
+ * populate this when `statusReason === "unmapped"` so users can see
48
+ * which status name to add to their config; MAY populate for mapped
49
+ * statuses too if the source's native vocabulary differs usefully
50
+ * from `CanonicalStatus`.
51
+ */
52
+ nativeStatus?: string;
29
53
  }
30
54
  export interface Issue {
31
- /** Canonical, source-prefixed id, e.g. "linear:eng-220" or "shell-jira:HRD-1". */
55
+ /** Canonical, source-prefixed id, e.g. "linear:eng-220" or "shell-jira:hrd-1". */
32
56
  id: string;
33
57
  /** Source name (the adapter's `name`, defaulting to its `kind`). */
34
58
  source: string;
@@ -52,9 +76,26 @@ export type GroundcrewIssue = Issue & {
52
76
  repository: string;
53
77
  };
54
78
  export declare function isGroundcrewIssue(issue: Issue): issue is GroundcrewIssue;
79
+ /**
80
+ * A parent ticket that was dropped from the fetch result because it has
81
+ * sub-issues. Surfaced separately so the dispatcher can log WHY a
82
+ * Todo+labelled ticket wasn't picked up (PR #80 behavior).
83
+ */
84
+ export interface ParentSkip {
85
+ /**
86
+ * Canonical, source-prefixed id, e.g. "linear:eng-220". Matches the form
87
+ * used by `Issue.id` so consumers can treat all ids uniformly and strip
88
+ * the prefix with `naturalIdFromCanonical` when displaying to operators.
89
+ */
90
+ id: string;
91
+ title: string;
92
+ childCount: number;
93
+ }
55
94
  export interface BoardState {
56
95
  timestamp: string;
57
96
  issues: Issue[];
97
+ /** Parent tickets skipped because they have sub-issues. */
98
+ parentSkips: readonly ParentSkip[];
58
99
  }
59
100
  export interface TicketSource {
60
101
  /** Stable identifier used as the id prefix and in log lines. Equal to the source's config `name`. */
@@ -67,6 +108,13 @@ export interface TicketSource {
67
108
  resolveOne(naturalId: string): Promise<Issue | undefined>;
68
109
  /** Writeback. The adapter downcasts `issue.sourceRef` internally. */
69
110
  markInProgress(issue: Issue): Promise<void>;
111
+ /**
112
+ * Optional: return parent tickets that were excluded from `fetch()` because
113
+ * they have sub-issues. Board surfaces these so the dispatcher can log WHY
114
+ * a Todo+labelled ticket was skipped (PR #80 behavior). Adapters that
115
+ * don't distinguish parents simply omit this method; Board returns [].
116
+ */
117
+ fetchParentSkips?(): Promise<readonly ParentSkip[]>;
70
118
  }
71
119
  export declare class RepositoryResolutionError extends Error {
72
120
  constructor(arguments_: {
@@ -80,4 +128,26 @@ export declare class AmbiguousTicketError extends Error {
80
128
  matches: readonly string[];
81
129
  });
82
130
  }
131
+ /**
132
+ * Build a canonical source-prefixed id from a source name and a natural
133
+ * (possibly mixed-case) id. Lower-cases the natural part so the same
134
+ * ticket always produces the same canonical id regardless of which code
135
+ * path or adapter constructed it.
136
+ *
137
+ * All adapters MUST use this helper when constructing canonical ids
138
+ * (rather than concatenating `${sourceName}:${naturalId}` inline) so
139
+ * that `Board.resolveOne` lookups against lower-cased natural-id input
140
+ * find the issue regardless of the casing the source emitted.
141
+ */
142
+ export declare function toCanonicalId(sourceName: string, naturalId: string): string;
143
+ /**
144
+ * Strip the source prefix from a canonical id, yielding the natural id
145
+ * the producing adapter exposed. Use at consumer boundaries where you
146
+ * need to compare a canonical id against natural-id artifacts like
147
+ * `WorktreeEntry.ticket` or filesystem directory names.
148
+ *
149
+ * Canonical ids always carry a `<source>:` prefix; the no-colon branch
150
+ * is a defensive fallback that's unreachable in normal operation.
151
+ */
152
+ export declare function naturalIdFromCanonical(id: string): string;
83
153
  //# sourceMappingURL=ticketSource.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ticketSource.d.ts","sourceRoot":"","sources":["../../src/lib/ticketSource.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,aAAa,GAAG,WAAW,GAAG,MAAM,GAAG,OAAO,CAAC;AAEtF,MAAM,WAAW,OAAO;IACtB,6DAA6D;IAC7D,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,eAAe,CAAC;CACzB;AAED,MAAM,WAAW,KAAK;IACpB,kFAAkF;IAClF,EAAE,EAAE,MAAM,CAAC;IACX,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,eAAe,CAAC;IACxB,8FAA8F;IAC9F,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,6DAA6D;IAC7D,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,eAAe,EAAE,OAAO,CAAC;IACzB,wFAAwF;IACxF,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,mEAAmE;AACnE,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,IAAI,eAAe,CAExE;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,KAAK,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,qGAAqG;IACrG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,oFAAoF;IACpF,KAAK,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;IAC1B,0EAA0E;IAC1E,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC;IAC1D,qEAAqE;IACrE,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7C;AAED,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,YAAmB,UAAU,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,EAMjF;CACF;AAED,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,YAAmB,UAAU,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,EAM/E;CACF"}
1
+ {"version":3,"file":"ticketSource.d.ts","sourceRoot":"","sources":["../../src/lib/ticketSource.ts"],"names":[],"mappings":"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"}