@enrichlayer/el-linear 1.10.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +126 -10
  2. package/claude-skills/linear-operations/SKILL.md +41 -1
  3. package/dist/auth/linear-credential.d.ts +27 -0
  4. package/dist/auth/linear-credential.js +1 -0
  5. package/dist/auth/oauth-app-config.d.ts +4 -3
  6. package/dist/auth/oauth-app-config.js +13 -2
  7. package/dist/auth/oauth-callback.d.ts +2 -3
  8. package/dist/auth/oauth-callback.js +2 -2
  9. package/dist/auth/oauth-client.d.ts +8 -2
  10. package/dist/auth/oauth-client.js +26 -0
  11. package/dist/auth/oauth-fs.d.ts +2 -1
  12. package/dist/auth/oauth-headless.d.ts +2 -1
  13. package/dist/auth/oauth-storage.d.ts +5 -1
  14. package/dist/auth/oauth-storage.js +1 -1
  15. package/dist/auth/oauth-token.d.ts +4 -3
  16. package/dist/auth/oauth-token.js +16 -4
  17. package/dist/auth/token-resolver.d.ts +14 -5
  18. package/dist/auth/token-resolver.js +6 -1
  19. package/dist/commands/batch.js +18 -21
  20. package/dist/commands/comments.js +5 -5
  21. package/dist/commands/config.js +178 -5
  22. package/dist/commands/init/aliases.js +1 -1
  23. package/dist/commands/init/defaults.d.ts +2 -1
  24. package/dist/commands/init/index.js +45 -35
  25. package/dist/commands/init/oauth.d.ts +4 -1
  26. package/dist/commands/init/oauth.js +22 -4
  27. package/dist/commands/init/shared.d.ts +24 -2
  28. package/dist/commands/init/shared.js +35 -4
  29. package/dist/commands/init/token.d.ts +3 -3
  30. package/dist/commands/init/token.js +5 -24
  31. package/dist/commands/init/workspace.d.ts +2 -1
  32. package/dist/commands/init/workspace.js +1 -1
  33. package/dist/commands/introspect.d.ts +27 -0
  34. package/dist/commands/introspect.js +178 -0
  35. package/dist/commands/issues/branch.js +9 -1
  36. package/dist/commands/issues/relations.d.ts +3 -14
  37. package/dist/commands/issues/relations.js +3 -3
  38. package/dist/commands/issues.js +222 -43
  39. package/dist/commands/labels.js +2 -1
  40. package/dist/commands/profile.js +1 -0
  41. package/dist/commands/projects.d.ts +2 -0
  42. package/dist/commands/projects.js +91 -7
  43. package/dist/commands/read-shortcut.d.ts +1 -1
  44. package/dist/commands/read-shortcut.js +28 -8
  45. package/dist/commands/refs.js +67 -8
  46. package/dist/commands/search.js +30 -5
  47. package/dist/commands/users.js +4 -2
  48. package/dist/config/config.d.ts +99 -1
  49. package/dist/config/config.js +264 -52
  50. package/dist/config/error-enrichment.d.ts +62 -0
  51. package/dist/config/error-enrichment.js +417 -0
  52. package/dist/config/issue-validation.d.ts +37 -0
  53. package/dist/config/issue-validation.js +63 -1
  54. package/dist/config/paths.d.ts +2 -8
  55. package/dist/config/paths.js +4 -2
  56. package/dist/config/resolver.d.ts +8 -1
  57. package/dist/config/resolver.js +9 -2
  58. package/dist/main.js +13 -1
  59. package/dist/queries/comments-types.d.ts +15 -9
  60. package/dist/queries/common.d.ts +2 -2
  61. package/dist/queries/common.js +8 -0
  62. package/dist/queries/documents-types.d.ts +4 -3
  63. package/dist/queries/introspect-types.d.ts +8 -7
  64. package/dist/queries/issues-types.d.ts +92 -27
  65. package/dist/queries/issues.d.ts +49 -10
  66. package/dist/queries/issues.js +125 -5
  67. package/dist/queries/labels-types.d.ts +7 -6
  68. package/dist/queries/project-milestones-types.d.ts +5 -4
  69. package/dist/queries/project-milestones.d.ts +1 -1
  70. package/dist/queries/projects-types.d.ts +8 -7
  71. package/dist/queries/releases-types.d.ts +5 -4
  72. package/dist/queries/search-types.d.ts +28 -12
  73. package/dist/queries/templates-types.d.ts +3 -2
  74. package/dist/types/linear.d.ts +13 -1
  75. package/dist/utils/auto-link-references.d.ts +3 -3
  76. package/dist/utils/auto-link-references.js +1 -10
  77. package/dist/utils/extract-field.d.ts +19 -0
  78. package/dist/utils/extract-field.js +99 -0
  79. package/dist/utils/file-service.d.ts +6 -13
  80. package/dist/utils/file-service.js +0 -2
  81. package/dist/utils/formatters/summary.js +6 -1
  82. package/dist/utils/graphql-issues-service.d.ts +101 -45
  83. package/dist/utils/graphql-issues-service.js +252 -39
  84. package/dist/utils/graphql-service.d.ts +10 -12
  85. package/dist/utils/graphql-service.js +0 -3
  86. package/dist/utils/issue-reference-extractor.d.ts +7 -0
  87. package/dist/utils/issue-reference-extractor.js +5 -3
  88. package/dist/utils/issues-service-bootstrap.d.ts +28 -0
  89. package/dist/utils/issues-service-bootstrap.js +27 -0
  90. package/dist/utils/linear-service.d.ts +21 -14
  91. package/dist/utils/linear-service.js +73 -11
  92. package/dist/utils/markdown-prosemirror.js +12 -12
  93. package/dist/utils/mention-resolver.js +1 -1
  94. package/dist/utils/output.d.ts +81 -3
  95. package/dist/utils/output.js +61 -6
  96. package/dist/utils/project-slug.d.ts +21 -0
  97. package/dist/utils/project-slug.js +45 -0
  98. package/dist/utils/protected-ranges.d.ts +14 -0
  99. package/dist/utils/protected-ranges.js +88 -2
  100. package/dist/utils/sanitize-for-log.d.ts +24 -0
  101. package/dist/utils/sanitize-for-log.js +38 -0
  102. package/dist/utils/table-formatter.js +24 -0
  103. package/dist/utils/validators.d.ts +7 -2
  104. package/dist/utils/validators.js +6 -0
  105. package/dist/utils/workspace-url.js +20 -4
  106. package/package.json +2 -2
@@ -1,6 +1,44 @@
1
1
  import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
2
4
  import { outputWarning } from "../utils/output.js";
3
- import { CONFIG_PATH, resolveActiveProfile } from "./paths.js";
5
+ import { CONFIG_PATH, LOCAL_CONFIG_PATH, resolveActiveProfile, } from "./paths.js";
6
+ /**
7
+ * Marker file written by the EL onboarding flow (`link-project.sh`) when a
8
+ * developer wires the tools repo to their machine. We treat its presence as
9
+ * the consent signal for auto-discovering the team config — see
10
+ * `discoverTeamConfigViaMarker` and DEV-4258.
11
+ *
12
+ * `os.homedir()` is evaluated lazily inside the helper so a mocked HOME in
13
+ * tests doesn't get baked into a module-level constant.
14
+ */
15
+ function elToolsRootMarkerPath() {
16
+ return path.join(os.homedir(), ".config", "el-tools-root");
17
+ }
18
+ /**
19
+ * Resolve the shared team config path by reading the `~/.config/el-tools-root`
20
+ * marker file written by the EL onboarding flow (`link-project.sh`). Looks for
21
+ * `<root>/config/el-linear.shared.json` and returns the absolute path when it
22
+ * exists. Returns `undefined` for any of: marker missing, marker unreadable,
23
+ * marker empty, shared file missing — every failure mode is silent because
24
+ * the marker is opt-in and a non-EL user (OSS audience) is expected to miss
25
+ * it.
26
+ *
27
+ * The marker is the consent signal: only developers who explicitly linked
28
+ * their tools repo see this auto-discovery. DEV-4258 / ALL-964.
29
+ */
30
+ function discoverTeamConfigViaMarker() {
31
+ try {
32
+ const root = fs.readFileSync(elToolsRootMarkerPath(), "utf8").trim();
33
+ if (!root)
34
+ return undefined;
35
+ const shared = path.join(root, "config", "el-linear.shared.json");
36
+ return fs.existsSync(shared) ? shared : undefined;
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ }
4
42
  const DEFAULT_CONFIG = {
5
43
  defaultTeam: "",
6
44
  defaultLabels: [],
@@ -22,77 +60,245 @@ const DEFAULT_CONFIG = {
22
60
  },
23
61
  terms: [],
24
62
  };
25
- // Profile-keyed cache. Pre-fix this was a single `cachedConfig` —
26
- // switching the active profile mid-process and calling `loadConfig`
27
- // again would return the OLD profile's config until
28
- // `_resetConfigCacheForTests` ran. Today the CLI sets the profile
29
- // in `preAction` before any command body, so this is latent — but
30
- // keying by profile makes it future-proof and makes test isolation
31
- // less footgun-prone (profile A's setup pollutes profile B's read).
32
- // ALL-935 deferred fix.
63
+ // Two-level cache to avoid re-reading personal config on every loadConfig() call.
64
+ //
65
+ // Level 1 — team config path resolution:
66
+ // Key: `"${profileKey}::${envPath}"` (both "" when absent)
67
+ // Value: the resolved team config path, or undefined if none is configured.
68
+ // Populated on the first loadConfig() call for a (profile, env) combination.
69
+ // On subsequent calls the cache hit lets us skip reading personal config and
70
+ // go straight to the merged-config cache.
71
+ //
72
+ // Level 2 — merged result (team + personal):
73
+ // Key: `"${profileKey}::${teamConfigPath}"`
74
+ // Value: the fully merged ElLinearConfig (before local.json overlay).
33
75
  //
34
- // Key: `null` for the legacy single-file layout (no active profile),
35
- // otherwise the profile name. We never mix the two paths in the
36
- // cache — the marker name selects exactly one path.
37
- const cachedConfigByProfile = new Map();
38
- /** Test seam — resets the cache between test cases. */
76
+ // Level 3 — local config:
77
+ // Key: profile name (null for legacy single-file layout).
78
+ // Value: the parsed ElLinearLocalConfig from local.json.
79
+ //
80
+ // ALL-935 deferred fix note: profile switching mid-process is covered because
81
+ // the profile name is part of all cache keys.
82
+ const cachedTeamConfigPath = new Map();
83
+ const cachedConfig = new Map();
84
+ const cachedLocalConfigByProfile = new Map();
85
+ /** Test seam — resets all caches between test cases. */
39
86
  export function _resetConfigCacheForTests() {
40
- cachedConfigByProfile.clear();
87
+ cachedTeamConfigPath.clear();
88
+ cachedConfig.clear();
89
+ cachedLocalConfigByProfile.clear();
41
90
  }
42
91
  export function loadConfig() {
43
- // Profile-aware: read from <CONFIG_DIR>/profiles/<name>/config.json
44
- // when a profile is active, falling back to the legacy single-file
45
- // path so existing setups keep working without migration.
46
92
  const active = resolveActiveProfile();
47
- const cacheKey = active.name;
48
- const cached = cachedConfigByProfile.get(cacheKey);
93
+ const teamConfigEnvPath = process.env.EL_LINEAR_TEAM_CONFIG?.trim() || undefined;
94
+ const profileKey = active.name ?? "";
95
+ const pathCacheKey = `${profileKey}::${teamConfigEnvPath ?? ""}`;
96
+ // Fast path: if we already know the team config path for this
97
+ // (profile, env) combination, skip reading personal config.
98
+ let teamConfigPath;
99
+ let personalRaw;
100
+ if (cachedTeamConfigPath.has(pathCacheKey)) {
101
+ teamConfigPath = cachedTeamConfigPath.get(pathCacheKey);
102
+ }
103
+ else if (teamConfigEnvPath !== undefined) {
104
+ // Env var fully determines the path — no need to read personal config.
105
+ teamConfigPath = teamConfigEnvPath;
106
+ cachedTeamConfigPath.set(pathCacheKey, teamConfigPath);
107
+ }
108
+ else {
109
+ // First time for this profile without an env var: read personal config
110
+ // to find teamConfigPath, then fall back to the EL onboarding marker
111
+ // (`~/.config/el-tools-root` → `<root>/config/el-linear.shared.json`).
112
+ // The marker is the opt-in consent signal — only developers who ran
113
+ // `link-project.sh` get the auto-discovery; OSS users hit the silent
114
+ // no-op path. DEV-4258 / ALL-964.
115
+ personalRaw = readRawPersonalConfig(active);
116
+ teamConfigPath = personalRaw.teamConfigPath;
117
+ if (teamConfigPath === undefined) {
118
+ teamConfigPath = discoverTeamConfigViaMarker();
119
+ }
120
+ cachedTeamConfigPath.set(pathCacheKey, teamConfigPath);
121
+ }
122
+ const cacheKey = `${profileKey}::${teamConfigPath ?? ""}`;
123
+ const cached = cachedConfig.get(cacheKey);
49
124
  if (cached) {
125
+ // Apply local config on top of cached merged result.
126
+ return applyLocalConfig(cached, loadLocalConfig());
127
+ }
128
+ // Cache miss — do the full merge. Reuse personalRaw if we already read it.
129
+ if (!personalRaw) {
130
+ personalRaw = readRawPersonalConfig(active);
131
+ }
132
+ // Load the team config layer (errors are warned, never thrown).
133
+ const teamRaw = teamConfigPath
134
+ ? loadTeamConfigRaw(teamConfigPath)
135
+ : {};
136
+ // teamConfigPath inside a team config file would be circular; strip it.
137
+ delete teamRaw.teamConfigPath;
138
+ // Merge order: defaults → team config → personal config.
139
+ // Arrays (terms, defaultLabels, etc.) are concatenated so personal entries
140
+ // extend team entries rather than replace them.
141
+ const afterTeam = deepMerge(DEFAULT_CONFIG, teamRaw);
142
+ const merged = deepMerge(afterTeam, personalRaw);
143
+ cachedConfig.set(cacheKey, merged);
144
+ // Overlay local.json on top — highest priority layer.
145
+ return applyLocalConfig(merged, loadLocalConfig());
146
+ }
147
+ /**
148
+ * Returns the team config file path that is active for the current process
149
+ * (resolved via `EL_LINEAR_TEAM_CONFIG` env var, `teamConfigPath` in personal
150
+ * config, or `~/.config/el-tools-root` marker — in that order). Returns
151
+ * `undefined` when no team config is configured.
152
+ *
153
+ * Calling this before `loadConfig()` will trigger a `loadConfig()` internally
154
+ * so the path cache is populated.
155
+ */
156
+ export function getActiveTeamConfigPath() {
157
+ const active = resolveActiveProfile();
158
+ const teamConfigEnvPath = process.env.EL_LINEAR_TEAM_CONFIG?.trim() || undefined;
159
+ const profileKey = active.name ?? "";
160
+ const pathCacheKey = `${profileKey}::${teamConfigEnvPath ?? ""}`;
161
+ if (!cachedTeamConfigPath.has(pathCacheKey)) {
162
+ loadConfig();
163
+ }
164
+ return cachedTeamConfigPath.get(pathCacheKey);
165
+ }
166
+ /**
167
+ * Returns the active team-config path AND the source it was resolved from
168
+ * (env var / personal config field / onboarding-marker auto-discovery).
169
+ * `el-linear config team show` uses this to render which resolver fired —
170
+ * so an operator debugging "why isn't my team config loading?" sees the
171
+ * answer in one command.
172
+ *
173
+ * Re-derives source on each call rather than caching it: the lookup is cheap
174
+ * (one file read at most for personal config, one for the marker) and only
175
+ * `config team show` calls it.
176
+ */
177
+ export function getActiveTeamConfigInfo() {
178
+ const envPath = process.env.EL_LINEAR_TEAM_CONFIG?.trim() || undefined;
179
+ if (envPath !== undefined) {
180
+ return { path: envPath, source: "env" };
181
+ }
182
+ const personalRaw = readRawPersonalConfig(resolveActiveProfile());
183
+ const personalPath = personalRaw.teamConfigPath;
184
+ if (personalPath !== undefined && personalPath !== "") {
185
+ return { path: personalPath, source: "personal" };
186
+ }
187
+ const markerPath = discoverTeamConfigViaMarker();
188
+ if (markerPath !== undefined) {
189
+ return { path: markerPath, source: "marker" };
190
+ }
191
+ return { path: undefined, source: null };
192
+ }
193
+ /**
194
+ * Load user-local overrides from `local.json`. Returns an empty object when
195
+ * no file exists — callers treat it as "no overrides". Errors are silent so
196
+ * a missing or malformed `local.json` never breaks a command.
197
+ */
198
+ export function loadLocalConfig() {
199
+ const active = resolveActiveProfile();
200
+ const cacheKey = active.name;
201
+ const cached = cachedLocalConfigByProfile.get(cacheKey);
202
+ if (cached !== undefined) {
50
203
  return cached;
51
204
  }
52
- const candidates = [active.configPath];
53
- if (active.configPath !== CONFIG_PATH)
54
- candidates.push(CONFIG_PATH);
205
+ const candidates = [active.localConfigPath];
206
+ if (active.localConfigPath !== LOCAL_CONFIG_PATH)
207
+ candidates.push(LOCAL_CONFIG_PATH);
55
208
  const sourcePath = candidates.find((p) => fs.existsSync(p));
56
- let resolved;
209
+ let local = {};
57
210
  if (sourcePath) {
58
211
  try {
59
- const userConfig = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
60
- // Migration: the legacy `brand: { name, reject }` config is auto-promoted to
61
- // a single entry in `terms[]`. We warn (not throw) so existing users get a
62
- // grace period to update their config.
63
- if (userConfig.brand &&
64
- typeof userConfig.brand === "object" &&
65
- !Array.isArray(userConfig.brand)) {
66
- const legacy = userConfig.brand;
67
- if (typeof legacy.name === "string" && Array.isArray(legacy.reject)) {
68
- outputWarning("config.brand is deprecated — replace it with config.terms = [{ canonical, reject }]. See README#term-enforcer.");
69
- const existing = Array.isArray(userConfig.terms)
70
- ? userConfig.terms
71
- : [];
72
- userConfig.terms = [
73
- ...existing,
74
- { canonical: legacy.name, reject: legacy.reject },
75
- ];
76
- }
77
- delete userConfig.brand;
78
- }
79
- resolved = deepMerge(DEFAULT_CONFIG, userConfig);
212
+ local = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
80
213
  }
81
214
  catch {
82
- outputWarning(`Failed to parse ${sourcePath}, using empty defaults`);
83
- resolved = DEFAULT_CONFIG;
215
+ outputWarning(`Failed to parse ${sourcePath}, ignoring local config`);
84
216
  }
85
217
  }
86
- else {
218
+ cachedLocalConfigByProfile.set(cacheKey, local);
219
+ return local;
220
+ }
221
+ /**
222
+ * Merge local config overrides onto the merged team+personal config.
223
+ * `assigneeEmail` maps to `defaultAssignee` as a convenience alias.
224
+ */
225
+ function applyLocalConfig(base, local) {
226
+ const hasApplicable = local.cacheTTLSeconds !== undefined ||
227
+ local.defaultPriority !== undefined ||
228
+ local.defaultAssignee !== undefined ||
229
+ local.assigneeEmail !== undefined;
230
+ if (!hasApplicable)
231
+ return base;
232
+ const result = { ...base };
233
+ if (local.cacheTTLSeconds !== undefined)
234
+ result.cacheTTLSeconds = local.cacheTTLSeconds;
235
+ if (local.defaultPriority !== undefined)
236
+ result.defaultPriority = local.defaultPriority;
237
+ if (local.defaultAssignee !== undefined) {
238
+ result.defaultAssignee = local.defaultAssignee;
239
+ }
240
+ else if (local.assigneeEmail !== undefined) {
241
+ result.defaultAssignee = local.assigneeEmail;
242
+ }
243
+ return result;
244
+ }
245
+ function readRawPersonalConfig(active) {
246
+ const candidates = [active.configPath];
247
+ if (active.configPath !== CONFIG_PATH)
248
+ candidates.push(CONFIG_PATH);
249
+ const sourcePath = candidates.find((p) => fs.existsSync(p));
250
+ if (!sourcePath) {
87
251
  const profileNote = active.name
88
252
  ? ` (active profile: \`${active.name}\` — expected at ${active.configPath})`
89
253
  : "";
90
254
  outputWarning(`No config found at ${active.configPath}${profileNote}. Run \`el-linear init\` (or \`el-linear profile add ${active.name ?? "<name>"}\`) to create one.`);
91
- resolved = DEFAULT_CONFIG;
255
+ return {};
256
+ }
257
+ try {
258
+ const raw = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
259
+ // Migration: the legacy `brand: { name, reject }` config is auto-promoted
260
+ // to a single entry in `terms[]`. We warn (not throw) so existing users get
261
+ // a grace period to update their config.
262
+ if (raw.brand &&
263
+ typeof raw.brand === "object" &&
264
+ !Array.isArray(raw.brand)) {
265
+ const legacy = raw.brand;
266
+ if (typeof legacy.name === "string" && Array.isArray(legacy.reject)) {
267
+ outputWarning("config.brand is deprecated — replace it with config.terms = [{ canonical, reject }]. See README#term-enforcer.");
268
+ const existing = Array.isArray(raw.terms) ? raw.terms : [];
269
+ raw.terms = [
270
+ ...existing,
271
+ { canonical: legacy.name, reject: legacy.reject },
272
+ ];
273
+ }
274
+ delete raw.brand;
275
+ }
276
+ return raw;
277
+ }
278
+ catch {
279
+ outputWarning(`Failed to parse ${sourcePath}, using empty defaults`);
280
+ return {};
281
+ }
282
+ }
283
+ function loadTeamConfigRaw(teamConfigPath) {
284
+ if (!fs.existsSync(teamConfigPath)) {
285
+ outputWarning(`Team config not found at ${teamConfigPath} — skipping team config layer.`);
286
+ return {};
287
+ }
288
+ try {
289
+ return JSON.parse(fs.readFileSync(teamConfigPath, "utf8"));
290
+ }
291
+ catch {
292
+ outputWarning(`Failed to parse team config at ${teamConfigPath} — skipping team config layer.`);
293
+ return {};
92
294
  }
93
- cachedConfigByProfile.set(cacheKey, resolved);
94
- return resolved;
95
295
  }
296
+ /**
297
+ * Deep-merge `source` into `target`. Objects are merged recursively; arrays
298
+ * are concatenated (target first, source appended) so that a team config's
299
+ * `terms` and `defaultLabels` are extended by personal config rather than
300
+ * replaced. Scalar values from `source` win over `target`.
301
+ */
96
302
  function deepMerge(target, source) {
97
303
  const result = { ...target };
98
304
  for (const key of Object.keys(source)) {
@@ -102,7 +308,13 @@ function deepMerge(target, source) {
102
308
  if (key === "__proto__" || key === "constructor" || key === "prototype") {
103
309
  continue;
104
310
  }
105
- if (source[key] &&
311
+ if (Array.isArray(source[key]) && Array.isArray(target[key])) {
312
+ result[key] = [
313
+ ...target[key],
314
+ ...source[key],
315
+ ];
316
+ }
317
+ else if (source[key] &&
106
318
  typeof source[key] === "object" &&
107
319
  !Array.isArray(source[key]) &&
108
320
  target[key] &&
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Validation error enrichment.
3
+ *
4
+ * When `validateIssueCreation` flags a missing required field AND `--team`
5
+ * is set, this module fetches concrete team-scoped suggestions (active
6
+ * projects, active members, valid labels) and appends a "Suggestions:" block
7
+ * to each relevant error message.
8
+ *
9
+ * Goal: reduce the agent's loop time. A single error should carry enough
10
+ * concrete options that the agent can rebuild a complete retry command
11
+ * without follow-up `projects list` / `users list` / `labels list` calls.
12
+ *
13
+ * Latency budget: enrichment only runs on the validation-fail path. The
14
+ * success path is unchanged. Fetches run in parallel and only for the fields
15
+ * that are actually missing.
16
+ */
17
+ import type { GraphQLService } from "../utils/graphql-service.js";
18
+ import type { LinearService } from "../utils/linear-service.js";
19
+ import { type ValidationResult } from "./issue-validation.js";
20
+ interface EnrichOptions {
21
+ team?: string;
22
+ title?: string;
23
+ }
24
+ /**
25
+ * Enrich validation errors with team-scoped suggestions.
26
+ *
27
+ * Mutates `result.errors` in place — each enrichable error gets a
28
+ * "Suggestions:" block appended. Verb→type inference is added as a prefixed
29
+ * "Inferred from title:" line on the `--labels` missing error.
30
+ *
31
+ * No-op when `--team` is not set or when there are no errors to enrich.
32
+ * Safe to call on the failure path only — never invoked when validation
33
+ * passes, so the success path's latency is unchanged.
34
+ *
35
+ * Failures (network, unknown team) are swallowed: enrichment is a best-effort
36
+ * UX hint, not part of the validation contract.
37
+ */
38
+ export declare function enrichValidationErrors(result: ValidationResult, options: EnrichOptions, services: {
39
+ graphQLService: GraphQLService;
40
+ linearService: LinearService;
41
+ }): Promise<void>;
42
+ /**
43
+ * Enrich a project resolver failure ({@link PROJECT_NOT_FOUND_PATTERN})
44
+ * with the same team-scoped suggestions that {@link enrichValidationErrors}
45
+ * appends to `Missing --project` validation errors. Returns the original
46
+ * message unchanged if the error doesn't match or `--team` is unknown.
47
+ *
48
+ * The validation path covers "user forgot `--project`"; this path covers
49
+ * "user pasted a URL/slug/name that didn't resolve" — same recovery hints
50
+ * apply in both cases. Synthesizes a `Missing --project: …` error string
51
+ * so the existing classifier and suggestion code can be reused without
52
+ * special-casing the resolver shape.
53
+ *
54
+ * Best-effort like `enrichValidationErrors`: any failure (network, unknown
55
+ * team, GraphQL error) returns the original message unchanged. The caller
56
+ * is expected to rethrow with the returned message.
57
+ */
58
+ export declare function enrichProjectResolverError(originalMessage: string, options: EnrichOptions, services: {
59
+ graphQLService: GraphQLService;
60
+ linearService: LinearService;
61
+ }): Promise<string>;
62
+ export {};