@oss-autopilot/core 3.10.0 → 3.12.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 (52) hide show
  1. package/dist/cli-registry.d.ts +7 -0
  2. package/dist/cli-registry.js +58 -5
  3. package/dist/cli.bundle.cjs +165 -112
  4. package/dist/cli.js +11 -3
  5. package/dist/commands/comments.js +31 -15
  6. package/dist/commands/compliance-score.js +12 -4
  7. package/dist/commands/daily-render.d.ts +2 -1
  8. package/dist/commands/daily-render.js +8 -2
  9. package/dist/commands/daily.d.ts +3 -1
  10. package/dist/commands/daily.js +54 -4
  11. package/dist/commands/dashboard-data.d.ts +17 -0
  12. package/dist/commands/dashboard-data.js +62 -4
  13. package/dist/commands/dashboard-server.js +100 -26
  14. package/dist/commands/dismiss.d.ts +4 -0
  15. package/dist/commands/dismiss.js +4 -4
  16. package/dist/commands/guidelines.d.ts +19 -0
  17. package/dist/commands/guidelines.js +23 -4
  18. package/dist/commands/index.d.ts +5 -1
  19. package/dist/commands/index.js +4 -0
  20. package/dist/commands/list-move-tier.d.ts +11 -3
  21. package/dist/commands/list-move-tier.js +18 -7
  22. package/dist/commands/move.d.ts +2 -0
  23. package/dist/commands/move.js +12 -8
  24. package/dist/commands/repo-vet.js +30 -8
  25. package/dist/commands/search.js +17 -3
  26. package/dist/commands/shelve.d.ts +4 -0
  27. package/dist/commands/shelve.js +4 -4
  28. package/dist/commands/verify-issue.d.ts +20 -0
  29. package/dist/commands/verify-issue.js +32 -0
  30. package/dist/core/daily-logic.js +65 -52
  31. package/dist/core/gist-state-store.js +42 -7
  32. package/dist/core/index.d.ts +3 -1
  33. package/dist/core/index.js +3 -1
  34. package/dist/core/issue-conversation.js +15 -2
  35. package/dist/core/issue-verification.d.ts +91 -0
  36. package/dist/core/issue-verification.js +270 -0
  37. package/dist/core/paths.d.ts +12 -0
  38. package/dist/core/paths.js +16 -0
  39. package/dist/core/pr-attention.d.ts +52 -0
  40. package/dist/core/pr-attention.js +76 -0
  41. package/dist/core/pr-comments-fetcher.d.ts +10 -2
  42. package/dist/core/pr-comments-fetcher.js +22 -4
  43. package/dist/core/state-persistence.d.ts +31 -9
  44. package/dist/core/state-persistence.js +51 -16
  45. package/dist/core/state.d.ts +18 -1
  46. package/dist/core/state.js +35 -3
  47. package/dist/core/types.d.ts +7 -0
  48. package/dist/core/untrusted-content.d.ts +24 -3
  49. package/dist/core/untrusted-content.js +31 -3
  50. package/dist/formatters/json.d.ts +83 -2
  51. package/dist/formatters/json.js +55 -1
  52. package/package.json +7 -7
@@ -248,62 +248,75 @@ export function collectActionableIssues(prs, lastDigestAt) {
248
248
  'merge_conflict',
249
249
  'incomplete_checklist',
250
250
  ];
251
- for (const reason of reasonOrder) {
252
- for (const pr of actionPRs) {
253
- if (pr.actionReason !== reason)
254
- continue;
255
- let label;
256
- let type;
257
- switch (reason) {
258
- case 'needs_response': {
259
- label = '[Needs Response]';
260
- type = 'needs_response';
261
- break;
262
- }
263
- case 'needs_changes': {
264
- label = '[Needs Changes]';
265
- type = 'needs_changes';
266
- break;
267
- }
268
- case 'failing_ci': {
269
- const checkInfo = pr.failingCheckNames.length > 0 ? ` (${pr.failingCheckNames.join(', ')})` : '';
270
- label = `[CI Failing${checkInfo}]`;
271
- type = 'ci_failing';
272
- break;
273
- }
274
- case 'merge_conflict': {
275
- label = '[Merge Conflict]';
276
- type = 'merge_conflict';
277
- break;
278
- }
279
- case 'incomplete_checklist': {
280
- const stats = pr.checklistStats ? ` (${pr.checklistStats.checked}/${pr.checklistStats.total})` : '';
281
- label = `[Incomplete Checklist${stats}]`;
282
- type = 'incomplete_checklist';
283
- break;
284
- }
285
- default: {
286
- // Defensive fallback for ActionReason values not explicitly handled
287
- // above (e.g. ci_not_running, needs_rebase, missing_required_files).
288
- // These aren't in reasonOrder today but this guards future additions.
289
- warn('daily-logic', `Unhandled ActionReason "${reason}" for PR ${pr.url} — falling back to needs_response`);
290
- label = `[${reason}]`;
291
- type = 'needs_response';
292
- }
251
+ // #1352: every needs_addressing PR produces exactly one entry, including
252
+ // PRs whose actionReason is missing or outside reasonOrder (the defensive
253
+ // default below). This keeps the CLI brief's count equal to the dashboard's
254
+ // needs_attention bucket by construction — previously an unmapped reason
255
+ // silently dropped the PR from the brief while the dashboard still counted
256
+ // it. Unmapped reasons sort last.
257
+ const sortedPRs = [...actionPRs].sort((a, b) => {
258
+ const rank = (pr) => {
259
+ const i = reasonOrder.indexOf(pr.actionReason);
260
+ return i === -1 ? reasonOrder.length : i;
261
+ };
262
+ return rank(a) - rank(b);
263
+ });
264
+ for (const pr of sortedPRs) {
265
+ if (pr.actionReason === undefined) {
266
+ warn('daily-logic', `needs_addressing PR ${pr.url} has no actionReason — defaulting to needs_response`);
267
+ }
268
+ const reason = pr.actionReason ?? 'needs_response';
269
+ let label;
270
+ let type;
271
+ switch (reason) {
272
+ case 'needs_response': {
273
+ label = '[Needs Response]';
274
+ type = 'needs_response';
275
+ break;
276
+ }
277
+ case 'needs_changes': {
278
+ label = '[Needs Changes]';
279
+ type = 'needs_changes';
280
+ break;
281
+ }
282
+ case 'failing_ci': {
283
+ const checkInfo = pr.failingCheckNames.length > 0 ? ` (${pr.failingCheckNames.join(', ')})` : '';
284
+ label = `[CI Failing${checkInfo}]`;
285
+ type = 'ci_failing';
286
+ break;
287
+ }
288
+ case 'merge_conflict': {
289
+ label = '[Merge Conflict]';
290
+ type = 'merge_conflict';
291
+ break;
293
292
  }
294
- // A PR is "new" if it was created after the last daily digest (first time seen).
295
- // If there's no previous digest (first run) or createdAt is invalid, assume new.
296
- const createdTime = new Date(pr.createdAt).getTime();
297
- let isNewContribution;
298
- if (isNaN(createdTime)) {
299
- warn('daily-logic', `Invalid createdAt "${pr.createdAt}" for PR ${pr.url}, assuming new contribution`);
300
- isNewContribution = true;
293
+ case 'incomplete_checklist': {
294
+ const stats = pr.checklistStats ? ` (${pr.checklistStats.checked}/${pr.checklistStats.total})` : '';
295
+ label = `[Incomplete Checklist${stats}]`;
296
+ type = 'incomplete_checklist';
297
+ break;
301
298
  }
302
- else {
303
- isNewContribution = isNaN(lastDigestTime) || createdTime > lastDigestTime;
299
+ default: {
300
+ // Defensive fallback for ActionReason values not explicitly handled
301
+ // above (e.g. ci_not_running, needs_rebase, missing_required_files).
302
+ // These aren't in reasonOrder today but this guards future additions.
303
+ warn('daily-logic', `Unhandled ActionReason "${reason}" for PR ${pr.url} — falling back to needs_response`);
304
+ label = `[${reason}]`;
305
+ type = 'needs_response';
304
306
  }
305
- issues.push({ type, pr, label, isNewContribution });
306
307
  }
308
+ // A PR is "new" if it was created after the last daily digest (first time seen).
309
+ // If there's no previous digest (first run) or createdAt is invalid, assume new.
310
+ const createdTime = new Date(pr.createdAt).getTime();
311
+ let isNewContribution;
312
+ if (isNaN(createdTime)) {
313
+ warn('daily-logic', `Invalid createdAt "${pr.createdAt}" for PR ${pr.url}, assuming new contribution`);
314
+ isNewContribution = true;
315
+ }
316
+ else {
317
+ isNewContribution = isNaN(lastDigestTime) || createdTime > lastDigestTime;
318
+ }
319
+ issues.push({ type, pr, label, isNewContribution });
307
320
  }
308
321
  return issues;
309
322
  }
@@ -49,7 +49,7 @@ import { AgentStateSchema } from './state-schema.js';
49
49
  import { atomicWriteFileSync, createFreshState, migrateV1ToV2, migrateV2ToV3, migrateV3ToV4, } from './state-persistence.js';
50
50
  import { getGistIdPath, getStateCachePath } from './paths.js';
51
51
  import { debug, warn } from './logger.js';
52
- import { GistPermissionError, GistConcurrencyError, GistCorruptError, isRateLimitError } from './errors.js';
52
+ import { ConfigurationError, GistPermissionError, GistConcurrencyError, GistCorruptError, isRateLimitError, } from './errors.js';
53
53
  const MODULE = 'gist-store';
54
54
  /**
55
55
  * Extract the ETag header from an Octokit response, tolerating both lower-
@@ -139,6 +139,13 @@ export class GistStateStore {
139
139
  return { gistId: localId, state, created: false };
140
140
  }
141
141
  catch (err) {
142
+ // A corrupt or permission-broken Gist must surface immediately
143
+ // (#1367): falling through to search would either re-find the same
144
+ // corrupt Gist or silently abandon it and create a fresh one.
145
+ if (err instanceof ConfigurationError)
146
+ throw err;
147
+ if (isRateLimitError(err))
148
+ throw err;
142
149
  warn(MODULE, `Failed to fetch Gist ${localId}, will search/create`, err);
143
150
  // Fall through to search
144
151
  }
@@ -159,10 +166,22 @@ export class GistStateStore {
159
166
  return { gistId: id, state, created: true };
160
167
  }
161
168
  catch (err) {
162
- // Configuration errors (e.g. GistPermissionError) must surface, not degrade
163
- if (err instanceof GistPermissionError)
169
+ // Configuration errors (GistPermissionError, GistCorruptError) and rate
170
+ // limits must surface, not degrade (#1367). A corrupt Gist especially:
171
+ // fetchAndCache arms this.gistId/lastFetchedEtag before the parse
172
+ // throws, so a degraded store could push() local or fresh state over
173
+ // the corrupt remote — the exact data loss #1201 exists to prevent.
174
+ // Rate limits propagate per the errors.ts contract; degrading would
175
+ // present "you are rate-limited" as a stale local cache.
176
+ if (err instanceof ConfigurationError)
164
177
  throw err;
165
- // All API paths failed — enter degraded mode
178
+ if (isRateLimitError(err))
179
+ throw err;
180
+ // All API paths failed — enter degraded mode. Disarm the remote write
181
+ // path first: a degraded store never verified its Gist, so it must
182
+ // never be able to push to one (#1367).
183
+ this.gistId = null;
184
+ this.lastFetchedEtag = null;
166
185
  warn(MODULE, 'All Gist API paths failed, entering degraded mode', err);
167
186
  // Try reading from local cache file
168
187
  const cachePath = getStateCachePath();
@@ -222,6 +241,12 @@ export class GistStateStore {
222
241
  return { gistId: localId, state, created: false, migrated: false };
223
242
  }
224
243
  catch (err) {
244
+ // See bootstrap(): corrupt/permission/rate-limit errors surface
245
+ // immediately rather than falling through (#1367).
246
+ if (err instanceof ConfigurationError)
247
+ throw err;
248
+ if (isRateLimitError(err))
249
+ throw err;
225
250
  warn(MODULE, `bootstrapWithMigration: failed to fetch Gist ${localId}, will search/create`, err);
226
251
  // Fall through to search
227
252
  }
@@ -242,10 +267,16 @@ export class GistStateStore {
242
267
  return { gistId: id, state, created: true, migrated: true };
243
268
  }
244
269
  catch (err) {
245
- // Configuration errors (e.g. GistPermissionError) must surface, not degrade
246
- if (err instanceof GistPermissionError)
270
+ // Same surfacing contract as bootstrap() (#1367): configuration errors
271
+ // and rate limits rethrow; only genuine API unavailability degrades.
272
+ if (err instanceof ConfigurationError)
273
+ throw err;
274
+ if (isRateLimitError(err))
247
275
  throw err;
248
- // All API paths failed — enter degraded mode
276
+ // All API paths failed — enter degraded mode with the push path
277
+ // disarmed (see bootstrap()).
278
+ this.gistId = null;
279
+ this.lastFetchedEtag = null;
249
280
  warn(MODULE, 'bootstrapWithMigration: all Gist API paths failed, entering degraded mode', err);
250
281
  // Try reading from local cache file
251
282
  const cachePath = getStateCachePath();
@@ -627,6 +658,10 @@ export class GistStateStore {
627
658
  }
628
659
  }
629
660
  catch (err) {
661
+ // A rate-limited search must not read as "no Gist found" — bootstrap
662
+ // would proceed to create a duplicate Gist while throttled (#1367).
663
+ if (isRateLimitError(err))
664
+ throw err;
630
665
  warn(MODULE, 'Failed to search Gists by description', err);
631
666
  }
632
667
  return null;
@@ -8,7 +8,7 @@ export { guidelinesFilename, repoFromGuidelinesFilename, GUIDELINES_FILE_PREFIX,
8
8
  export { PRMonitor, type PRCheckFailure, type FetchPRsResult, computeDisplayLabel, classifyCICheck, classifyFailingChecks, } from './pr-monitor.js';
9
9
  export { IssueConversationMonitor } from './issue-conversation.js';
10
10
  export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
11
- export { wrapUntrustedContent, extractFromFence, UNTRUSTED_OPEN_TAG_NAME, UNTRUSTED_CLOSE_TAG, type UntrustedContentMeta, } from './untrusted-content.js';
11
+ export { wrapUntrustedContent, extractFromFence, safeExtractFromFence, UNTRUSTED_OPEN_TAG_NAME, UNTRUSTED_CLOSE_TAG, type UntrustedContentMeta, } from './untrusted-content.js';
12
12
  export { getOctokit, checkRateLimit, type RateLimitInfo } from './github.js';
13
13
  export { parseGitHubUrl, splitRepo, isOwnRepo } from './urls.js';
14
14
  export { daysBetween, formatRelativeTime, byDateDescending } from './dates.js';
@@ -22,6 +22,8 @@ export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsBy
22
22
  export { computeContributionStats, type ContributionStats, type ComputeStatsInput } from './stats.js';
23
23
  export { fetchPRTemplate, type PRTemplateResult } from './pr-template.js';
24
24
  export { classifyLinkedPR, isLinkedPRStalled, STALLED_PR_THRESHOLD_DAYS, type LinkedPR, type LinkedPRClassification, type LinkedPRState, } from './linked-pr-classification.js';
25
+ export { classifyIssueAvailability, fetchIssueVerification, type IssueAvailabilityVerdict, type IssueVerification, type LinkedPRLinkType, type VerifiedLinkedPR, type VerifyIssueParams, } from './issue-verification.js';
26
+ export { classifyAttentionBucket, summarizeAttentionBuckets, STUCK_CI_THRESHOLD_DAYS, DORMANT_FOLLOWUP_THRESHOLD_DAYS, type AttentionBucket, type AttentionInput, type AttentionSummary, } from './pr-attention.js';
25
27
  export { scanForAntiLLMPolicy, type AntiLLMCategory, type AntiLLMMatch, type AntiLLMScanResult, } from './anti-llm-policy.js';
26
28
  export { detectFormatters, diagnoseCIFormatterFailure, getPreferredFormatter, type DetectedFormatter, type FormatterDetectionResult, type CIFormatterDiagnosis, type FormatterName, } from './formatter-detection.js';
27
29
  export { CONFIG_KEY_REGISTRY, type ConfigKeyDef, type SettableVia, isKnownKey, getKeyDef, getSetupKeys, getConfigKeys, suggestKey, formatUnknownKeyError, } from './config-registry.js';
@@ -9,7 +9,7 @@ export { PRMonitor, computeDisplayLabel, classifyCICheck, classifyFailingChecks,
9
9
  // Search/vetting now delegated to @oss-scout/core via commands/scout-bridge.ts
10
10
  export { IssueConversationMonitor } from './issue-conversation.js';
11
11
  export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
12
- export { wrapUntrustedContent, extractFromFence, UNTRUSTED_OPEN_TAG_NAME, UNTRUSTED_CLOSE_TAG, } from './untrusted-content.js';
12
+ export { wrapUntrustedContent, extractFromFence, safeExtractFromFence, UNTRUSTED_OPEN_TAG_NAME, UNTRUSTED_CLOSE_TAG, } from './untrusted-content.js';
13
13
  export { getOctokit, checkRateLimit } from './github.js';
14
14
  export { parseGitHubUrl, splitRepo, isOwnRepo } from './urls.js';
15
15
  export { daysBetween, formatRelativeTime, byDateDescending } from './dates.js';
@@ -23,6 +23,8 @@ export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsBy
23
23
  export { computeContributionStats } from './stats.js';
24
24
  export { fetchPRTemplate } from './pr-template.js';
25
25
  export { classifyLinkedPR, isLinkedPRStalled, STALLED_PR_THRESHOLD_DAYS, } from './linked-pr-classification.js';
26
+ export { classifyIssueAvailability, fetchIssueVerification, } from './issue-verification.js';
27
+ export { classifyAttentionBucket, summarizeAttentionBuckets, STUCK_CI_THRESHOLD_DAYS, DORMANT_FOLLOWUP_THRESHOLD_DAYS, } from './pr-attention.js';
26
28
  export { scanForAntiLLMPolicy, } from './anti-llm-policy.js';
27
29
  export { detectFormatters, diagnoseCIFormatterFailure, getPreferredFormatter, } from './formatter-detection.js';
28
30
  export { CONFIG_KEY_REGISTRY, isKnownKey, getKeyDef, getSetupKeys, getConfigKeys, suggestKey, formatUnknownKeyError, } from './config-registry.js';
@@ -12,7 +12,7 @@ import { getStateManager } from './state.js';
12
12
  import { daysBetween } from './dates.js';
13
13
  import { splitRepo, extractOwnerRepo, isOwnRepo } from './urls.js';
14
14
  import { runWorkerPool, DEFAULT_CONCURRENCY } from './concurrency.js';
15
- import { ConfigurationError, errorMessage } from './errors.js';
15
+ import { ConfigurationError, errorMessage, isRateLimitOrAuthError } from './errors.js';
16
16
  import { debug, warn } from './logger.js';
17
17
  const MODULE = 'issue-conversation';
18
18
  const MAX_CONCURRENT_REQUESTS = DEFAULT_CONCURRENCY;
@@ -108,6 +108,14 @@ export class IssueConversationMonitor {
108
108
  }
109
109
  }
110
110
  catch (error) {
111
+ // Rate-limit / auth failures must propagate, not degrade to "fewer
112
+ // results" — under throttling every sibling analysis fails the same
113
+ // way and the partial result silently looks like a quiet day (#1391).
114
+ // runWorkerPool aborts remaining workers and rejects; daily.ts and
115
+ // dashboard-data.ts rethrow rate-limit/auth from their phase catches,
116
+ // aborting the run just like PRMonitor's 429s do.
117
+ if (isRateLimitOrAuthError(error))
118
+ throw error;
111
119
  const msg = errorMessage(error);
112
120
  failures.push({ issueUrl: item.html_url, error: msg });
113
121
  warn(MODULE, `Error analyzing issue ${item.html_url}: ${msg}`);
@@ -117,7 +125,7 @@ export class IssueConversationMonitor {
117
125
  warn(MODULE, `${failures.length}/${candidates.length} issue analysis call(s) failed`);
118
126
  }
119
127
  if (failures.length === candidates.length && candidates.length > 0) {
120
- warn(MODULE, `All ${candidates.length} issue analysis call(s) failed. Possible systemic issue (rate limit, auth, network).`);
128
+ warn(MODULE, `All ${candidates.length} issue analysis call(s) failed. Possible systemic issue (network, GitHub availability).`);
121
129
  }
122
130
  // Sort: new_response first, then waiting, then acknowledged
123
131
  const statusOrder = {
@@ -199,6 +207,11 @@ export class IssueConversationMonitor {
199
207
  }
200
208
  }
201
209
  const labels = (item.labels || []).map((l) => l.name || '').filter(Boolean);
210
+ // Body excerpts stay RAW here on purpose (#1372): these objects feed the
211
+ // dashboard SPA and the CLI text renderers directly, and the matching
212
+ // above (acknowledgment / @mention) operates on raw text. The
213
+ // `<github-content>` fence is applied at the agent-facing serialization
214
+ // boundary in `toDailyOutput()` (commands/daily.ts).
202
215
  const base = {
203
216
  repo: repoFullName,
204
217
  number: item.number,
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Deterministic issue verification (#1353, #1354).
3
+ *
4
+ * One GraphQL round-trip answers the two questions the issue-scout agent
5
+ * historically hallucinated: "is this issue actually open?" and "is it
6
+ * actually taken?". The query fetches the issue's `state`/`stateReason`,
7
+ * its assignees, every cross-referenced PR with `closingIssuesReferences`,
8
+ * and `viewer.login` — so "is this my PR?" is derived from the same token
9
+ * that fetched the data instead of a cached username.
10
+ *
11
+ * Classification is a pure function (`classifyIssueAvailability`) so the
12
+ * verdict rules are unit-testable without network access. The distinction
13
+ * that matters (#1353): a PR whose `closingIssuesReferences` includes this
14
+ * issue is a real claim (`closing`); a PR that merely mentions the issue in
15
+ * its timeline is just a mention (`cross-referenced`) and must not drive a
16
+ * "Taken" verdict.
17
+ */
18
+ import type { Octokit } from '@octokit/rest';
19
+ /** How a PR found in the issue's timeline relates to the issue. */
20
+ export type LinkedPRLinkType = 'closing' | 'cross-referenced';
21
+ export interface VerifiedLinkedPR {
22
+ number: number;
23
+ url: string;
24
+ title: string;
25
+ /** Lowercase normalization of GraphQL's OPEN/CLOSED/MERGED union. */
26
+ state: 'open' | 'closed' | 'merged';
27
+ isDraft: boolean;
28
+ /** PR author login; null for ghost (deleted) accounts. */
29
+ author: string | null;
30
+ /** True when the PR author is the authenticated user (case-insensitive). */
31
+ isOwn: boolean;
32
+ /** `closing` = the PR's closingIssuesReferences names this issue (a real
33
+ * claim). `cross-referenced` = timeline mention only (NOT a claim). */
34
+ linkType: LinkedPRLinkType;
35
+ updatedAt: string | null;
36
+ }
37
+ /**
38
+ * The short-circuit signal consumers route on:
39
+ * - `closed` — issue is not open; stop all further analysis (#1353).
40
+ * - `own-open-pr` — the authenticated user already has an open closing PR;
41
+ * not a new opportunity (#1354).
42
+ * - `taken` — someone else has an open closing PR, or the issue is assigned
43
+ * to someone else.
44
+ * - `at-risk` — no hard claim, but signals competition or imminent closure
45
+ * (open cross-referencing PRs, or a merged closing PR awaiting issue close).
46
+ * - `available` — open, unassigned, no claiming PRs.
47
+ */
48
+ export type IssueAvailabilityVerdict = 'closed' | 'own-open-pr' | 'taken' | 'at-risk' | 'available';
49
+ export interface IssueVerification {
50
+ url: string;
51
+ owner: string;
52
+ repo: string;
53
+ number: number;
54
+ title: string;
55
+ /** Lowercase normalization of GraphQL's OPEN/CLOSED union. */
56
+ state: 'open' | 'closed';
57
+ /** Lowercase GraphQL IssueStateReason. `completed` = fixed upstream (mark
58
+ * Done); `not_planned` = wontfix (drop permanently). Note: an OPEN issue
59
+ * can carry `reopened` — never infer closed-ness from a non-null reason;
60
+ * branch on `state` only. */
61
+ stateReason: 'completed' | 'not_planned' | 'reopened' | 'duplicate' | null;
62
+ closedAt: string | null;
63
+ assignees: string[];
64
+ linkedPRs: VerifiedLinkedPR[];
65
+ verdict: IssueAvailabilityVerdict;
66
+ verdictReason: string;
67
+ /** Login of the authenticated user the verdict was computed for. */
68
+ userLogin: string;
69
+ }
70
+ /** Inputs for the pure verdict classifier — the fetched facts, no I/O. */
71
+ export interface ClassifyIssueAvailabilityInput {
72
+ state: 'open' | 'closed';
73
+ stateReason: IssueVerification['stateReason'];
74
+ assignees: string[];
75
+ linkedPRs: VerifiedLinkedPR[];
76
+ userLogin: string;
77
+ }
78
+ /**
79
+ * Compute the availability verdict from fetched facts. Rules in priority
80
+ * order; the first match wins.
81
+ */
82
+ export declare function classifyIssueAvailability(input: ClassifyIssueAvailabilityInput): {
83
+ verdict: IssueAvailabilityVerdict;
84
+ verdictReason: string;
85
+ };
86
+ export interface VerifyIssueParams {
87
+ owner: string;
88
+ repo: string;
89
+ number: number;
90
+ }
91
+ export declare function fetchIssueVerification(octokit: Octokit, params: VerifyIssueParams): Promise<IssueVerification>;