@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.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,
@@ -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
- const stateManager = getStateManager();
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
- if (pendingGistSyncWarnings.length === 0)
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, ...pendingGistSyncWarnings.filter((w) => !base.includes(w))];
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;
@@ -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';
@@ -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';
@@ -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
  */
@@ -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";
@@ -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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-autopilot/core",
3
- "version": "3.13.2",
3
+ "version": "3.13.4",
4
4
  "description": "CLI and core library for managing open source contributions",
5
5
  "type": "module",
6
6
  "bin": {