@oss-autopilot/core 3.2.0 → 3.3.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 (55) hide show
  1. package/dist/cli-registry.js +33 -3
  2. package/dist/cli.bundle.cjs +96 -93
  3. package/dist/commands/check-integration.js +8 -8
  4. package/dist/commands/comments.js +3 -0
  5. package/dist/commands/config.js +14 -7
  6. package/dist/commands/daily-render.js +10 -5
  7. package/dist/commands/daily.js +6 -1
  8. package/dist/commands/dashboard-lifecycle.js +1 -1
  9. package/dist/commands/dashboard-process.js +4 -4
  10. package/dist/commands/dashboard-server.js +7 -6
  11. package/dist/commands/dashboard.js +2 -2
  12. package/dist/commands/detect-formatters.js +3 -3
  13. package/dist/commands/doctor.js +5 -5
  14. package/dist/commands/guidelines.js +15 -3
  15. package/dist/commands/list-move-tier.js +5 -5
  16. package/dist/commands/local-repos.js +9 -9
  17. package/dist/commands/parse-list.js +10 -10
  18. package/dist/commands/scout-bridge.js +2 -2
  19. package/dist/commands/setup.js +24 -13
  20. package/dist/commands/skip-add.js +6 -3
  21. package/dist/commands/skip-file-parser.js +3 -3
  22. package/dist/commands/startup.js +11 -8
  23. package/dist/commands/state-cmd.js +1 -1
  24. package/dist/commands/status.js +7 -0
  25. package/dist/commands/validation.js +3 -3
  26. package/dist/commands/vet-list.js +12 -8
  27. package/dist/commands/vet.js +1 -2
  28. package/dist/core/__fixtures__/prompt-injection-payloads.d.ts +22 -0
  29. package/dist/core/__fixtures__/prompt-injection-payloads.js +109 -0
  30. package/dist/core/anti-llm-policy.js +5 -5
  31. package/dist/core/auth.js +5 -5
  32. package/dist/core/daily-logic.js +8 -4
  33. package/dist/core/dates.js +3 -3
  34. package/dist/core/errors.d.ts +29 -0
  35. package/dist/core/errors.js +63 -0
  36. package/dist/core/formatter-detection.js +9 -9
  37. package/dist/core/gist-state-store.d.ts +18 -2
  38. package/dist/core/gist-state-store.js +73 -13
  39. package/dist/core/guidelines-store.js +2 -2
  40. package/dist/core/http-cache.js +6 -6
  41. package/dist/core/index.d.ts +1 -0
  42. package/dist/core/index.js +1 -0
  43. package/dist/core/issue-conversation.js +3 -1
  44. package/dist/core/paths.js +4 -4
  45. package/dist/core/pr-monitor.js +1 -2
  46. package/dist/core/pr-template.js +1 -1
  47. package/dist/core/state-persistence.js +7 -7
  48. package/dist/core/state.d.ts +27 -0
  49. package/dist/core/state.js +66 -13
  50. package/dist/core/untrusted-content.d.ts +48 -0
  51. package/dist/core/untrusted-content.js +106 -0
  52. package/dist/core/urls.js +2 -2
  53. package/dist/formatters/json.d.ts +53 -3
  54. package/dist/formatters/json.js +49 -14
  55. package/package.json +1 -1
@@ -2,8 +2,8 @@
2
2
  * Check integration command (#83)
3
3
  * Detects new files in the current branch that aren't referenced elsewhere
4
4
  */
5
- import * as path from 'path';
6
- import { execFileSync } from 'child_process';
5
+ import * as path from 'node:path';
6
+ import { execFileSync } from 'node:child_process';
7
7
  import { debug } from '../core/index.js';
8
8
  import { errorMessage } from '../core/errors.js';
9
9
  /** File extensions we consider "code" that should be imported/referenced */
@@ -71,8 +71,8 @@ export async function runCheckIntegration(options) {
71
71
  let newFiles;
72
72
  try {
73
73
  const output = execFileSync('git', ['diff', '--name-only', '--diff-filter=A', `${base}...HEAD`], {
74
- encoding: 'utf-8',
75
- timeout: 10000,
74
+ encoding: 'utf8',
75
+ timeout: 10_000,
76
76
  }).trim();
77
77
  newFiles = output ? output.split('\n').filter(Boolean) : [];
78
78
  }
@@ -94,8 +94,8 @@ export async function runCheckIntegration(options) {
94
94
  let allFiles;
95
95
  try {
96
96
  allFiles = execFileSync('git', ['ls-files'], {
97
- encoding: 'utf-8',
98
- timeout: 10000,
97
+ encoding: 'utf8',
98
+ timeout: 10_000,
99
99
  })
100
100
  .trim()
101
101
  .split('\n')
@@ -124,8 +124,8 @@ export async function runCheckIntegration(options) {
124
124
  for (const pattern of patterns) {
125
125
  try {
126
126
  const grepOutput = execFileSync('git', ['grep', '-l', '--', pattern], {
127
- encoding: 'utf-8',
128
- timeout: 10000,
127
+ encoding: 'utf8',
128
+ timeout: 10_000,
129
129
  }).trim();
130
130
  if (grepOutput) {
131
131
  const matches = grepOutput.split('\n').filter((f) => f !== newFile);
@@ -7,6 +7,7 @@ import { ValidationError } from '../core/errors.js';
7
7
  import { warn } from '../core/logger.js';
8
8
  const MODULE = 'comments';
9
9
  import { paginateAll } from '../core/pagination.js';
10
+ import { buildStalenessWarning } from '../formatters/json.js';
10
11
  import { validateUrl, validateMessage, validateGitHubUrl, PR_URL_PATTERN, ISSUE_OR_PR_URL_PATTERN, ISSUE_URL_PATTERN, } from './validation.js';
11
12
  /**
12
13
  * Fetch all comments, reviews, and inline review comments for a PR.
@@ -76,6 +77,7 @@ export async function runComments(options) {
76
77
  const relevantReviews = reviews
77
78
  .filter((r) => filterComment(r) && r.body && r.body.trim())
78
79
  .sort((a, b) => new Date(b.submitted_at || 0).getTime() - new Date(a.submitted_at || 0).getTime());
80
+ const staleness = stateManager.getStateStaleness();
79
81
  return {
80
82
  pr: {
81
83
  title: pr.title,
@@ -107,6 +109,7 @@ export async function runComments(options) {
107
109
  inlineCommentCount: relevantReviewComments.length,
108
110
  discussionCommentCount: relevantIssueComments.length,
109
111
  },
112
+ ...(staleness ? { warnings: [buildStalenessWarning(staleness)] } : {}),
110
113
  };
111
114
  }
112
115
  /**
@@ -44,25 +44,29 @@ export async function runConfig(options) {
44
44
  const value = options.value;
45
45
  // Handle specific config keys
46
46
  switch (options.key) {
47
- case 'username':
47
+ case 'username': {
48
48
  stateManager.updateConfig({ githubUsername: validateGitHubUsername(value) });
49
49
  break;
50
- case 'add-language':
50
+ }
51
+ case 'add-language': {
51
52
  if (!currentConfig.languages.includes(value)) {
52
53
  stateManager.updateConfig({ languages: [...currentConfig.languages, value] });
53
54
  }
54
55
  break;
55
- case 'add-label':
56
+ }
57
+ case 'add-label': {
56
58
  if (!currentConfig.labels.includes(value)) {
57
59
  stateManager.updateConfig({ labels: [...currentConfig.labels, value] });
58
60
  }
59
61
  break;
60
- case 'remove-label':
62
+ }
63
+ case 'remove-label': {
61
64
  if (!currentConfig.labels.includes(value)) {
62
65
  throw new Error(`Label "${value}" is not currently configured. Current labels: ${currentConfig.labels.join(', ')}`);
63
66
  }
64
67
  stateManager.updateConfig({ labels: currentConfig.labels.filter((l) => l !== value) });
65
68
  break;
69
+ }
66
70
  case 'add-scope': {
67
71
  const scope = validateScope(value);
68
72
  const currentScopes = currentConfig.scope ?? [];
@@ -111,9 +115,10 @@ export async function runConfig(options) {
111
115
  }
112
116
  break;
113
117
  }
114
- case 'issueListPath':
118
+ case 'issueListPath': {
115
119
  stateManager.updateConfig({ issueListPath: value || undefined });
116
120
  break;
121
+ }
117
122
  case 'diffTool': {
118
123
  if (!DIFF_TOOLS.includes(value)) {
119
124
  throw new Error(`Invalid diffTool "${value}". Valid options: ${DIFF_TOOLS.join(', ')}`);
@@ -121,13 +126,15 @@ export async function runConfig(options) {
121
126
  stateManager.updateConfig({ diffTool: value });
122
127
  break;
123
128
  }
124
- case 'diffToolCustomCommand':
129
+ case 'diffToolCustomCommand': {
125
130
  stateManager.updateConfig({
126
131
  diffToolCustomCommand: value || undefined,
127
132
  });
128
133
  break;
129
- default:
134
+ }
135
+ default: {
130
136
  throw new ValidationError(formatUnknownKeyError(options.key, 'config'));
137
+ }
131
138
  }
132
139
  return { success: true, key: options.key, value };
133
140
  }
@@ -20,16 +20,21 @@ import { formatRelativeTime } from '../core/dates.js';
20
20
  */
21
21
  export function formatActionHint(hint) {
22
22
  switch (hint) {
23
- case 'demo_requested':
23
+ case 'demo_requested': {
24
24
  return 'demo/screenshot requested';
25
- case 'tests_requested':
25
+ }
26
+ case 'tests_requested': {
26
27
  return 'tests requested';
27
- case 'changes_requested':
28
+ }
29
+ case 'changes_requested': {
28
30
  return 'code changes requested';
29
- case 'docs_requested':
31
+ }
32
+ case 'docs_requested': {
30
33
  return 'documentation requested';
31
- case 'rebase_requested':
34
+ }
35
+ case 'rebase_requested': {
32
36
  return 'rebase requested';
37
+ }
33
38
  }
34
39
  }
35
40
  /**
@@ -12,7 +12,7 @@ import { warn } from '../core/logger.js';
12
12
  import { emptyPRCountsResult } from '../core/github-stats.js';
13
13
  import { createAutopilotScout } from './scout-bridge.js';
14
14
  import { updateMonthlyAnalytics } from './dashboard-data.js';
15
- import { deduplicateDigest, compactActionableIssues, compactRepoGroups, } from '../formatters/json.js';
15
+ import { deduplicateDigest, compactActionableIssues, compactRepoGroups, buildStalenessWarning, } from '../formatters/json.js';
16
16
  const MODULE = 'daily';
17
17
  /**
18
18
  * Record a non-fatal failure: push a structured entry into the run's warnings
@@ -504,6 +504,11 @@ async function executeDailyCheckInternal(token) {
504
504
  // One collector shared by every phase — threaded through explicitly so the
505
505
  // callgraph documents which phases can produce non-fatal warnings. See #1042.
506
506
  const warnings = [];
507
+ // Surface Gist staleness up-front so consumers see it even if Phase 1 fails (#1193).
508
+ const staleness = getStateManager().getStateStaleness();
509
+ if (staleness) {
510
+ warnings.push(buildStalenessWarning(staleness));
511
+ }
507
512
  // Phase 1: Fetch all PR data from GitHub
508
513
  const { prs, failures, mergedCounts, closedCounts, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed, recentlyClosedPRs, recentlyMergedPRs, commentedIssues, } = await fetchPRData(prMonitor, token, warnings);
509
514
  // Phase 2: Update repo scores (signals, star counts, trust sync)
@@ -3,7 +3,7 @@
3
3
  * Handles launching the interactive SPA dashboard as a background process
4
4
  * and detecting whether a server is already running.
5
5
  */
6
- import { spawn } from 'child_process';
6
+ import { spawn } from 'node:child_process';
7
7
  import { findRunningDashboardServer, isDashboardServerRunning, readDashboardServerInfo, removeDashboardServerInfo, } from './dashboard-process.js';
8
8
  import { resolveAssetsDir } from './dashboard.js';
9
9
  import { getCLIVersion } from '../core/index.js';
@@ -2,9 +2,9 @@
2
2
  * Dashboard server process management.
3
3
  * PID file operations, health probes, and running server detection.
4
4
  */
5
- import * as http from 'http';
6
- import * as fs from 'fs';
7
- import * as path from 'path';
5
+ import * as http from 'node:http';
6
+ import * as fs from 'node:fs';
7
+ import * as path from 'node:path';
8
8
  import { getDataDir } from '../core/index.js';
9
9
  import { warn } from '../core/logger.js';
10
10
  const MODULE = 'dashboard-server';
@@ -17,7 +17,7 @@ export function writeDashboardServerInfo(info) {
17
17
  }
18
18
  export function readDashboardServerInfo() {
19
19
  try {
20
- const content = fs.readFileSync(getDashboardPidPath(), 'utf-8');
20
+ const content = fs.readFileSync(getDashboardPidPath(), 'utf8');
21
21
  const parsed = JSON.parse(content);
22
22
  if (typeof parsed !== 'object' ||
23
23
  parsed === null ||
@@ -5,10 +5,10 @@
5
5
  *
6
6
  * Uses Node's built-in http module — no Express/Fastify.
7
7
  */
8
- import * as http from 'http';
9
- import * as fs from 'fs';
10
- import * as path from 'path';
11
- import * as crypto from 'crypto';
8
+ import * as http from 'node:http';
9
+ import * as fs from 'node:fs';
10
+ import * as path from 'node:path';
11
+ import * as crypto from 'node:crypto';
12
12
  import { getStateManager, getGitHubToken, getCLIVersion, applyStatusOverrides } from '../core/index.js';
13
13
  import { errorMessage, ValidationError } from '../core/errors.js';
14
14
  import { warn } from '../core/logger.js';
@@ -44,7 +44,7 @@ function readVettedIssues() {
44
44
  const info = detectIssueList();
45
45
  if (!info)
46
46
  return null;
47
- const content = fs.readFileSync(info.path, 'utf-8');
47
+ const content = fs.readFileSync(info.path, 'utf8');
48
48
  return parseIssueList(content);
49
49
  }
50
50
  catch (error) {
@@ -148,7 +148,7 @@ function readBody(req, maxBytes = MAX_BODY_BYTES) {
148
148
  });
149
149
  req.on('end', () => {
150
150
  if (!aborted)
151
- resolve(Buffer.concat(chunks).toString('utf-8'));
151
+ resolve(Buffer.concat(chunks).toString('utf8'));
152
152
  });
153
153
  req.on('error', (err) => {
154
154
  if (!aborted)
@@ -599,6 +599,7 @@ export async function startDashboardServer(options) {
599
599
  cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues, result.allMergedPRs, result.allClosedPRs, cachedPartialFailures);
600
600
  cachedIssueListMtimeMs = getIssueListMtimeMs();
601
601
  warn(MODULE, 'Background data refresh complete');
602
+ return;
602
603
  })
603
604
  .catch((error) => {
604
605
  warn(MODULE, `Background data refresh failed (serving cached data): ${errorMessage(error)}`);
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Dashboard command — serves the interactive Preact SPA dashboard.
3
3
  */
4
- import * as fs from 'fs';
5
- import * as path from 'path';
4
+ import * as fs from 'node:fs';
5
+ import * as path from 'node:path';
6
6
  import { getGitHubToken } from '../core/index.js';
7
7
  /**
8
8
  * Resolve the SPA assets directory from packages/dashboard/dist/.
@@ -3,8 +3,8 @@
3
3
  * Scans a local repository for configured formatters/linters.
4
4
  * Optionally diagnoses CI log output for formatting failures.
5
5
  */
6
- import * as fs from 'fs';
7
- import * as path from 'path';
6
+ import * as fs from 'node:fs';
7
+ import * as path from 'node:path';
8
8
  import { detectFormatters, diagnoseCIFormatterFailure } from '../core/formatter-detection.js';
9
9
  import { errorMessage } from '../core/errors.js';
10
10
  export async function runDetectFormatters(options) {
@@ -13,7 +13,7 @@ export async function runDetectFormatters(options) {
13
13
  if (options.ciLog) {
14
14
  let logContent;
15
15
  try {
16
- logContent = fs.readFileSync(path.resolve(options.ciLog), 'utf-8');
16
+ logContent = fs.readFileSync(path.resolve(options.ciLog), 'utf8');
17
17
  }
18
18
  catch (err) {
19
19
  throw new Error(`Failed to read CI log file: ${errorMessage(err)}`, { cause: err });
@@ -9,10 +9,10 @@
9
9
  * Each `check*` function is exported individually so tests can mock its
10
10
  * external dependencies in isolation.
11
11
  */
12
- import * as fs from 'fs';
13
- import * as path from 'path';
14
- import { execFile } from 'child_process';
15
- import { promisify } from 'util';
12
+ import * as fs from 'node:fs';
13
+ import * as path from 'node:path';
14
+ import { execFile } from 'node:child_process';
15
+ import { promisify } from 'node:util';
16
16
  import { getGitHubTokenAsync, getOctokit, getStatePath } from '../core/index.js';
17
17
  import { AgentStateSchema } from '../core/state-schema.js';
18
18
  import { errorMessage } from '../core/errors.js';
@@ -195,7 +195,7 @@ export function checkStateFile(options) {
195
195
  };
196
196
  }
197
197
  try {
198
- const raw = fs.readFileSync(statePath, 'utf-8');
198
+ const raw = fs.readFileSync(statePath, 'utf8');
199
199
  const parsed = JSON.parse(raw);
200
200
  const result = AgentStateSchema.safeParse(parsed);
201
201
  if (!result.success) {
@@ -11,7 +11,7 @@
11
11
  * Standalone-mode users see "not available" on store/reset/fetch-corpus
12
12
  * because per-repo guidelines require Gist persistence to be useful.
13
13
  */
14
- import { getStateManager, requireGitHubToken, getOctokit, GuidelinesNotAvailableError } from '../core/index.js';
14
+ import { getStateManager, requireGitHubToken, getOctokit, GuidelinesNotAvailableError, maybeCheckpoint, } from '../core/index.js';
15
15
  import { fetchPRCommentBundlesBatch } from '../core/pr-comments-fetcher.js';
16
16
  import { warn } from '../core/logger.js';
17
17
  const MODULE = 'guidelines';
@@ -36,7 +36,7 @@ export async function runGuidelinesView(options) {
36
36
  return {
37
37
  repo: options.repo,
38
38
  content,
39
- byteSize: content === null ? 0 : Buffer.byteLength(content, 'utf-8'),
39
+ byteSize: content === null ? 0 : Buffer.byteLength(content, 'utf8'),
40
40
  exists: content !== null,
41
41
  storageMode: sm.isGuidelinesAvailable() ? 'gist' : 'local-unavailable',
42
42
  };
@@ -54,9 +54,13 @@ export async function runGuidelinesStore(options) {
54
54
  const sm = getStateManager();
55
55
  // Throws GuidelinesNotAvailableError or GuidelinesTooLargeError on failure.
56
56
  sm.setGuidelines(options.repo, options.content);
57
+ // Push to Gist — autoSave only writes the local state-cache mirror in Gist
58
+ // mode, so without this checkpoint the change never propagates across
59
+ // machines (#1200).
60
+ await maybeCheckpoint(sm, MODULE);
57
61
  return {
58
62
  repo: options.repo,
59
- byteSize: Buffer.byteLength(options.content, 'utf-8'),
63
+ byteSize: Buffer.byteLength(options.content, 'utf8'),
60
64
  stored: true,
61
65
  };
62
66
  }
@@ -70,6 +74,8 @@ export async function runGuidelinesReset(options) {
70
74
  const existed = sm.getGuidelines(options.repo) !== null;
71
75
  if (existed) {
72
76
  sm.deleteGuidelines(options.repo);
77
+ // Push to Gist — see runGuidelinesStore note (#1200).
78
+ await maybeCheckpoint(sm, MODULE);
73
79
  }
74
80
  return { repo: options.repo, deleted: existed };
75
81
  }
@@ -131,6 +137,12 @@ export async function runFetchCorpus(options) {
131
137
  for (const bundle of bundles) {
132
138
  sm.markPRCommentsFetched(bundle.prUrl, now);
133
139
  }
140
+ // Push commentsFetchedAt stamps to Gist so other machines don't re-fetch
141
+ // the same PRs forever. autoSave only writes the local mirror in Gist
142
+ // mode (#1200).
143
+ if (bundles.length > 0) {
144
+ await maybeCheckpoint(sm, MODULE);
145
+ }
134
146
  return {
135
147
  repo: options.repo,
136
148
  bundles,
@@ -10,8 +10,8 @@
10
10
  *
11
11
  * No GitHub calls — pure read/transform/write of a local file.
12
12
  */
13
- import * as fs from 'fs';
14
- import * as path from 'path';
13
+ import * as fs from 'node:fs';
14
+ import * as path from 'node:path';
15
15
  import { errorMessage } from '../core/errors.js';
16
16
  const TIER_HEADERS = {
17
17
  pursue: '## Pursue',
@@ -47,7 +47,7 @@ function findIssueBlocks(lines, issueUrl) {
47
47
  for (let i = 0; i < lines.length; i++) {
48
48
  const line = lines[i];
49
49
  // Top-level list item — `- `, `* `, `+ `, or `1.` at the start (with no leading whitespace).
50
- const isTopLevelListItem = /^[-*+]\s|^\d+\.\s/.test(line);
50
+ const isTopLevelListItem = /^[*+-]\s|^\d+\.\s/.test(line);
51
51
  if (!isTopLevelListItem || !line.includes(issueUrl))
52
52
  continue;
53
53
  // Capture indented sub-bullets that follow this line.
@@ -166,7 +166,7 @@ export async function runListMoveTier(options) {
166
166
  }
167
167
  let content;
168
168
  try {
169
- content = fs.readFileSync(filePath, 'utf-8');
169
+ content = fs.readFileSync(filePath, 'utf8');
170
170
  }
171
171
  catch (error) {
172
172
  throw new Error(`Failed to read file: ${errorMessage(error)}`, { cause: error });
@@ -174,7 +174,7 @@ export async function runListMoveTier(options) {
174
174
  const result = moveIssueToTier(content, options.issueUrl, options.tier);
175
175
  if (result.moved) {
176
176
  try {
177
- fs.writeFileSync(filePath, result.content, 'utf-8');
177
+ fs.writeFileSync(filePath, result.content, 'utf8');
178
178
  }
179
179
  catch (error) {
180
180
  throw new Error(`Failed to write file: ${errorMessage(error)}`, { cause: error });
@@ -2,10 +2,10 @@
2
2
  * Local repos command (#84)
3
3
  * Scans configurable directories for local git clones and caches results
4
4
  */
5
- import * as fs from 'fs';
6
- import * as path from 'path';
7
- import * as os from 'os';
8
- import { execFileSync } from 'child_process';
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
7
+ import * as os from 'node:os';
8
+ import { execFileSync } from 'node:child_process';
9
9
  import { getStateManager, debug } from '../core/index.js';
10
10
  import { errorMessage } from '../core/errors.js';
11
11
  /** Default directories to scan for local clones */
@@ -21,7 +21,7 @@ const DEFAULT_SCAN_PATHS = [
21
21
  function getGitHubRemote(repoPath) {
22
22
  try {
23
23
  const remoteUrl = execFileSync('git', ['-C', repoPath, 'remote', 'get-url', 'origin'], {
24
- encoding: 'utf-8',
24
+ encoding: 'utf8',
25
25
  timeout: 5000,
26
26
  stdio: ['pipe', 'pipe', 'pipe'],
27
27
  }).trim();
@@ -30,7 +30,7 @@ function getGitHubRemote(repoPath) {
30
30
  if (httpsMatch)
31
31
  return httpsMatch[1];
32
32
  // Match SSH: git@github.com:owner/repo.git
33
- const sshMatch = remoteUrl.match(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
33
+ const sshMatch = remoteUrl.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/);
34
34
  if (sshMatch)
35
35
  return sshMatch[1];
36
36
  return null;
@@ -45,7 +45,7 @@ function getGitHubRemote(repoPath) {
45
45
  function getCurrentBranch(repoPath) {
46
46
  try {
47
47
  return (execFileSync('git', ['-C', repoPath, 'branch', '--show-current'], {
48
- encoding: 'utf-8',
48
+ encoding: 'utf8',
49
49
  timeout: 5000,
50
50
  stdio: ['pipe', 'pipe', 'pipe'],
51
51
  }).trim() || null);
@@ -66,8 +66,8 @@ export function scanForRepos(scanPaths) {
66
66
  let gitDirs;
67
67
  try {
68
68
  const output = execFileSync('find', [scanPath, '-maxdepth', '4', '-name', '.git', '-type', 'd'], {
69
- encoding: 'utf-8',
70
- timeout: 30000,
69
+ encoding: 'utf8',
70
+ timeout: 30_000,
71
71
  stdio: ['pipe', 'pipe', 'pipe'],
72
72
  }).trim();
73
73
  gitDirs = output ? output.split('\n').filter(Boolean) : [];
@@ -2,8 +2,8 @@
2
2
  * Parse issue list command (#82)
3
3
  * Parses markdown issue lists into structured JSON with tier classification
4
4
  */
5
- import * as fs from 'fs';
6
- import * as path from 'path';
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
7
7
  import { errorMessage } from '../core/errors.js';
8
8
  /** Extract GitHub issue/PR URLs from a markdown line */
9
9
  function extractGitHubUrl(line) {
@@ -26,11 +26,11 @@ function extractTitle(line) {
26
26
  // Remove list markers (-, *, +, numbered)
27
27
  cleaned = cleaned.replace(/^\s*[-*+]\s*/, '').replace(/^\s*\d+\.\s*/, '');
28
28
  // Remove checkboxes
29
- cleaned = cleaned.replace(/\[[ xX]\]\s*/, '');
29
+ cleaned = cleaned.replace(/\[[ x]\]\s*/i, '');
30
30
  // Remove strikethrough markers
31
31
  cleaned = cleaned.replace(/~~/g, '');
32
32
  // Remove "Done" markers
33
- cleaned = cleaned.replace(/\b(Done|DONE|done)\b/g, '');
33
+ cleaned = cleaned.replace(/\bdone\b/gi, '');
34
34
  // Remove leading/trailing punctuation and whitespace
35
35
  cleaned = cleaned.replace(/^[\s\-\u2013\u2014:]+/, '').replace(/[\s\-\u2013\u2014:]+$/, '');
36
36
  return cleaned.trim();
@@ -41,7 +41,7 @@ function isCompleted(line) {
41
41
  if (/~~.+~~/.test(line))
42
42
  return true;
43
43
  // Checked checkbox: [x] or [X]
44
- if (/\[[xX]\]/.test(line))
44
+ if (/\[x\]/i.test(line))
45
45
  return true;
46
46
  // "Done" marker (standalone word, case insensitive)
47
47
  if (/\bdone\b/i.test(line))
@@ -55,11 +55,11 @@ function extractScore(line) {
55
55
  }
56
56
  /** Check if a sub-bullet indicates the item is terminal (completed/abandoned — safe to prune) */
57
57
  function isSubBulletTerminal(line) {
58
- return /\*\*(Skip|Done|Dropped|Merged|Closed)\*\*/i.test(line);
58
+ return /\*\*(?:Skip|Done|Dropped|Merged|Closed)\*\*/i.test(line);
59
59
  }
60
60
  /** Check if a sub-bullet indicates the item is in-progress (not available, but NOT safe to prune) */
61
61
  function isSubBulletInProgress(line) {
62
- return /\*\*(In Progress|Wait|Waiting)\*\*/i.test(line);
62
+ return /\*\*(?:In Progress|Wait|Waiting)\*\*/i.test(line);
63
63
  }
64
64
  /** Parse a markdown string into structured issue items */
65
65
  export function parseIssueList(content) {
@@ -79,7 +79,7 @@ export function parseIssueList(content) {
79
79
  continue;
80
80
  }
81
81
  // Skip empty lines and non-list items
82
- if (!line.trim() || !/^\s*[-*+]|\s*\d+\.|\s*\[[ xX]\]/.test(line)) {
82
+ if (!line.trim() || !/^\s*[-*+]|\s*\d+\.|\s*\[[ x]\]/i.test(line)) {
83
83
  continue;
84
84
  }
85
85
  // Extract GitHub URL -- skip lines without one
@@ -193,7 +193,7 @@ export function pruneIssueList(content, minScore = 6) {
193
193
  if (/^---\s*$/.test(line)) {
194
194
  continue;
195
195
  }
196
- if (/^\s*(###?\s*)?(Removed|Previously dropped)/i.test(line)) {
196
+ if (/^\s*(?:###?\s*)?(?:Removed|Previously dropped)/i.test(line)) {
197
197
  continue;
198
198
  }
199
199
  // Skip blockquote metadata lines ("> Sources: ...", "> Prioritized ...")
@@ -242,7 +242,7 @@ export async function runParseList(options) {
242
242
  }
243
243
  let content;
244
244
  try {
245
- content = fs.readFileSync(filePath, 'utf-8');
245
+ content = fs.readFileSync(filePath, 'utf8');
246
246
  }
247
247
  catch (error) {
248
248
  const msg = errorMessage(error);
@@ -44,8 +44,8 @@ export function buildScoutState() {
44
44
  maxIssueAgeDays: config.maxIssueAgeDays,
45
45
  includeDocIssues: config.includeDocIssues,
46
46
  minRepoScoreThreshold: config.minRepoScoreThreshold,
47
- interPhaseDelayMs: 30000,
48
- broadPhaseDelayMs: 90000,
47
+ interPhaseDelayMs: 30_000,
48
+ broadPhaseDelayMs: 90_000,
49
49
  skipBroadWhenSufficientResults: 15,
50
50
  persistence: config.persistence,
51
51
  slmTriageModel: config.slmTriageModel,