@oss-autopilot/core 3.13.2 → 3.13.4
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/dist/cli.bundle.cjs +87 -87
- package/dist/cli.js +29 -2
- package/dist/commands/daily.js +9 -0
- package/dist/commands/dashboard-server.js +132 -6
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/state.d.ts +19 -0
- package/dist/core/state.js +43 -0
- package/dist/formatters/json.d.ts +13 -1
- package/dist/formatters/json.js +12 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -77,9 +77,36 @@ program.hook('preAction', async (thisCommand, actionCommand) => {
|
|
|
77
77
|
}
|
|
78
78
|
// Activate Gist persistence if configured, before any command runs.
|
|
79
79
|
// Shared helper peeks at the state file and only pre-sets the singleton
|
|
80
|
-
// when Gist mode is the configured persistence (#1000).
|
|
80
|
+
// when Gist mode is the configured persistence (#1000). Hard errors
|
|
81
|
+
// still throw (#1202); the resolving degraded modes are surfaced in the
|
|
82
|
+
// JSON envelope so --json consumers see them too (#1433).
|
|
81
83
|
const { ensureGistPersistence } = await import('./core/index.js');
|
|
82
|
-
await ensureGistPersistence(token);
|
|
84
|
+
const status = await ensureGistPersistence(token);
|
|
85
|
+
if (status === 'degraded' || status === 'state-unreadable') {
|
|
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.');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// #1431: localOnly skips the AUTH GATE, not gist persistence. Mutating
|
|
94
|
+
// localOnly commands (shelve/move/dismiss/override/...) already call
|
|
95
|
+
// maybeCheckpoint, which silently no-ops when the singleton never
|
|
96
|
+
// bootstrapped — so a gist-configured user's CLI mutations were written
|
|
97
|
+
// local-only with no warning and never reached the Gist. Best-effort
|
|
98
|
+
// bootstrap; the warning semantics live (and are unit-tested) in
|
|
99
|
+
// bootstrapGistBestEffort.
|
|
100
|
+
const { bootstrapGistBestEffort } = await import('./core/index.js');
|
|
101
|
+
const localOnlyWarning = await bootstrapGistBestEffort(getGitHubTokenAsync);
|
|
102
|
+
if (localOnlyWarning) {
|
|
103
|
+
console.error(`Warning: ${localOnlyWarning}`);
|
|
104
|
+
// Also thread it into the JSON envelope: shelve/move/dismiss --json
|
|
105
|
+
// consumers (the agent harness) must see the mutation will not sync —
|
|
106
|
+
// stderr alone is invisible to them (#1433).
|
|
107
|
+
const { setEnvelopeGistWarning } = await import('./formatters/json.js');
|
|
108
|
+
setEnvelopeGistWarning(localOnlyWarning);
|
|
109
|
+
}
|
|
83
110
|
}
|
|
84
111
|
});
|
|
85
112
|
// First-run detection: if no subcommand was provided and no state file exists,
|
package/dist/commands/daily.js
CHANGED
|
@@ -546,6 +546,15 @@ async function executeDailyCheckInternal(token) {
|
|
|
546
546
|
// One collector shared by every phase — threaded through explicitly so the
|
|
547
547
|
// callgraph documents which phases can produce non-fatal warnings. See #1042.
|
|
548
548
|
const warnings = [];
|
|
549
|
+
// Surface gist-mode degradation in the machine-readable envelope (#1431):
|
|
550
|
+
// a process whose config says `persistence: gist` but whose manager is
|
|
551
|
+
// local-only (transient init fallback, or a localOnly entry point that
|
|
552
|
+
// never bootstrapped) writes mutations that will not sync. Previously the
|
|
553
|
+
// only signal was a stderr warn, invisible to --json consumers.
|
|
554
|
+
const smForGistCheck = getStateManager();
|
|
555
|
+
if (smForGistCheck.getState().config.persistence === 'gist' && !smForGistCheck.isGistMode()) {
|
|
556
|
+
recordWarning(warnings, 'gist-init', 'Gist persistence degraded', new Error('configured for Gist but running local-only in this process; mutations will not sync until Gist init succeeds'));
|
|
557
|
+
}
|
|
549
558
|
// Surface Gist staleness up-front so consumers see it even if Phase 1 fails (#1193).
|
|
550
559
|
const staleness = getStateManager().getStateStaleness();
|
|
551
560
|
if (staleness) {
|
|
@@ -9,8 +9,8 @@ import * as http from 'node:http';
|
|
|
9
9
|
import * as fs from 'node:fs';
|
|
10
10
|
import * as path from 'node:path';
|
|
11
11
|
import * as crypto from 'node:crypto';
|
|
12
|
-
import { getStateManager, getGitHubToken, getCLIVersion, applyStatusOverrides, classifyAttentionBucket, maybeCheckpoint, } from '../core/index.js';
|
|
13
|
-
import { errorMessage, ValidationError, ConcurrencyError, GistConcurrencyError } from '../core/errors.js';
|
|
12
|
+
import { getStateManager, getGitHubToken, getCLIVersion, applyStatusOverrides, classifyAttentionBucket, maybeCheckpoint, ensureGistPersistence, } from '../core/index.js';
|
|
13
|
+
import { errorMessage, ValidationError, ConcurrencyError, ConfigurationError, GistConcurrencyError, } from '../core/errors.js';
|
|
14
14
|
import { warn } from '../core/logger.js';
|
|
15
15
|
import { validateUrl, validateGitHubUrl, PR_URL_PATTERN, ISSUE_URL_PATTERN } from './validation.js';
|
|
16
16
|
import { fetchDashboardData, computePRsByRepo, computeTopRepos, getMonthlyData, buildDashboardStats, reconcileShelvePartition, storedToMergedPRs, storedToClosedPRs, } from './dashboard-data.js';
|
|
@@ -288,7 +288,10 @@ function sendConflict(res) {
|
|
|
288
288
|
// ── Server ─────────────────────────────────────────────────────────────────────
|
|
289
289
|
export async function startDashboardServer(options) {
|
|
290
290
|
const { port: requestedPort, assetsDir, token, open } = options;
|
|
291
|
-
|
|
291
|
+
// `let` (#1433): a degraded gist recovery replaces the core singleton, and
|
|
292
|
+
// this long-lived server must re-resolve its reference or every handler
|
|
293
|
+
// keeps using the orphaned local manager.
|
|
294
|
+
let stateManager = getStateManager();
|
|
292
295
|
const resolvedAssetsDir = path.resolve(assetsDir);
|
|
293
296
|
// ── CSRF token ──────────────────────────────────────────────────────────
|
|
294
297
|
// Fresh per server-start. Exposed to the SPA via X-CSRF-Token on every
|
|
@@ -336,10 +339,20 @@ export async function startDashboardServer(options) {
|
|
|
336
339
|
/** Merge pending gist-sync warnings into a partialFailures payload for the
|
|
337
340
|
* SPA banner without coupling their lifecycles. */
|
|
338
341
|
function withPendingGistWarnings(failures) {
|
|
339
|
-
|
|
342
|
+
const extras = [...pendingGistSyncWarnings, ...recoveryLossNotices];
|
|
343
|
+
if (gistConfiguredButLocal()) {
|
|
344
|
+
extras.push(recoveryHaltedReason === null
|
|
345
|
+
? GIST_DEGRADED_WARNING
|
|
346
|
+
: `Gist persistence is configured but recovery FAILED permanently: ${recoveryHaltedReason} — ` +
|
|
347
|
+
'fix the Gist setup (check the token gist scope, or run state-show), then restart the dashboard.');
|
|
348
|
+
}
|
|
349
|
+
else if (gistBootstrapDegraded()) {
|
|
350
|
+
extras.push(GIST_STALE_BOOTSTRAP_WARNING);
|
|
351
|
+
}
|
|
352
|
+
if (extras.length === 0)
|
|
340
353
|
return failures;
|
|
341
354
|
const base = failures ?? [];
|
|
342
|
-
return [...base, ...
|
|
355
|
+
return [...base, ...extras.filter((w) => !base.includes(w))];
|
|
343
356
|
}
|
|
344
357
|
/** Push-before-pull (#1417): an un-pushed mutation would be silently
|
|
345
358
|
* reverted by the next Gist pull. Retry the checkpoint first so a recovered
|
|
@@ -350,6 +363,92 @@ export async function startDashboardServer(options) {
|
|
|
350
363
|
return;
|
|
351
364
|
recordGistSyncOutcome(await maybeCheckpoint(stateManager, MODULE));
|
|
352
365
|
}
|
|
366
|
+
/** True while the config asks for gist but this process's manager is
|
|
367
|
+
* local-only (#1433) — the degraded window in which every dashboard
|
|
368
|
+
* mutation is acknowledged and then clobbered by the next pull. */
|
|
369
|
+
function gistConfiguredButLocal() {
|
|
370
|
+
// Defensive: this is an advisory check that runs on EVERY request path
|
|
371
|
+
// (rebuilds, recovery probes). A getState failure has its own handling
|
|
372
|
+
// wherever state is actually consumed — the degraded probe must not
|
|
373
|
+
// become a new crash surface in front of it (#994's stale-serving path).
|
|
374
|
+
try {
|
|
375
|
+
return stateManager.getState().config.persistence === 'gist' && !stateManager.isGistMode();
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
warn(MODULE, `Degraded-gist probe failed (treating as not degraded): ${errorMessage(err)}`);
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/** Gist-backed but the bootstrap itself fell back to the local cache —
|
|
383
|
+
* reads may be stale even though isGistMode() is true (#1433 review). */
|
|
384
|
+
function gistBootstrapDegraded() {
|
|
385
|
+
try {
|
|
386
|
+
return stateManager.isGistMode() && stateManager.isGistDegraded();
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const GIST_DEGRADED_WARNING = 'Gist persistence is configured but this dashboard process is running LOCAL-ONLY; ' +
|
|
393
|
+
'changes made here will NOT sync and may be overwritten by the next successful Gist read. ' +
|
|
394
|
+
'Recovery is retried automatically.';
|
|
395
|
+
const GIST_STALE_BOOTSTRAP_WARNING = 'Gist persistence is active but the last Gist read fell back to the local cache; ' +
|
|
396
|
+
'data shown may be stale until the next successful Gist read.';
|
|
397
|
+
// Recovery throttling + halt (#1433 review): a PERMANENT failure (token
|
|
398
|
+
// lacks gist scope, corrupt Gist) must not turn every dashboard poll into
|
|
399
|
+
// a doomed GitHub round trip for the life of the server, and its root
|
|
400
|
+
// cause must reach the banner — the detached spawn discards stderr.
|
|
401
|
+
const RECOVERY_RETRY_INTERVAL_MS = 30_000;
|
|
402
|
+
let lastRecoveryAttemptAt = 0;
|
|
403
|
+
let recoveryHaltedReason = null;
|
|
404
|
+
// Mutations acknowledged while degraded (#1433 review): a successful
|
|
405
|
+
// recovery bootstraps FROM the existing Gist, which reverts them — the
|
|
406
|
+
// user must get a retrospective notice, not just the prospective banner
|
|
407
|
+
// that clears at the exact moment of the loss. Cleared on a successful
|
|
408
|
+
// full refresh (by then the user has seen the notice across the window).
|
|
409
|
+
let degradedMutationCount = 0;
|
|
410
|
+
let recoveryLossNotices = [];
|
|
411
|
+
/** Re-attempt gist init while degraded (#1433). The serve process used to
|
|
412
|
+
* bootstrap exactly once at CLI preAction — with its stderr discarded by
|
|
413
|
+
* the detached spawn — and never retry, so one blip at startup meant
|
|
414
|
+
* local-only writes for the server's lifetime. Never throws. Transient
|
|
415
|
+
* failures retry no more often than RECOVERY_RETRY_INTERVAL_MS; permanent
|
|
416
|
+
* (ConfigurationError-class) failures halt retries and surface the reason
|
|
417
|
+
* in the banner. */
|
|
418
|
+
async function maybeRecoverGist() {
|
|
419
|
+
if (!gistConfiguredButLocal())
|
|
420
|
+
return;
|
|
421
|
+
if (recoveryHaltedReason !== null)
|
|
422
|
+
return;
|
|
423
|
+
const now = Date.now();
|
|
424
|
+
if (now - lastRecoveryAttemptAt < RECOVERY_RETRY_INTERVAL_MS)
|
|
425
|
+
return;
|
|
426
|
+
const currentToken = token || getGitHubToken();
|
|
427
|
+
// A token-less probe is free (the auth cache answers instantly) and must
|
|
428
|
+
// not burn the retry window — stamp only when a real attempt starts.
|
|
429
|
+
if (!currentToken)
|
|
430
|
+
return;
|
|
431
|
+
lastRecoveryAttemptAt = now;
|
|
432
|
+
try {
|
|
433
|
+
await ensureGistPersistence(currentToken);
|
|
434
|
+
// The upgrade replaced the core singleton — re-resolve our reference.
|
|
435
|
+
stateManager = getStateManager();
|
|
436
|
+
if (stateManager.isGistMode() && degradedMutationCount > 0) {
|
|
437
|
+
recoveryLossNotices.push(`Gist persistence recovered, but ${degradedMutationCount} change(s) made while degraded were ` +
|
|
438
|
+
'saved locally only and were NOT merged into the Gist — they may have been reverted in this view. ' +
|
|
439
|
+
'Re-apply anything missing.');
|
|
440
|
+
degradedMutationCount = 0;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
catch (err) {
|
|
444
|
+
// ConfigurationError-class failures are permanent until the user acts
|
|
445
|
+
// (#1202 semantics) — stop hammering GitHub and say WHY in the banner.
|
|
446
|
+
if (err instanceof ConfigurationError) {
|
|
447
|
+
recoveryHaltedReason = errorMessage(err);
|
|
448
|
+
}
|
|
449
|
+
warn(MODULE, `Gist recovery attempt failed: ${errorMessage(err)}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
353
452
|
if (!cachedDigest) {
|
|
354
453
|
throw new Error('No dashboard data available. Run the daily check first: GITHUB_TOKEN=$(gh auth token) npm start -- daily');
|
|
355
454
|
}
|
|
@@ -397,6 +496,11 @@ export async function startDashboardServer(options) {
|
|
|
397
496
|
}
|
|
398
497
|
else {
|
|
399
498
|
stateChanged = stateManager.reloadIfChanged();
|
|
499
|
+
await maybeRecoverGist();
|
|
500
|
+
// A successful recovery is a state-source change: rebuild so the
|
|
501
|
+
// degraded banner clears without waiting for another edit.
|
|
502
|
+
if (stateManager.isGistMode())
|
|
503
|
+
stateChanged = true;
|
|
400
504
|
}
|
|
401
505
|
// Also rebuild when the vetted issue list file was edited outside this server (#924)
|
|
402
506
|
const currentIssueListMtimeMs = getIssueListMtimeMs();
|
|
@@ -486,7 +590,15 @@ export async function startDashboardServer(options) {
|
|
|
486
590
|
await stateManager.refreshFromGist();
|
|
487
591
|
}
|
|
488
592
|
else {
|
|
489
|
-
stateManager.reloadIfChanged()
|
|
593
|
+
if (stateManager.reloadIfChanged()) {
|
|
594
|
+
// An external config edit may BE the fix for a permanently-halted
|
|
595
|
+
// recovery (new token scope, repaired gist id) — give it one fresh
|
|
596
|
+
// attempt cycle (#1433 pass-2).
|
|
597
|
+
recoveryHaltedReason = null;
|
|
598
|
+
}
|
|
599
|
+
// reloadIfChanged may have just pulled a persistence=gist flip made
|
|
600
|
+
// from a terminal, and a degraded server heals here too (#1433).
|
|
601
|
+
await maybeRecoverGist();
|
|
490
602
|
}
|
|
491
603
|
}
|
|
492
604
|
async function handleAction(req, res) {
|
|
@@ -596,6 +708,11 @@ export async function startDashboardServer(options) {
|
|
|
596
708
|
// cachedPartialFailures — because gist warnings clear on a successful
|
|
597
709
|
// PUSH, while fetch failures clear on a successful pull/refresh.
|
|
598
710
|
recordGistSyncOutcome(gistSyncWarning);
|
|
711
|
+
// Count mutations acknowledged while degraded (#1433): a later recovery
|
|
712
|
+
// bootstraps from the existing Gist and reverts them, and the loss
|
|
713
|
+
// notice needs to know whether there is anything to lose.
|
|
714
|
+
if (gistConfiguredButLocal())
|
|
715
|
+
degradedMutationCount++;
|
|
599
716
|
// Rebuild dashboard data from cached digest + updated state. Persist
|
|
600
717
|
// the last-known partialFailures across action rebuilds (#1035) so the
|
|
601
718
|
// SPA banner survives user interactions until the next successful
|
|
@@ -612,6 +729,12 @@ export async function startDashboardServer(options) {
|
|
|
612
729
|
return;
|
|
613
730
|
}
|
|
614
731
|
try {
|
|
732
|
+
// Clear PRE-EXISTING loss notices before the reload: by now they have
|
|
733
|
+
// been visible across the degraded window. Order matters — this
|
|
734
|
+
// refresh's own reloadState may RECOVER and produce a fresh notice,
|
|
735
|
+
// which must survive into the rebuild below, not be wiped 10 lines
|
|
736
|
+
// after its creation (#1433 pass-2).
|
|
737
|
+
recoveryLossNotices = [];
|
|
615
738
|
await reloadState();
|
|
616
739
|
warn(MODULE, 'Refreshing dashboard data from GitHub...');
|
|
617
740
|
const result = await fetchDashboardData(currentToken);
|
|
@@ -740,12 +863,15 @@ export async function startDashboardServer(options) {
|
|
|
740
863
|
if (token) {
|
|
741
864
|
fetchDashboardData(token)
|
|
742
865
|
.then(async (result) => {
|
|
866
|
+
// Same clear-before-recover ordering as handleRefresh (#1433 pass-2).
|
|
867
|
+
recoveryLossNotices = [];
|
|
743
868
|
if (stateManager.isGistMode()) {
|
|
744
869
|
await flushPendingGistSync();
|
|
745
870
|
await stateManager.refreshFromGist();
|
|
746
871
|
}
|
|
747
872
|
else {
|
|
748
873
|
stateManager.reloadIfChanged();
|
|
874
|
+
await maybeRecoverGist();
|
|
749
875
|
}
|
|
750
876
|
cachedDigest = result.digest;
|
|
751
877
|
cachedCommentedIssues = result.commentedIssues;
|
package/dist/core/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Core module exports
|
|
3
3
|
* Re-exports all core functionality for convenient imports
|
|
4
4
|
*/
|
|
5
|
-
export { StateManager, getStateManager, getStateManagerAsync, ensureGistPersistence, type GistPersistenceStatus, maybeCheckpoint, resetStateManager, type Stats, } from './state.js';
|
|
5
|
+
export { StateManager, getStateManager, getStateManagerAsync, ensureGistPersistence, bootstrapGistBestEffort, type GistPersistenceStatus, maybeCheckpoint, resetStateManager, type Stats, } from './state.js';
|
|
6
6
|
export { GistStateStore } from './gist-state-store.js';
|
|
7
7
|
export { guidelinesFilename, repoFromGuidelinesFilename, GUIDELINES_FILE_PREFIX, GUIDELINES_MAX_BYTES, GuidelinesNotAvailableError, GuidelinesTooLargeError, } from './guidelines-store.js';
|
|
8
8
|
export { PRMonitor, type PRCheckFailure, type FetchPRsResult, computeDisplayLabel, classifyCICheck, classifyFailingChecks, } from './pr-monitor.js';
|
package/dist/core/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Core module exports
|
|
3
3
|
* Re-exports all core functionality for convenient imports
|
|
4
4
|
*/
|
|
5
|
-
export { StateManager, getStateManager, getStateManagerAsync, ensureGistPersistence, maybeCheckpoint, resetStateManager, } from './state.js';
|
|
5
|
+
export { StateManager, getStateManager, getStateManagerAsync, ensureGistPersistence, bootstrapGistBestEffort, maybeCheckpoint, resetStateManager, } from './state.js';
|
|
6
6
|
export { GistStateStore } from './gist-state-store.js';
|
|
7
7
|
export { guidelinesFilename, repoFromGuidelinesFilename, GUIDELINES_FILE_PREFIX, GUIDELINES_MAX_BYTES, GuidelinesNotAvailableError, GuidelinesTooLargeError, } from './guidelines-store.js';
|
|
8
8
|
export { PRMonitor, computeDisplayLabel, classifyCICheck, classifyFailingChecks, } from './pr-monitor.js';
|
package/dist/core/state.d.ts
CHANGED
|
@@ -456,6 +456,25 @@ export type GistPersistenceStatus =
|
|
|
456
456
|
* to local-only (transient network failure). A later call may recover. */
|
|
457
457
|
| 'degraded';
|
|
458
458
|
export declare function ensureGistPersistence(token: string | null): Promise<GistPersistenceStatus>;
|
|
459
|
+
/**
|
|
460
|
+
* Best-effort gist bootstrap for entry points WITHOUT the auth gate (#1431):
|
|
461
|
+
* the CLI's localOnly commands (shelve/move/dismiss/override/config/setup/...)
|
|
462
|
+
* skip token enforcement, but a gist-configured user's mutations must still
|
|
463
|
+
* reach the Gist — their maybeCheckpoint calls silently no-op when the
|
|
464
|
+
* singleton never bootstrapped.
|
|
465
|
+
*
|
|
466
|
+
* Returns a human-readable LOCAL-ONLY warning when the process will write
|
|
467
|
+
* local-only despite a gist config, or null when no warning is needed
|
|
468
|
+
* (local mode, or gist mode successfully activated).
|
|
469
|
+
*
|
|
470
|
+
* Ordering: peek first with no token (zero spawn cost for genuinely-local
|
|
471
|
+
* users), fetch a token only when the config asks for gist. Hard
|
|
472
|
+
* ConfigurationErrors are converted to a warning instead of thrown — the
|
|
473
|
+
* localOnly set includes config/setup, the very commands needed to REPAIR a
|
|
474
|
+
* broken gist setup, so they must not be bricked by it. (The auth-gated
|
|
475
|
+
* path keeps throwing per #1202.)
|
|
476
|
+
*/
|
|
477
|
+
export declare function bootstrapGistBestEffort(fetchToken: () => Promise<string | null>): Promise<string | null>;
|
|
459
478
|
/**
|
|
460
479
|
* Reset the singleton StateManager instance to null. Intended for test isolation.
|
|
461
480
|
*/
|
package/dist/core/state.js
CHANGED
|
@@ -1000,6 +1000,49 @@ export async function ensureGistPersistence(token) {
|
|
|
1000
1000
|
const mgr = await getStateManagerAsync(token);
|
|
1001
1001
|
return mgr.isGistMode() ? 'gist' : 'degraded';
|
|
1002
1002
|
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Best-effort gist bootstrap for entry points WITHOUT the auth gate (#1431):
|
|
1005
|
+
* the CLI's localOnly commands (shelve/move/dismiss/override/config/setup/...)
|
|
1006
|
+
* skip token enforcement, but a gist-configured user's mutations must still
|
|
1007
|
+
* reach the Gist — their maybeCheckpoint calls silently no-op when the
|
|
1008
|
+
* singleton never bootstrapped.
|
|
1009
|
+
*
|
|
1010
|
+
* Returns a human-readable LOCAL-ONLY warning when the process will write
|
|
1011
|
+
* local-only despite a gist config, or null when no warning is needed
|
|
1012
|
+
* (local mode, or gist mode successfully activated).
|
|
1013
|
+
*
|
|
1014
|
+
* Ordering: peek first with no token (zero spawn cost for genuinely-local
|
|
1015
|
+
* users), fetch a token only when the config asks for gist. Hard
|
|
1016
|
+
* ConfigurationErrors are converted to a warning instead of thrown — the
|
|
1017
|
+
* localOnly set includes config/setup, the very commands needed to REPAIR a
|
|
1018
|
+
* broken gist setup, so they must not be bricked by it. (The auth-gated
|
|
1019
|
+
* path keeps throwing per #1202.)
|
|
1020
|
+
*/
|
|
1021
|
+
export async function bootstrapGistBestEffort(fetchToken) {
|
|
1022
|
+
let reason = null;
|
|
1023
|
+
try {
|
|
1024
|
+
let status = await ensureGistPersistence(null);
|
|
1025
|
+
if (status === 'no-token') {
|
|
1026
|
+
status = await ensureGistPersistence(await fetchToken());
|
|
1027
|
+
}
|
|
1028
|
+
if (status === 'no-token')
|
|
1029
|
+
reason = 'no GitHub token is available';
|
|
1030
|
+
else if (status === 'state-unreadable')
|
|
1031
|
+
reason = 'the state file could not be read';
|
|
1032
|
+
else if (status === 'degraded')
|
|
1033
|
+
reason = 'Gist initialization hit a transient network failure';
|
|
1034
|
+
}
|
|
1035
|
+
catch (err) {
|
|
1036
|
+
reason =
|
|
1037
|
+
err instanceof ConfigurationError
|
|
1038
|
+
? `Gist initialization failed (${errorMessage(err)}) — fix the Gist setup (check the token's gist scope, or run state-show / setup) before relying on sync`
|
|
1039
|
+
: `Gist initialization failed: ${errorMessage(err)}`;
|
|
1040
|
+
}
|
|
1041
|
+
if (reason === null)
|
|
1042
|
+
return null;
|
|
1043
|
+
return (`Gist persistence is configured but ${reason} — changes made by this command are ` +
|
|
1044
|
+
'LOCAL-ONLY and may be overwritten by the next successful Gist sync.');
|
|
1045
|
+
}
|
|
1003
1046
|
/**
|
|
1004
1047
|
* Reset the singleton StateManager instance to null. Intended for test isolation.
|
|
1005
1048
|
*/
|
|
@@ -16,7 +16,16 @@ export interface JsonOutput<T = unknown> {
|
|
|
16
16
|
error?: string;
|
|
17
17
|
errorCode?: ErrorCode;
|
|
18
18
|
timestamp: string;
|
|
19
|
+
/** Present when the process is gist-configured but running local-only
|
|
20
|
+
* (#1433): machine consumers of mutating --json commands must see that
|
|
21
|
+
* the mutation will not sync. Set once per process from the CLI bootstrap
|
|
22
|
+
* via {@link setEnvelopeGistWarning}; envelope-level (not data-level) so
|
|
23
|
+
* per-command output schemas are untouched. */
|
|
24
|
+
gistInitWarning?: string;
|
|
19
25
|
}
|
|
26
|
+
/** Set (or clear) the gist degradation warning carried by every subsequent
|
|
27
|
+
* JSON envelope. Exported for the CLI bootstrap and for test isolation. */
|
|
28
|
+
export declare function setEnvelopeGistWarning(warning: string | null): void;
|
|
20
29
|
/**
|
|
21
30
|
* Deduplicated daily digest for JSON output (#287).
|
|
22
31
|
*
|
|
@@ -51,7 +60,7 @@ export interface CompactRepoGroup {
|
|
|
51
60
|
* See `DailyWarning` and issue #1042 for the rationale — keeping this a
|
|
52
61
|
* fixed union so downstream consumers can switch on it without drift.
|
|
53
62
|
*/
|
|
54
|
-
export type DailyWarningPhase = 'fetch' | 'repo-scores' | 'analytics' | 'scout-sync' | 'partition' | 'dismiss-filter' | 'gist-checkpoint' | 'gist-staleness' | 'state-load';
|
|
63
|
+
export type DailyWarningPhase = 'fetch' | 'repo-scores' | 'analytics' | 'scout-sync' | 'partition' | 'dismiss-filter' | 'gist-init' | 'gist-checkpoint' | 'gist-staleness' | 'state-load';
|
|
55
64
|
/**
|
|
56
65
|
* A single non-fatal failure surfaced from the `daily` pipeline. Unlike
|
|
57
66
|
* `PRCheckFailure` (which is scoped to per-PR fetch errors), this covers
|
|
@@ -179,6 +188,7 @@ export declare const StatusOutputSchema: z.ZodObject<{
|
|
|
179
188
|
"scout-sync": "scout-sync";
|
|
180
189
|
partition: "partition";
|
|
181
190
|
"dismiss-filter": "dismiss-filter";
|
|
191
|
+
"gist-init": "gist-init";
|
|
182
192
|
"gist-checkpoint": "gist-checkpoint";
|
|
183
193
|
"gist-staleness": "gist-staleness";
|
|
184
194
|
"state-load": "state-load";
|
|
@@ -394,6 +404,7 @@ export declare const DailyOutputSchema: z.ZodObject<{
|
|
|
394
404
|
"scout-sync": "scout-sync";
|
|
395
405
|
partition: "partition";
|
|
396
406
|
"dismiss-filter": "dismiss-filter";
|
|
407
|
+
"gist-init": "gist-init";
|
|
397
408
|
"gist-checkpoint": "gist-checkpoint";
|
|
398
409
|
"gist-staleness": "gist-staleness";
|
|
399
410
|
"state-load": "state-load";
|
|
@@ -541,6 +552,7 @@ export declare const CompactDailyOutputSchema: z.ZodObject<{
|
|
|
541
552
|
"scout-sync": "scout-sync";
|
|
542
553
|
partition: "partition";
|
|
543
554
|
"dismiss-filter": "dismiss-filter";
|
|
555
|
+
"gist-init": "gist-init";
|
|
544
556
|
"gist-checkpoint": "gist-checkpoint";
|
|
545
557
|
"gist-staleness": "gist-staleness";
|
|
546
558
|
"state-load": "state-load";
|
package/dist/formatters/json.js
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { fenceFetchedPR } from '../core/untrusted-content.js';
|
|
7
|
+
// Process-level gist degradation warning threaded into every JSON envelope
|
|
8
|
+
// (#1433). The CLI preAction bootstrap sets it; one-shot processes never
|
|
9
|
+
// clear it (the whole invocation runs degraded or it doesn't).
|
|
10
|
+
let envelopeGistWarning = null;
|
|
11
|
+
/** Set (or clear) the gist degradation warning carried by every subsequent
|
|
12
|
+
* JSON envelope. Exported for the CLI bootstrap and for test isolation. */
|
|
13
|
+
export function setEnvelopeGistWarning(warning) {
|
|
14
|
+
envelopeGistWarning = warning;
|
|
15
|
+
}
|
|
7
16
|
export function buildStalenessWarning(info) {
|
|
8
17
|
return {
|
|
9
18
|
phase: 'gist-staleness',
|
|
@@ -90,6 +99,7 @@ const DailyWarningPhaseSchema = z.enum([
|
|
|
90
99
|
'scout-sync',
|
|
91
100
|
'partition',
|
|
92
101
|
'dismiss-filter',
|
|
102
|
+
'gist-init',
|
|
93
103
|
'gist-checkpoint',
|
|
94
104
|
'gist-staleness',
|
|
95
105
|
'state-load',
|
|
@@ -728,6 +738,7 @@ export function jsonSuccess(data) {
|
|
|
728
738
|
success: true,
|
|
729
739
|
data,
|
|
730
740
|
timestamp: new Date().toISOString(),
|
|
741
|
+
...(envelopeGistWarning ? { gistInitWarning: envelopeGistWarning } : {}),
|
|
731
742
|
};
|
|
732
743
|
}
|
|
733
744
|
/**
|
|
@@ -739,6 +750,7 @@ export function jsonError(message, errorCode) {
|
|
|
739
750
|
error: message,
|
|
740
751
|
errorCode,
|
|
741
752
|
timestamp: new Date().toISOString(),
|
|
753
|
+
...(envelopeGistWarning ? { gistInitWarning: envelopeGistWarning } : {}),
|
|
742
754
|
};
|
|
743
755
|
}
|
|
744
756
|
/**
|