@oss-autopilot/core 3.13.4 → 3.14.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 (85) hide show
  1. package/README.md +3 -3
  2. package/dist/cli-registry.js +50 -83
  3. package/dist/cli.bundle.cjs +110 -107
  4. package/dist/cli.js +5 -4
  5. package/dist/commands/comments.js +44 -10
  6. package/dist/commands/config.d.ts +2 -0
  7. package/dist/commands/config.js +50 -2
  8. package/dist/commands/curated-list.d.ts +17 -0
  9. package/dist/commands/curated-list.js +25 -0
  10. package/dist/commands/daily.d.ts +7 -1
  11. package/dist/commands/daily.js +136 -57
  12. package/dist/commands/dashboard-cache.d.ts +69 -0
  13. package/dist/commands/dashboard-cache.js +219 -0
  14. package/dist/commands/dashboard-data.d.ts +18 -10
  15. package/dist/commands/dashboard-data.js +35 -7
  16. package/dist/commands/dashboard-gist-sync.d.ts +93 -0
  17. package/dist/commands/dashboard-gist-sync.js +237 -0
  18. package/dist/commands/dashboard-server.d.ts +6 -10
  19. package/dist/commands/dashboard-server.js +148 -341
  20. package/dist/commands/features.js +6 -0
  21. package/dist/commands/guidelines.d.ts +6 -0
  22. package/dist/commands/guidelines.js +7 -0
  23. package/dist/commands/index.d.ts +2 -5
  24. package/dist/commands/index.js +2 -4
  25. package/dist/commands/init.d.ts +2 -0
  26. package/dist/commands/init.js +7 -1
  27. package/dist/commands/list-mark-done.js +6 -21
  28. package/dist/commands/list-move-tier.js +3 -5
  29. package/dist/commands/locate-issue-list.d.ts +25 -0
  30. package/dist/commands/locate-issue-list.js +67 -0
  31. package/dist/commands/merge-loop.d.ts +63 -0
  32. package/dist/commands/merge-loop.js +157 -0
  33. package/dist/commands/repo-vet.js +40 -1
  34. package/dist/commands/scout-bridge.d.ts +35 -2
  35. package/dist/commands/scout-bridge.js +65 -13
  36. package/dist/commands/search.d.ts +4 -6
  37. package/dist/commands/search.js +58 -11
  38. package/dist/commands/setup.d.ts +2 -0
  39. package/dist/commands/setup.js +56 -2
  40. package/dist/commands/skip-file-parser.d.ts +23 -0
  41. package/dist/commands/skip-file-parser.js +23 -10
  42. package/dist/commands/startup.d.ts +1 -6
  43. package/dist/commands/startup.js +25 -59
  44. package/dist/commands/track.d.ts +2 -2
  45. package/dist/commands/track.js +2 -2
  46. package/dist/commands/vet-list.js +4 -0
  47. package/dist/core/config-registry.js +36 -0
  48. package/dist/core/daily-logic.d.ts +25 -2
  49. package/dist/core/daily-logic.js +58 -3
  50. package/dist/core/gist-health.d.ts +81 -0
  51. package/dist/core/gist-health.js +39 -0
  52. package/dist/core/gist-state-store.d.ts +3 -1
  53. package/dist/core/gist-state-store.js +7 -2
  54. package/dist/core/github-stats.d.ts +2 -2
  55. package/dist/core/github-stats.js +20 -4
  56. package/dist/core/index.d.ts +4 -3
  57. package/dist/core/index.js +4 -3
  58. package/dist/core/issue-conversation.js +8 -2
  59. package/dist/core/issue-grading.d.ts +9 -0
  60. package/dist/core/issue-grading.js +9 -0
  61. package/dist/core/pagination.d.ts +27 -0
  62. package/dist/core/pagination.js +23 -5
  63. package/dist/core/pr-comments-fetcher.d.ts +7 -0
  64. package/dist/core/pr-comments-fetcher.js +19 -8
  65. package/dist/core/pr-monitor.d.ts +2 -0
  66. package/dist/core/pr-monitor.js +26 -9
  67. package/dist/core/repo-score-manager.d.ts +2 -2
  68. package/dist/core/repo-score-manager.js +3 -3
  69. package/dist/core/repo-vet.d.ts +2 -2
  70. package/dist/core/repo-vet.js +1 -1
  71. package/dist/core/review-analysis.d.ts +19 -0
  72. package/dist/core/review-analysis.js +28 -0
  73. package/dist/core/state-schema.d.ts +43 -6
  74. package/dist/core/state-schema.js +81 -4
  75. package/dist/core/state.d.ts +36 -5
  76. package/dist/core/state.js +177 -28
  77. package/dist/core/strategy.js +6 -5
  78. package/dist/core/types.d.ts +8 -0
  79. package/dist/core/untrusted-content.d.ts +45 -0
  80. package/dist/core/untrusted-content.js +54 -0
  81. package/dist/formatters/json.d.ts +81 -7
  82. package/dist/formatters/json.js +55 -2
  83. package/package.json +2 -2
  84. package/dist/commands/shelve.d.ts +0 -45
  85. package/dist/commands/shelve.js +0 -54
package/dist/cli.js CHANGED
@@ -80,13 +80,14 @@ program.hook('preAction', async (thisCommand, actionCommand) => {
80
80
  // when Gist mode is the configured persistence (#1000). Hard errors
81
81
  // still throw (#1202); the resolving degraded modes are surfaced in the
82
82
  // JSON envelope so --json consumers see them too (#1433).
83
- const { ensureGistPersistence } = await import('./core/index.js');
83
+ const { ensureGistPersistence, renderGistWarning } = await import('./core/index.js');
84
84
  const status = await ensureGistPersistence(token);
85
85
  if (status === 'degraded' || status === 'state-unreadable') {
86
86
  const { setEnvelopeGistWarning } = await import('./formatters/json.js');
87
- setEnvelopeGistWarning('Gist persistence is configured but this run is LOCAL-ONLY (' +
88
- (status === 'degraded' ? 'transient network failure during init' : 'state file could not be read') +
89
- '); changes may be overwritten by the next successful Gist sync.');
87
+ // Shared renderer (#1444). 'degraded' is ensureGistPersistence's
88
+ // deliberate conflation of the transient init fallback and a #1443
89
+ // degraded bootstrap rendered as the init-fallback cause, as before.
90
+ setEnvelopeGistWarning(renderGistWarning(status === 'degraded' ? 'init-fallback' : 'state-unreadable'));
90
91
  }
91
92
  }
92
93
  else {
@@ -7,7 +7,7 @@ import { wrapUntrustedContent } from '../core/untrusted-content.js';
7
7
  import { ValidationError, isRateLimitOrAuthError } from '../core/errors.js';
8
8
  import { warn } from '../core/logger.js';
9
9
  const MODULE = 'comments';
10
- import { paginateAll } from '../core/pagination.js';
10
+ import { paginateAllDetailed } from '../core/pagination.js';
11
11
  import { buildStalenessWarning } from '../formatters/json.js';
12
12
  import { validateUrl, validateMessage, validateGitHubUrl, PR_URL_PATTERN, ISSUE_OR_PR_URL_PATTERN, ISSUE_URL_PATTERN, } from './validation.js';
13
13
  /**
@@ -35,22 +35,22 @@ export async function runComments(options) {
35
35
  // Get PR details
36
36
  const { data: pr } = await octokit.pulls.get({ owner, repo, pull_number });
37
37
  // Fetch review comments, issue comments, and reviews in parallel
38
- const [reviewComments, issueComments, reviews] = await Promise.all([
39
- paginateAll((page) => octokit.pulls.listReviewComments({
38
+ const [reviewCommentsResult, issueCommentsResult, reviewsResult] = await Promise.all([
39
+ paginateAllDetailed((page) => octokit.pulls.listReviewComments({
40
40
  owner,
41
41
  repo,
42
42
  pull_number,
43
43
  per_page: 100,
44
44
  page,
45
45
  })),
46
- paginateAll((page) => octokit.issues.listComments({
46
+ paginateAllDetailed((page) => octokit.issues.listComments({
47
47
  owner,
48
48
  repo,
49
49
  issue_number: pull_number,
50
50
  per_page: 100,
51
51
  page,
52
52
  })),
53
- paginateAll((page) => octokit.pulls.listReviews({
53
+ paginateAllDetailed((page) => octokit.pulls.listReviews({
54
54
  owner,
55
55
  repo,
56
56
  pull_number,
@@ -58,12 +58,28 @@ export async function runComments(options) {
58
58
  page,
59
59
  })),
60
60
  ]);
61
+ const { items: reviewComments } = reviewCommentsResult;
62
+ const { items: issueComments } = issueCommentsResult;
63
+ const { items: reviews } = reviewsResult;
64
+ // Pagination truncation is a data-quality signal, not a failure: these
65
+ // endpoints return oldest-first, so hitting the page cap drops the NEWEST
66
+ // comments — exactly the ones a "what needs response" consumer cares
67
+ // about. Thread it into the structured warnings channel (#1456).
68
+ const truncatedStreams = [
69
+ ...(reviewsResult.truncated ? ['reviews'] : []),
70
+ ...(reviewCommentsResult.truncated ? ['inline review comments'] : []),
71
+ ...(issueCommentsResult.truncated ? ['discussion comments'] : []),
72
+ ];
61
73
  // Filter out own comments, optionally show bots
62
74
  const username = stateManager.getState().config.githubUsername;
63
75
  const filterComment = (c) => {
64
76
  if (!c.user)
65
77
  return false;
66
- if (c.user.login === username)
78
+ // Lowercase both sides: GitHub logins are case-insensitive, and a
79
+ // non-canonical-case configured username must not leak the user's own
80
+ // comments into "needs response" (#1456). Mirrors review-analysis.ts
81
+ // and issue-conversation.ts.
82
+ if (c.user.login?.toLowerCase() === username?.toLowerCase())
67
83
  return false;
68
84
  if (c.user.type === 'Bot' && !options.showBots)
69
85
  return false;
@@ -85,6 +101,18 @@ export async function runComments(options) {
85
101
  const fenceLabel = `${owner}/${repo}#${pull_number}`;
86
102
  const fence = (body, source, author, association) => wrapUntrustedContent(body, fenceLabel, { author, association, source });
87
103
  const staleness = stateManager.getStateStaleness();
104
+ const warnings = [];
105
+ if (truncatedStreams.length > 0) {
106
+ warnings.push({
107
+ phase: 'fetch',
108
+ operation: 'fetch PR comments (truncated)',
109
+ message: `Pagination cap reached fetching ${truncatedStreams.join(', ')} for ${owner}/${repo}#${pull_number}; ` +
110
+ `the newest entries in those streams may be missing.`,
111
+ });
112
+ }
113
+ if (staleness) {
114
+ warnings.push(buildStalenessWarning(staleness));
115
+ }
88
116
  return {
89
117
  pr: {
90
118
  title: pr.title,
@@ -116,7 +144,7 @@ export async function runComments(options) {
116
144
  inlineCommentCount: relevantReviewComments.length,
117
145
  discussionCommentCount: relevantIssueComments.length,
118
146
  },
119
- ...(staleness ? { warnings: [buildStalenessWarning(staleness)] } : {}),
147
+ ...(warnings.length > 0 ? { warnings } : {}),
120
148
  };
121
149
  }
122
150
  /**
@@ -212,6 +240,7 @@ export async function runClaim(options) {
212
240
  });
213
241
  // Add to tracked issues — non-fatal if state save fails (comment already posted)
214
242
  let gistSyncWarning = null;
243
+ let stateSaveWarning;
215
244
  try {
216
245
  const stateManager = getStateManager();
217
246
  stateManager.addIssue({
@@ -232,13 +261,18 @@ export async function runClaim(options) {
232
261
  gistSyncWarning = await maybeCheckpoint(stateManager, MODULE);
233
262
  }
234
263
  catch (error) {
235
- // Structured warning instead of bare console.error so the breadcrumb shows
236
- // up in the plugin's log pipeline (#1056 M24).
237
- warn(MODULE, `Comment posted on ${options.issueUrl} but failed to save to local state: ${error instanceof Error ? error.message : error}`);
264
+ // The claim comment is live on GitHub but local state never tracked it —
265
+ // an otherwise-clean envelope would hide an invisible untracked claim, so
266
+ // thread the failure into the output (#1448). Structured warning instead
267
+ // of bare console.error so the breadcrumb shows up in the plugin's log
268
+ // pipeline (#1056 M24).
269
+ stateSaveWarning = `Comment posted on ${options.issueUrl} but failed to save to local state: ${error instanceof Error ? error.message : error}`;
270
+ warn(MODULE, stateSaveWarning);
238
271
  }
239
272
  return {
240
273
  commentUrl: comment.html_url,
241
274
  issueUrl: options.issueUrl,
242
275
  ...(gistSyncWarning ? { gistSyncWarning } : {}),
276
+ ...(stateSaveWarning ? { stateSaveWarning } : {}),
243
277
  };
244
278
  }
@@ -13,6 +13,8 @@ export interface ConfigSetOutput {
13
13
  success: true;
14
14
  key: string;
15
15
  value: string;
16
+ /** Set when the post-mutation Gist checkpoint failed; the local mutation succeeded (#1440). */
17
+ gistSyncWarning?: string;
16
18
  }
17
19
  export interface ConfigListKeysOutput {
18
20
  keys: readonly ConfigKeyDef[];
@@ -2,10 +2,11 @@
2
2
  * Config command
3
3
  * Shows or updates configuration
4
4
  */
5
- import { CONFIG_KEY_REGISTRY, formatUnknownKeyError, getStateManager } from '../core/index.js';
5
+ import { CONFIG_KEY_REGISTRY, formatUnknownKeyError, getStateManager, maybeCheckpoint, } from '../core/index.js';
6
6
  import { ValidationError } from '../core/errors.js';
7
7
  import { ISSUE_SCOPES, DIFF_TOOLS } from '../core/types.js';
8
8
  import { validateGitHubUsername } from './validation.js';
9
+ const MODULE = 'config';
9
10
  function validateScope(value) {
10
11
  if (!ISSUE_SCOPES.includes(value)) {
11
12
  throw new Error(`Invalid scope "${value}". Valid scopes: ${ISSUE_SCOPES.join(', ')}`);
@@ -115,6 +116,49 @@ export async function runConfig(options) {
115
116
  }
116
117
  break;
117
118
  }
119
+ case 'add-avoid-repo': {
120
+ // Same owner/repo shape as exclude-repo, but no cleanupExcludedData
121
+ // call: avoidRepos is a soft ranking penalty, not a hard exclusion,
122
+ // so tracked data for the repo stays intact (#1464).
123
+ const parts = value.split('/');
124
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
125
+ throw new Error(`Invalid repo format "${value}". Use "owner/repo" format.`);
126
+ }
127
+ const currentAvoid = currentConfig.avoidRepos ?? [];
128
+ const valueLower = value.toLowerCase();
129
+ if (!currentAvoid.some((r) => r.toLowerCase() === valueLower)) {
130
+ stateManager.updateConfig({ avoidRepos: [...currentAvoid, value] });
131
+ }
132
+ break;
133
+ }
134
+ case 'remove-avoid-repo': {
135
+ const currentAvoid = currentConfig.avoidRepos ?? [];
136
+ const valueLower = value.toLowerCase();
137
+ if (!currentAvoid.some((r) => r.toLowerCase() === valueLower)) {
138
+ throw new Error(`Repo "${value}" is not on the avoid list. Current avoid list: ${currentAvoid.join(', ') || '(empty)'}`);
139
+ }
140
+ stateManager.updateConfig({ avoidRepos: currentAvoid.filter((r) => r.toLowerCase() !== valueLower) });
141
+ break;
142
+ }
143
+ case 'add-boost-issue-type': {
144
+ // Scout matches boostIssueTypes against issue labels case-insensitively,
145
+ // so the duplicate check is case-insensitive too (#1464).
146
+ const currentTypes = currentConfig.boostIssueTypes ?? [];
147
+ const valueLower = value.toLowerCase();
148
+ if (!currentTypes.some((t) => t.toLowerCase() === valueLower)) {
149
+ stateManager.updateConfig({ boostIssueTypes: [...currentTypes, value] });
150
+ }
151
+ break;
152
+ }
153
+ case 'remove-boost-issue-type': {
154
+ const currentTypes = currentConfig.boostIssueTypes ?? [];
155
+ const valueLower = value.toLowerCase();
156
+ if (!currentTypes.some((t) => t.toLowerCase() === valueLower)) {
157
+ throw new Error(`Issue type "${value}" is not on the boost list. Current boost list: ${currentTypes.join(', ') || '(empty)'}`);
158
+ }
159
+ stateManager.updateConfig({ boostIssueTypes: currentTypes.filter((t) => t.toLowerCase() !== valueLower) });
160
+ break;
161
+ }
118
162
  case 'issueListPath': {
119
163
  stateManager.updateConfig({ issueListPath: value || undefined });
120
164
  break;
@@ -136,5 +180,9 @@ export async function runConfig(options) {
136
180
  throw new ValidationError(formatUnknownKeyError(options.key, 'config'));
137
181
  }
138
182
  }
139
- return { success: true, key: options.key, value };
183
+ // Push the config mutation to the Gist in gist mode (no-op locally).
184
+ // Without this the change only hits the local cache and the next
185
+ // bootstrap reverts it from the Gist (#1440).
186
+ const gistSyncWarning = await maybeCheckpoint(stateManager, MODULE);
187
+ return { success: true, key: options.key, value, ...(gistSyncWarning ? { gistSyncWarning } : {}) };
140
188
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Shared line-matching helpers for the curated issue-list commands
3
+ * (`list-move-tier`, `list-mark-done`). Both commands locate entries in the
4
+ * same markdown file, so they must agree on what counts as an entry line
5
+ * and on URL matching semantics — they had drifted on both (#1442).
6
+ */
7
+ /** Top-level list entry — `- `, `* `, `+ `, or `1.` at the start (no leading whitespace). */
8
+ export declare const ENTRY_LINE_RE: RegExp;
9
+ /** The entry's leading list marker, for stripping before inspecting the body. */
10
+ export declare const ENTRY_MARKER_RE: RegExp;
11
+ /**
12
+ * Match the URL only when followed by a non-digit character (or end of
13
+ * line). A bare `includes(issueUrl)` would match `issues/1` against a line
14
+ * containing `issues/10`, so acting on issue 1 would also hit issues
15
+ * 10/100/... (#1442).
16
+ */
17
+ export declare function lineMentionsUrl(line: string, issueUrl: string): boolean;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Shared line-matching helpers for the curated issue-list commands
3
+ * (`list-move-tier`, `list-mark-done`). Both commands locate entries in the
4
+ * same markdown file, so they must agree on what counts as an entry line
5
+ * and on URL matching semantics — they had drifted on both (#1442).
6
+ */
7
+ /** Top-level list entry — `- `, `* `, `+ `, or `1.` at the start (no leading whitespace). */
8
+ export const ENTRY_LINE_RE = /^[*+-]\s|^\d+\.\s/;
9
+ /** The entry's leading list marker, for stripping before inspecting the body. */
10
+ export const ENTRY_MARKER_RE = /^(?:[*+-]\s+|\d+\.\s+)/;
11
+ /**
12
+ * Match the URL only when followed by a non-digit character (or end of
13
+ * line). A bare `includes(issueUrl)` would match `issues/1` against a line
14
+ * containing `issues/10`, so acting on issue 1 would also hit issues
15
+ * 10/100/... (#1442).
16
+ */
17
+ export function lineMentionsUrl(line, issueUrl) {
18
+ const idx = line.indexOf(issueUrl);
19
+ if (idx === -1)
20
+ return false;
21
+ const next = line.charCodeAt(idx + issueUrl.length);
22
+ // NaN (end of string) → boundary OK. Otherwise reject any digit
23
+ // immediately after the URL so 'issues/1' doesn't match 'issues/10'.
24
+ return Number.isNaN(next) || next < 48 /* '0' */ || next > 57; /* '9' */
25
+ }
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { type AttentionSummary, type DailyDigest, type CommentedIssue, type PRCheckFailure, type RepoGroup } from '../core/index.js';
10
10
  import { type StrategyResult } from '../core/strategy.js';
11
- import { type DailyOutput, type DailyWarning, type CapacityAssessment, type ActionableIssue, type ActionMenu } from '../formatters/json.js';
11
+ import { type DailyOutput, type DailyWarning, type CapacityAssessment, type ActionableIssue, type ActionMenu, type MergedPRListUpdate } from '../formatters/json.js';
12
12
  export { applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, printDigest, CRITICAL_STATUSES, } from '../core/index.js';
13
13
  import { buildStarFilter } from '../core/daily-logic.js';
14
14
  export { buildStarFilter };
@@ -38,6 +38,12 @@ export interface DailyCheckResult {
38
38
  * where the gate stays silent.
39
39
  */
40
40
  strategySummary?: StrategyResult | null;
41
+ /**
42
+ * Curated-list entries auto-marked done because their PR merged (#1463).
43
+ * Set only when at least one entry was struck this run; merge-free runs
44
+ * omit it so serialized output (and contract goldens) stay unchanged.
45
+ */
46
+ listUpdates?: MergedPRListUpdate[];
41
47
  }
42
48
  /**
43
49
  * Convert a full DailyCheckResult to the compact DailyOutput for JSON serialization (#287).