@oss-autopilot/core 3.13.3 → 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.
- package/README.md +3 -3
- package/dist/cli-registry.js +50 -83
- package/dist/cli.bundle.cjs +110 -107
- package/dist/cli.js +17 -3
- package/dist/commands/comments.js +44 -10
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +50 -2
- package/dist/commands/curated-list.d.ts +17 -0
- package/dist/commands/curated-list.js +25 -0
- package/dist/commands/daily.d.ts +7 -1
- package/dist/commands/daily.js +136 -57
- package/dist/commands/dashboard-cache.d.ts +69 -0
- package/dist/commands/dashboard-cache.js +219 -0
- package/dist/commands/dashboard-data.d.ts +18 -10
- package/dist/commands/dashboard-data.js +35 -7
- package/dist/commands/dashboard-gist-sync.d.ts +93 -0
- package/dist/commands/dashboard-gist-sync.js +237 -0
- package/dist/commands/dashboard-server.d.ts +6 -10
- package/dist/commands/dashboard-server.js +155 -222
- package/dist/commands/features.js +6 -0
- package/dist/commands/guidelines.d.ts +6 -0
- package/dist/commands/guidelines.js +7 -0
- package/dist/commands/index.d.ts +2 -5
- package/dist/commands/index.js +2 -4
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +7 -1
- package/dist/commands/list-mark-done.js +6 -21
- package/dist/commands/list-move-tier.js +3 -5
- package/dist/commands/locate-issue-list.d.ts +25 -0
- package/dist/commands/locate-issue-list.js +67 -0
- package/dist/commands/merge-loop.d.ts +63 -0
- package/dist/commands/merge-loop.js +157 -0
- package/dist/commands/repo-vet.js +40 -1
- package/dist/commands/scout-bridge.d.ts +35 -2
- package/dist/commands/scout-bridge.js +65 -13
- package/dist/commands/search.d.ts +4 -6
- package/dist/commands/search.js +58 -11
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +56 -2
- package/dist/commands/skip-file-parser.d.ts +23 -0
- package/dist/commands/skip-file-parser.js +23 -10
- package/dist/commands/startup.d.ts +1 -6
- package/dist/commands/startup.js +25 -59
- package/dist/commands/track.d.ts +2 -2
- package/dist/commands/track.js +2 -2
- package/dist/commands/vet-list.js +4 -0
- package/dist/core/config-registry.js +36 -0
- package/dist/core/daily-logic.d.ts +25 -2
- package/dist/core/daily-logic.js +58 -3
- package/dist/core/gist-health.d.ts +81 -0
- package/dist/core/gist-health.js +39 -0
- package/dist/core/gist-state-store.d.ts +3 -1
- package/dist/core/gist-state-store.js +7 -2
- package/dist/core/github-stats.d.ts +2 -2
- package/dist/core/github-stats.js +20 -4
- package/dist/core/index.d.ts +4 -3
- package/dist/core/index.js +4 -3
- package/dist/core/issue-conversation.js +8 -2
- package/dist/core/issue-grading.d.ts +9 -0
- package/dist/core/issue-grading.js +9 -0
- package/dist/core/pagination.d.ts +27 -0
- package/dist/core/pagination.js +23 -5
- package/dist/core/pr-comments-fetcher.d.ts +7 -0
- package/dist/core/pr-comments-fetcher.js +19 -8
- package/dist/core/pr-monitor.d.ts +2 -0
- package/dist/core/pr-monitor.js +26 -9
- package/dist/core/repo-score-manager.d.ts +2 -2
- package/dist/core/repo-score-manager.js +3 -3
- package/dist/core/repo-vet.d.ts +2 -2
- package/dist/core/repo-vet.js +1 -1
- package/dist/core/review-analysis.d.ts +19 -0
- package/dist/core/review-analysis.js +28 -0
- package/dist/core/state-schema.d.ts +43 -6
- package/dist/core/state-schema.js +81 -4
- package/dist/core/state.d.ts +36 -5
- package/dist/core/state.js +177 -28
- package/dist/core/strategy.js +6 -5
- package/dist/core/types.d.ts +8 -0
- package/dist/core/untrusted-content.d.ts +45 -0
- package/dist/core/untrusted-content.js +54 -0
- package/dist/formatters/json.d.ts +89 -6
- package/dist/formatters/json.js +65 -1
- package/package.json +2 -2
- package/dist/commands/shelve.d.ts +0 -45
- package/dist/commands/shelve.js +0 -54
|
@@ -4,21 +4,30 @@
|
|
|
4
4
|
* for live data fetching and state mutations (PR state transitions, issue dismiss).
|
|
5
5
|
*
|
|
6
6
|
* Uses Node's built-in http module — no Express/Fastify.
|
|
7
|
+
*
|
|
8
|
+
* Collaborators (#1457): the gist-sync lifecycle lives in
|
|
9
|
+
* GistSyncCoordinator (dashboard-gist-sync.ts) and the cached-response
|
|
10
|
+
* state in DashboardCache (dashboard-cache.ts); this file is wiring,
|
|
11
|
+
* routing and the HTTP handlers.
|
|
7
12
|
*/
|
|
8
13
|
import * as http from 'node:http';
|
|
9
14
|
import * as fs from 'node:fs';
|
|
10
15
|
import * as path from 'node:path';
|
|
11
16
|
import * as crypto from 'node:crypto';
|
|
12
|
-
import {
|
|
17
|
+
import { getGitHubToken, getCLIVersion, maybeCheckpoint } from '../core/index.js';
|
|
13
18
|
import { errorMessage, ValidationError, ConcurrencyError, GistConcurrencyError } from '../core/errors.js';
|
|
14
19
|
import { warn } from '../core/logger.js';
|
|
15
20
|
import { validateUrl, validateGitHubUrl, PR_URL_PATTERN, ISSUE_URL_PATTERN } from './validation.js';
|
|
16
|
-
import { fetchDashboardData
|
|
17
|
-
import { openInBrowser
|
|
18
|
-
import { parseIssueList } from './parse-list.js';
|
|
21
|
+
import { fetchDashboardData } from './dashboard-data.js';
|
|
22
|
+
import { openInBrowser } from './startup.js';
|
|
19
23
|
import { writeDashboardServerInfo, removeDashboardServerInfo } from './dashboard-process.js';
|
|
20
24
|
import { RateLimiter } from './rate-limiter.js';
|
|
21
|
-
import {
|
|
25
|
+
import { GistSyncCoordinator } from './dashboard-gist-sync.js';
|
|
26
|
+
import { DashboardCache, getIssueListMtimeMs } from './dashboard-cache.js';
|
|
27
|
+
// Re-export the payload builder for backward compatibility — it moved to
|
|
28
|
+
// dashboard-cache.ts with the cache extraction (#1457) but is unit-tested
|
|
29
|
+
// (and consumed) under this module's name.
|
|
30
|
+
export { buildDashboardJson } from './dashboard-cache.js';
|
|
22
31
|
// Re-export process management functions for backward compatibility
|
|
23
32
|
export { getDashboardPidPath, writeDashboardServerInfo, readDashboardServerInfo, removeDashboardServerInfo, isDashboardServerRunning, findRunningDashboardServer, } from './dashboard-process.js';
|
|
24
33
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
@@ -36,120 +45,6 @@ const MIME_TYPES = {
|
|
|
36
45
|
'.ico': 'image/x-icon',
|
|
37
46
|
};
|
|
38
47
|
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
39
|
-
/**
|
|
40
|
-
* Read and parse the vetted issue list file (non-fatal on failure).
|
|
41
|
-
*/
|
|
42
|
-
function readVettedIssues() {
|
|
43
|
-
try {
|
|
44
|
-
const info = detectIssueList();
|
|
45
|
-
if (!info)
|
|
46
|
-
return null;
|
|
47
|
-
const content = fs.readFileSync(info.path, 'utf8');
|
|
48
|
-
return parseIssueList(content);
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
warn(MODULE, `Failed to read vetted issue list: ${errorMessage(error)}`);
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Get the mtime of the vetted issue list file in ms, or null if unknown.
|
|
57
|
-
* Used to detect external edits and invalidate the cached dashboard payload.
|
|
58
|
-
*/
|
|
59
|
-
function getIssueListMtimeMs() {
|
|
60
|
-
try {
|
|
61
|
-
const info = detectIssueList();
|
|
62
|
-
if (!info)
|
|
63
|
-
return null;
|
|
64
|
-
return fs.statSync(info.path).mtimeMs;
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Build the JSON payload that the SPA expects from GET /api/data.
|
|
72
|
-
*
|
|
73
|
-
* Exported for unit testing of response-shape concerns that the full
|
|
74
|
-
* handler harness can't reach (it bakes a stale cachedDigest at server
|
|
75
|
-
* start-up, so tests that need a specific digest should call this directly).
|
|
76
|
-
*/
|
|
77
|
-
export function buildDashboardJson(digest, state, commentedIssues, allMergedPRs, allClosedPRs, partialFailures) {
|
|
78
|
-
// Apply status overrides ONCE, before the shelve partition is derived, so
|
|
79
|
-
// the dashboard partitions on the same post-override status the CLI
|
|
80
|
-
// partitions on (#1416). This also covers overrides set AFTER the digest
|
|
81
|
-
// was cached (a dashboard move stores an override; the action path rebuilds
|
|
82
|
-
// from the cached digest). Work on a copy — the caller's cached digest must
|
|
83
|
-
// not accumulate per-request derivations.
|
|
84
|
-
const overriddenDigest = {
|
|
85
|
-
...digest,
|
|
86
|
-
openPRs: applyStatusOverrides(digest.openPRs || [], state),
|
|
87
|
-
summary: { ...digest.summary },
|
|
88
|
-
};
|
|
89
|
-
// Re-derive the shelve partition from the CURRENT state before reading it.
|
|
90
|
-
// The POST /api/action path rebuilds with a cached digest whose shelvedPRs
|
|
91
|
-
// predates the shelve/unshelve, so without this the SPA action appears to do
|
|
92
|
-
// nothing until the next full /api/refresh.
|
|
93
|
-
reconcileShelvePartition(overriddenDigest, state);
|
|
94
|
-
const prsByRepo = computePRsByRepo(overriddenDigest, state);
|
|
95
|
-
const topRepos = computeTopRepos(prsByRepo);
|
|
96
|
-
const { monthlyMerged, monthlyOpened, monthlyClosed } = getMonthlyData(state);
|
|
97
|
-
// Derive from state if not provided (e.g. initial load from cached state)
|
|
98
|
-
const mergedPRs = allMergedPRs ?? storedToMergedPRs(getStateManager().getMergedPRs());
|
|
99
|
-
const closedPRs = allClosedPRs ?? storedToClosedPRs(getStateManager().getClosedPRs());
|
|
100
|
-
// Filter out PRs from repos below the minStars threshold
|
|
101
|
-
const minStars = state.config.minStars ?? 50;
|
|
102
|
-
const repoScores = state.repoScores || {};
|
|
103
|
-
const isAboveMinStars = (pr) => !isBelowMinStars(repoScores[pr.repo]?.stargazersCount, minStars);
|
|
104
|
-
const filteredMergedPRs = mergedPRs.filter(isAboveMinStars);
|
|
105
|
-
const filteredClosedPRs = closedPRs.filter(isAboveMinStars);
|
|
106
|
-
const stats = buildDashboardStats(overriddenDigest, state, filteredMergedPRs.length, filteredClosedPRs.length);
|
|
107
|
-
const dismissedIssues = state.config.dismissedIssues || {};
|
|
108
|
-
const issueResponses = commentedIssues.filter((i) => i.status === 'new_response' && !(i.url in dismissedIssues));
|
|
109
|
-
// Build repo metadata map from repoScores — omit repos without stars or language to avoid empty entries
|
|
110
|
-
const repoMetadata = {};
|
|
111
|
-
for (const [repo, score] of Object.entries(repoScores)) {
|
|
112
|
-
if (score.stargazersCount !== undefined || score.language !== undefined) {
|
|
113
|
-
repoMetadata[repo] = { stars: score.stargazersCount, language: score.language };
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
const vettedIssues = readVettedIssues();
|
|
117
|
-
if (vettedIssues) {
|
|
118
|
-
stats.availableIssues = vettedIssues.availableCount;
|
|
119
|
-
}
|
|
120
|
-
return {
|
|
121
|
-
stats,
|
|
122
|
-
prsByRepo,
|
|
123
|
-
topRepos: topRepos.map(([repo, data]) => ({ repo, ...data })),
|
|
124
|
-
monthlyMerged,
|
|
125
|
-
monthlyOpened,
|
|
126
|
-
monthlyClosed,
|
|
127
|
-
// #1352: stamp the unified attention bucket so the SPA renders the same
|
|
128
|
-
// taxonomy the CLI brief counts (single classifier, no second opinion).
|
|
129
|
-
// Overrides were already applied when overriddenDigest was built — a
|
|
130
|
-
// second application here would be a no-op but obscures the single
|
|
131
|
-
// apply-then-partition ordering (#1416).
|
|
132
|
-
activePRs: (overriddenDigest.openPRs || []).map((pr) => ({
|
|
133
|
-
...pr,
|
|
134
|
-
attentionBucket: classifyAttentionBucket(pr),
|
|
135
|
-
})),
|
|
136
|
-
// Source of truth is digest.shelvedPRs (union of explicitly-shelved URLs
|
|
137
|
-
// and dormant-non-addressing PRs auto-shelved for display). Returning
|
|
138
|
-
// only state.config.shelvedPRUrls would under-count and desync from
|
|
139
|
-
// stats.shelvedPRs, which is already derived from digest.shelvedPRs. (#981)
|
|
140
|
-
shelvedPRUrls: (overriddenDigest.shelvedPRs || []).map((ref) => ref.url),
|
|
141
|
-
recentlyMergedPRs: overriddenDigest.recentlyMergedPRs || [],
|
|
142
|
-
recentlyClosedPRs: overriddenDigest.recentlyClosedPRs || [],
|
|
143
|
-
autoUnshelvedPRs: overriddenDigest.autoUnshelvedPRs || [],
|
|
144
|
-
commentedIssues,
|
|
145
|
-
issueResponses,
|
|
146
|
-
allMergedPRs: filteredMergedPRs,
|
|
147
|
-
allClosedPRs: filteredClosedPRs,
|
|
148
|
-
repoMetadata,
|
|
149
|
-
vettedIssues,
|
|
150
|
-
partialFailures: partialFailures && partialFailures.length > 0 ? partialFailures : undefined,
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
48
|
/**
|
|
154
49
|
* Read the full request body as a UTF-8 string, with a size limit.
|
|
155
50
|
*/
|
|
@@ -264,6 +159,14 @@ function sendJson(res, statusCode, data) {
|
|
|
264
159
|
function sendError(res, statusCode, message) {
|
|
265
160
|
sendJson(res, statusCode, { error: message });
|
|
266
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Collapse CR/LF in a string destined for an HTTP header value — Node's
|
|
164
|
+
* setHeader throws on embedded newlines, and staleness reasons are built
|
|
165
|
+
* from arbitrary error messages (#1446 item 2).
|
|
166
|
+
*/
|
|
167
|
+
function sanitizeHeaderValue(value) {
|
|
168
|
+
return value.replace(/[\r\n]+/g, ' ').trim();
|
|
169
|
+
}
|
|
267
170
|
/**
|
|
268
171
|
* True when an error is an optimistic-concurrency conflict on state.json
|
|
269
172
|
* (local mtime CAS) or the state Gist (ETag CAS). Both carry the
|
|
@@ -288,7 +191,12 @@ function sendConflict(res) {
|
|
|
288
191
|
// ── Server ─────────────────────────────────────────────────────────────────────
|
|
289
192
|
export async function startDashboardServer(options) {
|
|
290
193
|
const { port: requestedPort, assetsDir, token, open } = options;
|
|
291
|
-
|
|
194
|
+
// Gist-sync lifecycle state machine (#1457): pending push warnings (#1417),
|
|
195
|
+
// degraded recovery with throttle/halt (#1433), loss notices (#1443). It
|
|
196
|
+
// also owns the live StateManager reference — a degraded gist recovery
|
|
197
|
+
// replaces the core singleton, and this long-lived server must re-resolve
|
|
198
|
+
// its reference (#1433), so always read it via gistSync.stateManager.
|
|
199
|
+
const gistSync = new GistSyncCoordinator(token, MODULE);
|
|
292
200
|
const resolvedAssetsDir = path.resolve(assetsDir);
|
|
293
201
|
// ── CSRF token ──────────────────────────────────────────────────────────
|
|
294
202
|
// Fresh per server-start. Exposed to the SPA via X-CSRF-Token on every
|
|
@@ -302,62 +210,14 @@ export async function startDashboardServer(options) {
|
|
|
302
210
|
// Start immediately with state.json data (written by the daily check that
|
|
303
211
|
// precedes this server launch). A background GitHub fetch refreshes the
|
|
304
212
|
// cache after the port is bound, so the startup poller sees us in time.
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
// Persist the last-known partialFailures across rebuild requests (#1035).
|
|
308
|
-
// Cleared only when a fresh fetchDashboardData returns zero failures;
|
|
309
|
-
// re-threaded into every buildDashboardJson call so the SPA banner does
|
|
310
|
-
// not disappear when /api/data rebuilds after a state change or after a
|
|
311
|
-
// POST /api/action completes.
|
|
312
|
-
let cachedPartialFailures = undefined;
|
|
313
|
-
// Gist checkpoint warnings from dashboard mutations (#1417). DELIBERATELY
|
|
314
|
-
// separate from cachedPartialFailures: fetch failures clear on a successful
|
|
315
|
-
// PULL, but a gist-sync warning means an un-pushed mutation, and a pull is
|
|
316
|
-
// exactly the event that can destroy it (refreshFromGist wholesale-replaces
|
|
317
|
-
// state). These clear only when a checkpoint PUSH succeeds.
|
|
318
|
-
let pendingGistSyncWarnings = [];
|
|
319
|
-
// Tracks the last background-refresh failure so /api/data can surface
|
|
320
|
-
// staleness to the SPA via the X-Dashboard-Stale header (#1205). Cleared
|
|
321
|
-
// when a refresh succeeds. Without this, token expiry / GitHub outage
|
|
322
|
-
// produces silent stale data hours old with no client-visible signal.
|
|
323
|
-
let lastBackgroundRefreshError = null;
|
|
324
|
-
/** Record a mutation's checkpoint outcome. `null` means the push succeeded
|
|
325
|
-
* (or local mode) — and a successful push carries the FULL current state,
|
|
326
|
-
* so any previously pending warning is resolved with it. */
|
|
327
|
-
function recordGistSyncOutcome(warning) {
|
|
328
|
-
if (warning === null) {
|
|
329
|
-
pendingGistSyncWarnings = [];
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
if (!pendingGistSyncWarnings.includes(warning)) {
|
|
333
|
-
pendingGistSyncWarnings.push(warning);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
/** Merge pending gist-sync warnings into a partialFailures payload for the
|
|
337
|
-
* SPA banner without coupling their lifecycles. */
|
|
338
|
-
function withPendingGistWarnings(failures) {
|
|
339
|
-
if (pendingGistSyncWarnings.length === 0)
|
|
340
|
-
return failures;
|
|
341
|
-
const base = failures ?? [];
|
|
342
|
-
return [...base, ...pendingGistSyncWarnings.filter((w) => !base.includes(w))];
|
|
343
|
-
}
|
|
344
|
-
/** Push-before-pull (#1417): an un-pushed mutation would be silently
|
|
345
|
-
* reverted by the next Gist pull. Retry the checkpoint first so a recovered
|
|
346
|
-
* network turns the pending warning into a real push before any pull runs.
|
|
347
|
-
* No-op when nothing is pending. */
|
|
348
|
-
async function flushPendingGistSync() {
|
|
349
|
-
if (pendingGistSyncWarnings.length === 0)
|
|
350
|
-
return;
|
|
351
|
-
recordGistSyncOutcome(await maybeCheckpoint(stateManager, MODULE));
|
|
352
|
-
}
|
|
353
|
-
if (!cachedDigest) {
|
|
213
|
+
const initialDigest = gistSync.stateManager.getState().lastDigest;
|
|
214
|
+
if (!initialDigest) {
|
|
354
215
|
throw new Error('No dashboard data available. Run the daily check first: GITHUB_TOKEN=$(gh auth token) npm start -- daily');
|
|
355
216
|
}
|
|
217
|
+
const cache = new DashboardCache(initialDigest, (failures) => gistSync.withPendingGistWarnings(failures));
|
|
356
218
|
// ── Build cached JSON response ───────────────────────────────────────────
|
|
357
|
-
let cachedJsonData;
|
|
358
|
-
let cachedIssueListMtimeMs = getIssueListMtimeMs();
|
|
359
219
|
try {
|
|
360
|
-
|
|
220
|
+
cache.rebuild(gistSync.stateManager.getState());
|
|
361
221
|
}
|
|
362
222
|
catch (error) {
|
|
363
223
|
throw new Error(`Failed to build dashboard data: ${errorMessage(error)}. State data may be corrupted — try running: daily --json`, { cause: error });
|
|
@@ -389,26 +249,36 @@ export async function startDashboardServer(options) {
|
|
|
389
249
|
// Expose the CSRF token to the SPA on every data fetch so the client
|
|
390
250
|
// can attach it on subsequent POSTs. Fresh fetch → fresh token view.
|
|
391
251
|
res.setHeader('X-CSRF-Token', csrfToken);
|
|
392
|
-
// Re-read state if modified externally (file mtime for local, Gist
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
252
|
+
// Re-read state if modified externally (file mtime for local, Gist
|
|
253
|
+
// API for Gist mode). Shared with the POST paths via reloadState()
|
|
254
|
+
// so the recoveryHaltedReason un-halt lives in exactly one place —
|
|
255
|
+
// this GET path used to hand-duplicate the sequence minus the
|
|
256
|
+
// un-halt and had already drifted once (#1446 item 5). The #1443
|
|
257
|
+
// degraded-bootstrap recovery (and its banner-clearing stateChanged
|
|
258
|
+
// signal) lives inside reloadState too — do NOT add a second
|
|
259
|
+
// maybeRecoverGist call here or recovery would double-run per poll.
|
|
260
|
+
const stateChanged = await reloadState();
|
|
261
|
+
// Gist-mode staleness (#1446 item 2): refreshFromGist() returns
|
|
262
|
+
// false on BOTH "no change" and "fetch failed", so the boolean can't
|
|
263
|
+
// signal failure. getStateStaleness() is the API built for exactly
|
|
264
|
+
// this — StateManager sets the marker when in-memory state diverged
|
|
265
|
+
// from the canonical Gist (refresh failure, invalid payload,
|
|
266
|
+
// degraded bootstrap) and clears it on a successful pull.
|
|
267
|
+
const staleness = gistSync.stateManager.getStateStaleness();
|
|
268
|
+
if (staleness !== null) {
|
|
269
|
+
res.setHeader('X-Dashboard-Stale', '1');
|
|
270
|
+
res.setHeader('X-Dashboard-Stale-Reason', sanitizeHeaderValue(`state-stale: ${staleness.reason}`));
|
|
400
271
|
}
|
|
401
272
|
// Also rebuild when the vetted issue list file was edited outside this server (#924)
|
|
402
273
|
const currentIssueListMtimeMs = getIssueListMtimeMs();
|
|
403
|
-
const issueListChanged = currentIssueListMtimeMs !==
|
|
274
|
+
const issueListChanged = currentIssueListMtimeMs !== cache.issueListMtimeMs;
|
|
404
275
|
if (stateChanged || issueListChanged) {
|
|
405
276
|
try {
|
|
406
|
-
|
|
407
|
-
cachedIssueListMtimeMs = currentIssueListMtimeMs;
|
|
277
|
+
cache.rebuild(gistSync.stateManager.getState(), undefined, undefined, currentIssueListMtimeMs);
|
|
408
278
|
}
|
|
409
279
|
catch (error) {
|
|
410
280
|
warn(MODULE, `Failed to rebuild dashboard data after state reload: ${errorMessage(error)}`);
|
|
411
|
-
// Serve previous
|
|
281
|
+
// Serve previous cached payload rather than returning 500.
|
|
412
282
|
// Signal staleness via response header so clients can detect the degraded mode (#994).
|
|
413
283
|
res.setHeader('X-Dashboard-Stale', '1');
|
|
414
284
|
}
|
|
@@ -417,11 +287,11 @@ export async function startDashboardServer(options) {
|
|
|
417
287
|
// token expiry / GitHub outage produces a client-visible signal
|
|
418
288
|
// rather than silent stale data. Only set the header when a failure
|
|
419
289
|
// is recorded — successful refreshes clear it.
|
|
420
|
-
if (lastBackgroundRefreshError !== null) {
|
|
290
|
+
if (cache.lastBackgroundRefreshError !== null) {
|
|
421
291
|
res.setHeader('X-Dashboard-Stale', '1');
|
|
422
|
-
res.setHeader('X-Dashboard-Stale-Reason', `background-refresh-failed: ${lastBackgroundRefreshError}`);
|
|
292
|
+
res.setHeader('X-Dashboard-Stale-Reason', sanitizeHeaderValue(`background-refresh-failed: ${cache.lastBackgroundRefreshError}`));
|
|
423
293
|
}
|
|
424
|
-
sendJson(res, 200,
|
|
294
|
+
sendJson(res, 200, cache.jsonData);
|
|
425
295
|
return;
|
|
426
296
|
}
|
|
427
297
|
if (url === '/api/action' && method === 'POST') {
|
|
@@ -479,15 +349,54 @@ export async function startDashboardServer(options) {
|
|
|
479
349
|
});
|
|
480
350
|
server.requestTimeout = REQUEST_TIMEOUT_MS;
|
|
481
351
|
// ── POST /api/action handler ─────────────────────────────────────────────
|
|
482
|
-
/** Re-read state written by external processes (CLI) before mutating.
|
|
352
|
+
/** Re-read state written by external processes (CLI) before mutating.
|
|
353
|
+
* Returns true when the state source changed (gist pull refreshed, local
|
|
354
|
+
* file reloaded, or a gist recovery completed) — GET /api/data uses this
|
|
355
|
+
* to decide whether to rebuild the cached payload (#1446 item 5). */
|
|
483
356
|
async function reloadState() {
|
|
484
|
-
if (stateManager.isGistMode()) {
|
|
485
|
-
await flushPendingGistSync();
|
|
486
|
-
|
|
357
|
+
if (gistSync.stateManager.isGistMode()) {
|
|
358
|
+
await gistSync.flushPendingGistSync();
|
|
359
|
+
// Both post-pull steps consume the same refreshFromGist outcome, in
|
|
360
|
+
// this order: pullFromGist (#1443) settles the LOSS side first —
|
|
361
|
+
// still-pending push warnings become a loss notice at the instant the
|
|
362
|
+
// pull replaces state — then adoptNewerPulledDigest harvests the GAIN
|
|
363
|
+
// side (#1446 item 6) by reading the freshly replaced state, so it
|
|
364
|
+
// cannot run before the pull. Adoption stays out of pullFromGist
|
|
365
|
+
// because the background-refresh path also pulls but replaces
|
|
366
|
+
// cachedDigest unconditionally right after — adoption is a
|
|
367
|
+
// reloadState-path concern only.
|
|
368
|
+
const refreshed = await gistSync.pullFromGist();
|
|
369
|
+
if (refreshed)
|
|
370
|
+
cache.adoptNewerPulledDigest(gistSync.stateManager.getState().lastDigest);
|
|
371
|
+
if (gistSync.gistBootstrapDegraded()) {
|
|
372
|
+
// #1443: a degraded bootstrap keeps isGistMode() true while the
|
|
373
|
+
// store is disarmed, so refreshFromGist() short-circuits forever
|
|
374
|
+
// and the local-branch recovery below never sees it. Same
|
|
375
|
+
// throttle/halt machinery applies inside maybeRecoverGist.
|
|
376
|
+
await gistSync.maybeRecoverGist();
|
|
377
|
+
// A successful recovery replaced the singleton with state pulled
|
|
378
|
+
// from the Gist — report a change so callers rebuild and the
|
|
379
|
+
// stale-bootstrap banner clears now.
|
|
380
|
+
if (!gistSync.gistBootstrapDegraded())
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
return refreshed;
|
|
487
384
|
}
|
|
488
|
-
|
|
489
|
-
|
|
385
|
+
let changed = gistSync.stateManager.reloadIfChanged();
|
|
386
|
+
if (changed) {
|
|
387
|
+
// An external config edit may BE the fix for a permanently-halted
|
|
388
|
+
// recovery (new token scope, repaired gist id) — give it one fresh
|
|
389
|
+
// attempt cycle (#1433 pass-2).
|
|
390
|
+
gistSync.unhaltRecovery();
|
|
490
391
|
}
|
|
392
|
+
// reloadIfChanged may have just pulled a persistence=gist flip made
|
|
393
|
+
// from a terminal, and a degraded server heals here too (#1433).
|
|
394
|
+
await gistSync.maybeRecoverGist();
|
|
395
|
+
// A successful recovery is a state-source change: rebuild so the
|
|
396
|
+
// degraded banner clears without waiting for another edit.
|
|
397
|
+
if (gistSync.stateManager.isGistMode())
|
|
398
|
+
changed = true;
|
|
399
|
+
return changed;
|
|
491
400
|
}
|
|
492
401
|
async function handleAction(req, res) {
|
|
493
402
|
let body;
|
|
@@ -547,11 +456,11 @@ export async function startDashboardServer(options) {
|
|
|
547
456
|
else {
|
|
548
457
|
// dismiss_issue_response
|
|
549
458
|
applyMutation = async () => {
|
|
550
|
-
stateManager.dismissIssue(body.url, new Date().toISOString());
|
|
459
|
+
gistSync.stateManager.dismissIssue(body.url, new Date().toISOString());
|
|
551
460
|
// Mirror runMove's contract: every mutating surface checkpoints to
|
|
552
461
|
// Gist and surfaces the warning. Never throws — failures come back
|
|
553
462
|
// as the warning string.
|
|
554
|
-
gistSyncWarning = await maybeCheckpoint(stateManager, MODULE);
|
|
463
|
+
gistSyncWarning = await maybeCheckpoint(gistSync.stateManager, MODULE);
|
|
555
464
|
};
|
|
556
465
|
}
|
|
557
466
|
// Reload state before mutating to avoid overwriting external CLI changes.
|
|
@@ -595,14 +504,17 @@ export async function startDashboardServer(options) {
|
|
|
595
504
|
// SPA already renders (#1417). Tracked in pendingGistSyncWarnings — NOT
|
|
596
505
|
// cachedPartialFailures — because gist warnings clear on a successful
|
|
597
506
|
// PUSH, while fetch failures clear on a successful pull/refresh.
|
|
598
|
-
recordGistSyncOutcome(gistSyncWarning);
|
|
507
|
+
gistSync.recordGistSyncOutcome(gistSyncWarning);
|
|
508
|
+
// Count mutations acknowledged while degraded (#1433): a later recovery
|
|
509
|
+
// bootstraps from the existing Gist and reverts them, and the loss
|
|
510
|
+
// notice needs to know whether there is anything to lose.
|
|
511
|
+
gistSync.recordMutationWhileDegraded();
|
|
599
512
|
// Rebuild dashboard data from cached digest + updated state. Persist
|
|
600
513
|
// the last-known partialFailures across action rebuilds (#1035) so the
|
|
601
514
|
// SPA banner survives user interactions until the next successful
|
|
602
515
|
// refresh clears it.
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
sendJson(res, 200, cachedJsonData);
|
|
516
|
+
cache.rebuild(gistSync.stateManager.getState());
|
|
517
|
+
sendJson(res, 200, cache.jsonData);
|
|
606
518
|
}
|
|
607
519
|
// ── POST /api/refresh handler ────────────────────────────────────────────
|
|
608
520
|
async function handleRefresh(_req, res) {
|
|
@@ -612,17 +524,18 @@ export async function startDashboardServer(options) {
|
|
|
612
524
|
return;
|
|
613
525
|
}
|
|
614
526
|
try {
|
|
527
|
+
// Clear PRE-EXISTING loss notices before the reload: by now they have
|
|
528
|
+
// been visible across the degraded window. Order matters — this
|
|
529
|
+
// refresh's own reloadState may RECOVER and produce a fresh notice,
|
|
530
|
+
// which must survive into the rebuild below, not be wiped 10 lines
|
|
531
|
+
// after its creation (#1433 pass-2).
|
|
532
|
+
gistSync.clearRecoveryLossNotices();
|
|
615
533
|
await reloadState();
|
|
616
534
|
warn(MODULE, 'Refreshing dashboard data from GitHub...');
|
|
617
535
|
const result = await fetchDashboardData(currentToken);
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
// set when one or more sub-fetches degraded. See #1035.
|
|
622
|
-
cachedPartialFailures = result.partialFailures.length > 0 ? result.partialFailures : undefined;
|
|
623
|
-
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues, result.allMergedPRs, result.allClosedPRs, withPendingGistWarnings(cachedPartialFailures));
|
|
624
|
-
cachedIssueListMtimeMs = getIssueListMtimeMs();
|
|
625
|
-
sendJson(res, 200, cachedJsonData);
|
|
536
|
+
cache.adoptFetchResult(result);
|
|
537
|
+
cache.rebuild(gistSync.stateManager.getState(), result.allMergedPRs, result.allClosedPRs);
|
|
538
|
+
sendJson(res, 200, cache.jsonData);
|
|
626
539
|
}
|
|
627
540
|
catch (error) {
|
|
628
541
|
// No server-side retry here (unlike handleAction): a refresh re-run is a
|
|
@@ -662,17 +575,28 @@ export async function startDashboardServer(options) {
|
|
|
662
575
|
sendError(res, 403, 'Forbidden');
|
|
663
576
|
return;
|
|
664
577
|
}
|
|
578
|
+
// Hashed build outputs live under /assets/ (Vite's content-addressed
|
|
579
|
+
// bundles). A missing file there must be a real 404, not the SPA
|
|
580
|
+
// fallback: serving index.html as 200 text/html makes the module loader
|
|
581
|
+
// reject the response, and after a plugin upgrade a cached index.html
|
|
582
|
+
// referencing deleted bundles would blank the dashboard (#1459).
|
|
583
|
+
const isAssetPath = urlPath.startsWith('/assets/');
|
|
584
|
+
const indexPath = path.join(resolvedAssetsDir, 'index.html');
|
|
665
585
|
// If file doesn't exist or is a directory, serve index.html for SPA routing
|
|
666
586
|
try {
|
|
667
587
|
const stat = fs.statSync(filePath);
|
|
668
588
|
if (stat.isDirectory()) {
|
|
669
|
-
filePath =
|
|
589
|
+
filePath = indexPath;
|
|
670
590
|
}
|
|
671
591
|
}
|
|
672
592
|
catch (err) {
|
|
673
593
|
const nodeErr = err;
|
|
674
594
|
if (nodeErr.code === 'ENOENT') {
|
|
675
|
-
|
|
595
|
+
if (isAssetPath) {
|
|
596
|
+
sendError(res, 404, 'Not found');
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
filePath = indexPath;
|
|
676
600
|
}
|
|
677
601
|
else {
|
|
678
602
|
warn(MODULE, `Failed to stat file: ${filePath}`);
|
|
@@ -682,13 +606,19 @@ export async function startDashboardServer(options) {
|
|
|
682
606
|
}
|
|
683
607
|
const ext = path.extname(filePath).toLowerCase();
|
|
684
608
|
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
|
609
|
+
// Honest caching (#1459): only hashed /assets/* files are safe to cache —
|
|
610
|
+
// their names change with their content. index.html (direct or SPA
|
|
611
|
+
// fallback) and other unhashed files (favicon, etc.) must revalidate so a
|
|
612
|
+
// plugin upgrade is picked up on the next navigation instead of serving a
|
|
613
|
+
// stale page that references deleted bundles for up to an hour.
|
|
614
|
+
const cacheControl = isAssetPath && filePath !== indexPath ? 'public, max-age=3600' : 'no-cache';
|
|
685
615
|
try {
|
|
686
616
|
const content = fs.readFileSync(filePath);
|
|
687
617
|
setSecurityHeaders(res);
|
|
688
618
|
res.writeHead(200, {
|
|
689
619
|
'Content-Type': contentType,
|
|
690
620
|
'Content-Length': content.length,
|
|
691
|
-
'Cache-Control':
|
|
621
|
+
'Cache-Control': cacheControl,
|
|
692
622
|
});
|
|
693
623
|
res.end(content);
|
|
694
624
|
}
|
|
@@ -740,28 +670,31 @@ export async function startDashboardServer(options) {
|
|
|
740
670
|
if (token) {
|
|
741
671
|
fetchDashboardData(token)
|
|
742
672
|
.then(async (result) => {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
673
|
+
// Same clear-before-recover ordering as handleRefresh (#1433 pass-2).
|
|
674
|
+
gistSync.clearRecoveryLossNotices();
|
|
675
|
+
if (gistSync.stateManager.isGistMode()) {
|
|
676
|
+
await gistSync.flushPendingGistSync();
|
|
677
|
+
await gistSync.pullFromGist();
|
|
678
|
+
// Heal a degraded bootstrap from the background refresh too (#1443).
|
|
679
|
+
if (gistSync.gistBootstrapDegraded())
|
|
680
|
+
await gistSync.maybeRecoverGist();
|
|
746
681
|
}
|
|
747
682
|
else {
|
|
748
|
-
stateManager.reloadIfChanged();
|
|
683
|
+
gistSync.stateManager.reloadIfChanged();
|
|
684
|
+
await gistSync.maybeRecoverGist();
|
|
749
685
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
cachedPartialFailures = result.partialFailures.length > 0 ? result.partialFailures : undefined;
|
|
753
|
-
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues, result.allMergedPRs, result.allClosedPRs, withPendingGistWarnings(cachedPartialFailures));
|
|
754
|
-
cachedIssueListMtimeMs = getIssueListMtimeMs();
|
|
686
|
+
cache.adoptFetchResult(result);
|
|
687
|
+
cache.rebuild(gistSync.stateManager.getState(), result.allMergedPRs, result.allClosedPRs);
|
|
755
688
|
// Successful refresh clears any prior failure signal (#1205).
|
|
756
|
-
lastBackgroundRefreshError = null;
|
|
689
|
+
cache.lastBackgroundRefreshError = null;
|
|
757
690
|
warn(MODULE, 'Background data refresh complete');
|
|
758
691
|
return;
|
|
759
692
|
})
|
|
760
693
|
.catch((error) => {
|
|
761
694
|
// Capture so /api/data can surface staleness via X-Dashboard-Stale
|
|
762
695
|
// header — previously the catch only logged to stderr (#1205).
|
|
763
|
-
lastBackgroundRefreshError = errorMessage(error);
|
|
764
|
-
warn(MODULE, `Background data refresh failed (serving cached data): ${lastBackgroundRefreshError}`);
|
|
696
|
+
cache.lastBackgroundRefreshError = errorMessage(error);
|
|
697
|
+
warn(MODULE, `Background data refresh failed (serving cached data): ${cache.lastBackgroundRefreshError}`);
|
|
765
698
|
});
|
|
766
699
|
}
|
|
767
700
|
// ── Open browser ─────────────────────────────────────────────────────────
|
|
@@ -119,6 +119,12 @@ function toFeaturesCandidate(scoutCandidate, getState) {
|
|
|
119
119
|
*/
|
|
120
120
|
export async function runFeatures(options) {
|
|
121
121
|
const scout = await createAutopilotScout();
|
|
122
|
+
// Strategy bias (preferLanguages/preferRepos/avoidRepos/boostIssueTypes/
|
|
123
|
+
// diversityRatio) reaches this path through the bridge-built
|
|
124
|
+
// ScoutPreferences (#1464) — the same derivation `runSearch` uses per-call.
|
|
125
|
+
// scout.features() in scout 1.1.0 accepts only count/anchorThreshold/
|
|
126
|
+
// splitRatio/broad — do NOT pass bias knobs here; unsupported options
|
|
127
|
+
// would be silently ignored. Preferences are the supported channel.
|
|
122
128
|
const result = await scout.features({
|
|
123
129
|
count: options.maxResults,
|
|
124
130
|
anchorThreshold: options.anchorThreshold,
|
|
@@ -50,6 +50,12 @@ export interface FetchCorpusOutput {
|
|
|
50
50
|
}>;
|
|
51
51
|
/** Set when the post-mutation Gist checkpoint failed; the local mutation succeeded (#1370). */
|
|
52
52
|
gistSyncWarning?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Non-fatal data-quality warnings (#1456). Currently produced when a
|
|
55
|
+
* bundle's comment streams hit the pagination cap, meaning the newest
|
|
56
|
+
* comments on that PR are missing from the corpus. Omitted on clean runs.
|
|
57
|
+
*/
|
|
58
|
+
warnings?: string[];
|
|
53
59
|
}
|
|
54
60
|
interface RepoOption {
|
|
55
61
|
repo: string;
|
|
@@ -167,6 +167,12 @@ export async function runFetchCorpus(options) {
|
|
|
167
167
|
if (bundles.length > 0) {
|
|
168
168
|
gistSyncWarning = await maybeCheckpoint(sm, MODULE);
|
|
169
169
|
}
|
|
170
|
+
// Surface pagination truncation per bundle (#1456): a truncated bundle is
|
|
171
|
+
// missing its newest comments, so the host should know the corpus for that
|
|
172
|
+
// PR is partial rather than assuming every reviewer voice is present.
|
|
173
|
+
const warnings = bundles
|
|
174
|
+
.filter((b) => b.truncated)
|
|
175
|
+
.map((b) => `${b.prUrl}: comment streams hit the pagination cap; newest comments are missing from the corpus`);
|
|
170
176
|
return {
|
|
171
177
|
repo: options.repo,
|
|
172
178
|
bundles,
|
|
@@ -174,6 +180,7 @@ export async function runFetchCorpus(options) {
|
|
|
174
180
|
skipped,
|
|
175
181
|
failures,
|
|
176
182
|
...(gistSyncWarning ? { gistSyncWarning } : {}),
|
|
183
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
177
184
|
};
|
|
178
185
|
}
|
|
179
186
|
function clampLimit(limit) {
|
package/dist/commands/index.d.ts
CHANGED
|
@@ -39,10 +39,6 @@ export { runTrack } from './track.js';
|
|
|
39
39
|
export { runComplianceScore } from './compliance-score.js';
|
|
40
40
|
/** Compute repo health rubric (1–10 score + verdict) via the typed core function (#1271, follow-up to #1242). */
|
|
41
41
|
export { runRepoVet } from './repo-vet.js';
|
|
42
|
-
/** Temporarily hide a PR from the daily digest. */
|
|
43
|
-
export { runShelve } from './shelve.js';
|
|
44
|
-
/** Restore a shelved PR to the daily digest. */
|
|
45
|
-
export { runUnshelve } from './shelve.js';
|
|
46
42
|
/** Move a PR between states: attention, waiting, shelved, auto. */
|
|
47
43
|
export { runMove } from './move.js';
|
|
48
44
|
/** Dismiss issue reply notifications (auto-resurfaces on new activity). */
|
|
@@ -85,6 +81,8 @@ export { runParseList, pruneIssueList } from './parse-list.js';
|
|
|
85
81
|
export { runListMoveTier, moveIssueToTier, type Tier, type ListMoveTierOptions, type ListMoveTierOutput, } from './list-move-tier.js';
|
|
86
82
|
/** Mark an issue line in a curated list as done with strikethrough + Done sub-bullet (#1299). */
|
|
87
83
|
export { runMarkIssueListItemDone, markIssueAsDone, type MarkDoneOptions, type MarkDoneOutput, } from './list-mark-done.js';
|
|
84
|
+
/** Daily merge-loop reconciliation — auto-mark curated-list entries whose PR merged (#1463). */
|
|
85
|
+
export { reconcileMergedPRsWithList, findListEntryUrlByPrUrl } from './merge-loop.js';
|
|
88
86
|
/** Check if new files are properly referenced/integrated. */
|
|
89
87
|
export { runCheckIntegration } from './check-integration.js';
|
|
90
88
|
/** System-health diagnostic — verifies tokens, bundle, state, scout, rate limit. */
|
|
@@ -98,7 +96,6 @@ export type { ErrorCode } from '../formatters/json.js';
|
|
|
98
96
|
export type { DailyOutput, SearchOutput, SearchCandidate, CandidateLinkedPR, FeaturesOutput, FeaturesCandidate, FeaturesHorizon, StartupOutput, StatusOutput, TrackOutput, } from '../formatters/json.js';
|
|
99
97
|
export type { VetOutput, CommentsOutput, PostOutput, ClaimOutput, VetListOutput, VetListItemStatus, } from '../formatters/json.js';
|
|
100
98
|
export type { ConfigOutput, DetectFormattersOutput, ParseIssueListOutput, ParsedIssueItem, CheckIntegrationOutput, LocalReposOutput, } from '../formatters/json.js';
|
|
101
|
-
export type { ShelveOutput, UnshelveOutput } from './shelve.js';
|
|
102
99
|
export type { MoveOutput, MoveTarget } from './move.js';
|
|
103
100
|
export type { GuidelinesListOutput, GuidelinesViewOutput, GuidelinesStoreOutput, GuidelinesResetOutput, FetchCorpusOutput, } from './guidelines.js';
|
|
104
101
|
export type { DismissOutput, UndismissOutput } from './dismiss.js';
|
package/dist/commands/index.js
CHANGED
|
@@ -41,10 +41,6 @@ export { runTrack } from './track.js';
|
|
|
41
41
|
export { runComplianceScore } from './compliance-score.js';
|
|
42
42
|
/** Compute repo health rubric (1–10 score + verdict) via the typed core function (#1271, follow-up to #1242). */
|
|
43
43
|
export { runRepoVet } from './repo-vet.js';
|
|
44
|
-
/** Temporarily hide a PR from the daily digest. */
|
|
45
|
-
export { runShelve } from './shelve.js';
|
|
46
|
-
/** Restore a shelved PR to the daily digest. */
|
|
47
|
-
export { runUnshelve } from './shelve.js';
|
|
48
44
|
/** Move a PR between states: attention, waiting, shelved, auto. */
|
|
49
45
|
export { runMove } from './move.js';
|
|
50
46
|
/** Dismiss issue reply notifications (auto-resurfaces on new activity). */
|
|
@@ -92,6 +88,8 @@ export { runParseList, pruneIssueList } from './parse-list.js';
|
|
|
92
88
|
export { runListMoveTier, moveIssueToTier, } from './list-move-tier.js';
|
|
93
89
|
/** Mark an issue line in a curated list as done with strikethrough + Done sub-bullet (#1299). */
|
|
94
90
|
export { runMarkIssueListItemDone, markIssueAsDone, } from './list-mark-done.js';
|
|
91
|
+
/** Daily merge-loop reconciliation — auto-mark curated-list entries whose PR merged (#1463). */
|
|
92
|
+
export { reconcileMergedPRsWithList, findListEntryUrlByPrUrl } from './merge-loop.js';
|
|
95
93
|
/** Check if new files are properly referenced/integrated. */
|
|
96
94
|
export { runCheckIntegration } from './check-integration.js';
|
|
97
95
|
/** System-health diagnostic — verifies tokens, bundle, state, scout, rate limit. */
|
package/dist/commands/init.d.ts
CHANGED