@oss-autopilot/core 3.10.0 → 3.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/cli-registry.d.ts +7 -0
  2. package/dist/cli-registry.js +58 -5
  3. package/dist/cli.bundle.cjs +165 -112
  4. package/dist/cli.js +11 -3
  5. package/dist/commands/comments.js +31 -15
  6. package/dist/commands/compliance-score.js +12 -4
  7. package/dist/commands/daily-render.d.ts +2 -1
  8. package/dist/commands/daily-render.js +8 -2
  9. package/dist/commands/daily.d.ts +3 -1
  10. package/dist/commands/daily.js +54 -4
  11. package/dist/commands/dashboard-data.d.ts +17 -0
  12. package/dist/commands/dashboard-data.js +62 -4
  13. package/dist/commands/dashboard-server.js +100 -26
  14. package/dist/commands/dismiss.d.ts +4 -0
  15. package/dist/commands/dismiss.js +4 -4
  16. package/dist/commands/guidelines.d.ts +19 -0
  17. package/dist/commands/guidelines.js +23 -4
  18. package/dist/commands/index.d.ts +5 -1
  19. package/dist/commands/index.js +4 -0
  20. package/dist/commands/list-move-tier.d.ts +11 -3
  21. package/dist/commands/list-move-tier.js +18 -7
  22. package/dist/commands/move.d.ts +2 -0
  23. package/dist/commands/move.js +12 -8
  24. package/dist/commands/repo-vet.js +30 -8
  25. package/dist/commands/search.js +17 -3
  26. package/dist/commands/shelve.d.ts +4 -0
  27. package/dist/commands/shelve.js +4 -4
  28. package/dist/commands/verify-issue.d.ts +20 -0
  29. package/dist/commands/verify-issue.js +32 -0
  30. package/dist/core/daily-logic.js +65 -52
  31. package/dist/core/gist-state-store.js +42 -7
  32. package/dist/core/index.d.ts +3 -1
  33. package/dist/core/index.js +3 -1
  34. package/dist/core/issue-conversation.js +15 -2
  35. package/dist/core/issue-verification.d.ts +91 -0
  36. package/dist/core/issue-verification.js +270 -0
  37. package/dist/core/paths.d.ts +12 -0
  38. package/dist/core/paths.js +16 -0
  39. package/dist/core/pr-attention.d.ts +52 -0
  40. package/dist/core/pr-attention.js +76 -0
  41. package/dist/core/pr-comments-fetcher.d.ts +10 -2
  42. package/dist/core/pr-comments-fetcher.js +22 -4
  43. package/dist/core/state-persistence.d.ts +31 -9
  44. package/dist/core/state-persistence.js +51 -16
  45. package/dist/core/state.d.ts +18 -1
  46. package/dist/core/state.js +35 -3
  47. package/dist/core/types.d.ts +7 -0
  48. package/dist/core/untrusted-content.d.ts +24 -3
  49. package/dist/core/untrusted-content.js +31 -3
  50. package/dist/formatters/json.d.ts +83 -2
  51. package/dist/formatters/json.js +55 -1
  52. package/package.json +7 -7
@@ -6,15 +6,12 @@
6
6
  import * as fs from 'node:fs';
7
7
  import * as path from 'node:path';
8
8
  import { AgentStateSchema } from './state-schema.js';
9
- import { getStatePath, getBackupDir, getDataDir } from './paths.js';
9
+ import { getStatePath, getBackupDir, getDataDir, getLegacyStatePath, getLegacyBackupDir } from './paths.js';
10
10
  import { errorMessage, ConcurrencyError } from './errors.js';
11
11
  import { debug, warn } from './logger.js';
12
12
  const MODULE = 'state';
13
13
  // Lock file timeout: if a lock is older than this, it is considered stale
14
14
  const LOCK_TIMEOUT_MS = 30_000; // 30 seconds
15
- // Legacy path for migration
16
- const LEGACY_STATE_FILE = path.join(process.cwd(), 'data', 'state.json');
17
- const LEGACY_BACKUP_DIR = path.join(process.cwd(), 'data', 'backups');
18
15
  /**
19
16
  * Check whether an existing lock file is stale (expired or corrupt).
20
17
  * Returns true if the lock should be considered stale and can be removed.
@@ -182,6 +179,10 @@ export function createFreshState() {
182
179
  */
183
180
  function migrateFromLegacyLocation() {
184
181
  const newStatePath = getStatePath();
182
+ // Resolved via paths.js (not module constants) so tests can mock the
183
+ // legacy location into per-test temp dirs — see #1382.
184
+ const LEGACY_STATE_FILE = getLegacyStatePath();
185
+ const LEGACY_BACKUP_DIR = getLegacyBackupDir();
185
186
  // If new state already exists, no migration needed
186
187
  if (fs.existsSync(newStatePath)) {
187
188
  return false;
@@ -316,11 +317,33 @@ function tryRestoreFromBackup() {
316
317
  }
317
318
  return null;
318
319
  }
320
+ /**
321
+ * Copy a rejected state file to `<statePath>.rejected-<timestamp>` so the
322
+ * user can recover data manually before the file is overwritten by a backup
323
+ * restore or fresh state. Returns the preserved path, or null if the copy
324
+ * failed (preservation is best-effort and never throws).
325
+ */
326
+ function preserveRejectedStateFile(statePath) {
327
+ try {
328
+ const rejectedPath = statePath + '.rejected-' + Date.now();
329
+ fs.copyFileSync(statePath, rejectedPath);
330
+ warn(MODULE, `Previous state preserved at: ${rejectedPath}`);
331
+ return rejectedPath;
332
+ }
333
+ catch (preserveErr) {
334
+ warn(MODULE, `Could not preserve rejected state file: ${errorMessage(preserveErr)}`);
335
+ return null;
336
+ }
337
+ }
319
338
  /**
320
339
  * Load state from file, or create initial state if none exists.
321
340
  * If the main state file is corrupted, attempts to restore from the most recent backup.
322
341
  * Performs migration from legacy ./data/ location if needed.
323
- * @returns Object with the loaded state and the file's mtime (for change detection).
342
+ * @returns Object with the loaded state, the file's mtime (for change detection),
343
+ * and recovery details when the main file was rejected.
344
+ * @throws Error when the state file exists but cannot be read due to a
345
+ * permission error (EACCES/EPERM) — that is an environment problem, not
346
+ * corruption, so restore-or-fresh would mask it and risk clobbering data.
324
347
  */
325
348
  export function loadState() {
326
349
  // Try to migrate from legacy location first
@@ -355,21 +378,16 @@ export function loadState() {
355
378
  warn(MODULE, 'Attempting to restore from backup...');
356
379
  debug(MODULE, 'Full validation errors:', parsed.error.issues);
357
380
  // Preserve the rejected state file so the user can recover
358
- try {
359
- const rejectedPath = statePath + '.rejected-' + Date.now();
360
- fs.copyFileSync(statePath, rejectedPath);
361
- warn(MODULE, `Previous state preserved at: ${rejectedPath}`);
362
- }
363
- catch (preserveErr) {
364
- warn(MODULE, `Could not preserve rejected state file: ${errorMessage(preserveErr)}`);
365
- }
381
+ const rejectedPath = preserveRejectedStateFile(statePath);
382
+ const reason = `Invalid state file structure: ${summary}`;
366
383
  const restoredState = tryRestoreFromBackup();
367
384
  if (restoredState) {
368
385
  const mtimeMs = safeGetMtimeMs(statePath);
369
- return { state: restoredState, mtimeMs };
386
+ const recovery = { source: 'backup', rejectedPath, reason };
387
+ return { state: restoredState, mtimeMs, recovery };
370
388
  }
371
389
  warn(MODULE, 'No valid backup found, starting fresh');
372
- return { state: createFreshState(), mtimeMs: 0 };
390
+ return { state: createFreshState(), mtimeMs: 0, recovery: { source: 'fresh', rejectedPath, reason } };
373
391
  }
374
392
  // Save migrated state only after validation succeeds
375
393
  if (wasMigrated) {
@@ -408,14 +426,31 @@ export function loadState() {
408
426
  }
409
427
  }
410
428
  catch (error) {
429
+ // Permission errors are environmental, not corruption — restoring a
430
+ // backup or starting fresh here would mask the real problem and risk
431
+ // clobbering a perfectly valid state file. Rethrow with guidance (#1371).
432
+ const code = error?.code;
433
+ if (code === 'EACCES' || code === 'EPERM') {
434
+ throw new Error(`Permission denied reading state file (${code}): ${errorMessage(error)}. ` +
435
+ 'This is an environment problem, not state corruption. Check the ownership and ' +
436
+ 'permissions of ~/.oss-autopilot/state.json (e.g. run `ls -l ~/.oss-autopilot/state.json` ' +
437
+ 'and fix with chown/chmod), then retry.', { cause: error });
438
+ }
411
439
  warn(MODULE, 'Error loading state:', error);
412
440
  warn(MODULE, 'Attempting to restore from backup...');
441
+ // Preserve the unparseable file before tryRestoreFromBackup() overwrites
442
+ // it with restored backup contents (#1371). Only attempt when the file
443
+ // actually exists — the error may have come from elsewhere in the block.
444
+ const rejectedPath = fs.existsSync(statePath) ? preserveRejectedStateFile(statePath) : null;
445
+ const reason = `Error loading state: ${errorMessage(error)}`;
413
446
  const restoredState = tryRestoreFromBackup();
414
447
  if (restoredState) {
415
448
  const mtimeMs = safeGetMtimeMs(statePath);
416
- return { state: restoredState, mtimeMs };
449
+ const recovery = { source: 'backup', rejectedPath, reason };
450
+ return { state: restoredState, mtimeMs, recovery };
417
451
  }
418
452
  warn(MODULE, 'No valid backup found, starting fresh');
453
+ return { state: createFreshState(), mtimeMs: 0, recovery: { source: 'fresh', rejectedPath, reason } };
419
454
  }
420
455
  debug(MODULE, 'No existing state found, initializing...');
421
456
  return { state: createFreshState(), mtimeMs: 0 };
@@ -4,9 +4,11 @@
4
4
  * and scoring logic to repo-score-manager.ts.
5
5
  */
6
6
  import { AgentState, TrackedIssue, RepoScore, RepoScoreUpdate, DailyDigest, LocalRepoCache, StatusOverride, FetchedPRStatus, StoredMergedPR, StoredClosedPR } from './types.js';
7
+ import { type LoadRecoveryInfo } from './state-persistence.js';
7
8
  import type { Stats } from './repo-score-manager.js';
8
9
  import { GistStateStore } from './gist-state-store.js';
9
10
  export { acquireLock, releaseLock, atomicWriteFileSync } from './state-persistence.js';
11
+ export type { LoadRecoveryInfo } from './state-persistence.js';
10
12
  export type { Stats } from './repo-score-manager.js';
11
13
  /**
12
14
  * Push state to the backing Gist when Gist mode is active. Best-effort:
@@ -17,8 +19,15 @@ export type { Stats } from './repo-score-manager.js';
17
19
  * Intended for state-mutating PR-flow commands (shelve / unshelve / move /
18
20
  * dismiss / undismiss / claim) so Gist-sync users don't see day-long drift
19
21
  * between machines waiting for the next `daily` checkpoint. See issue #1036.
22
+ *
23
+ * @returns `null` when the push succeeded (or Gist mode is off); otherwise a
24
+ * human-readable warning the caller should surface in its structured output.
25
+ * `checkpoint()` resolves `false` without throwing when the push failed
26
+ * after its retry — previously that was discarded everywhere except
27
+ * `state --sync`, so a failed cross-machine sync reported clean success
28
+ * (#1370). stderr `warn()` alone is invisible to MCP/dashboard consumers.
20
29
  */
21
- export declare function maybeCheckpoint(stateManager: StateManager, callerModule: string): Promise<void>;
30
+ export declare function maybeCheckpoint(stateManager: StateManager, callerModule: string): Promise<string | null>;
22
31
  /**
23
32
  * Singleton manager for persistent agent state stored in ~/.oss-autopilot/state.json.
24
33
  *
@@ -54,6 +63,7 @@ export declare class StateManager {
54
63
  protected gistDegraded: boolean;
55
64
  private staleness;
56
65
  private lastSuccessfulRefreshAt;
66
+ private loadRecovery;
57
67
  /**
58
68
  * Create a new StateManager instance.
59
69
  * @param inMemoryOnly - When true, state is held only in memory and never read from or
@@ -157,6 +167,13 @@ export declare class StateManager {
157
167
  * Returns true if state was reloaded, false if unchanged or in-memory mode.
158
168
  */
159
169
  reloadIfChanged(): boolean;
170
+ /**
171
+ * Recovery details from the most recent state load, or null when the last
172
+ * load read the state file cleanly (or no load from disk has happened,
173
+ * e.g. in-memory mode). Set when loadState() fell back to a backup or
174
+ * fresh state because the main file was corrupt or invalid (#1371).
175
+ */
176
+ getLoadRecovery(): LoadRecoveryInfo | null;
160
177
  /**
161
178
  * Re-fetch state from the backing Gist (if in Gist mode).
162
179
  * Throttled to once per 30 seconds by GistStateStore. Returns true if state was refreshed.
@@ -42,15 +42,30 @@ function filterValidUrlEntries(entries, kind) {
42
42
  * Intended for state-mutating PR-flow commands (shelve / unshelve / move /
43
43
  * dismiss / undismiss / claim) so Gist-sync users don't see day-long drift
44
44
  * between machines waiting for the next `daily` checkpoint. See issue #1036.
45
+ *
46
+ * @returns `null` when the push succeeded (or Gist mode is off); otherwise a
47
+ * human-readable warning the caller should surface in its structured output.
48
+ * `checkpoint()` resolves `false` without throwing when the push failed
49
+ * after its retry — previously that was discarded everywhere except
50
+ * `state --sync`, so a failed cross-machine sync reported clean success
51
+ * (#1370). stderr `warn()` alone is invisible to MCP/dashboard consumers.
45
52
  */
46
53
  export async function maybeCheckpoint(stateManager, callerModule) {
47
54
  if (!stateManager.isGistMode())
48
- return;
55
+ return null;
49
56
  try {
50
- await stateManager.checkpoint();
57
+ const pushed = await stateManager.checkpoint();
58
+ if (!pushed) {
59
+ const msg = 'Gist checkpoint push failed after retry; the local mutation is saved and will sync on the next successful push';
60
+ warn(callerModule, msg);
61
+ return msg;
62
+ }
63
+ return null;
51
64
  }
52
65
  catch (err) {
53
- warn(callerModule, `Gist checkpoint failed (local mutation succeeded, will retry on next push): ${errorMessage(err)}`);
66
+ const msg = `Gist checkpoint failed (local mutation succeeded, will retry on next push): ${errorMessage(err)}`;
67
+ warn(callerModule, msg);
68
+ return msg;
54
69
  }
55
70
  }
56
71
  export class StateManager {
@@ -63,6 +78,7 @@ export class StateManager {
63
78
  gistDegraded = false;
64
79
  staleness = null;
65
80
  lastSuccessfulRefreshAt = null;
81
+ loadRecovery = null;
66
82
  /**
67
83
  * Create a new StateManager instance.
68
84
  * @param inMemoryOnly - When true, state is held only in memory and never read from or
@@ -78,6 +94,7 @@ export class StateManager {
78
94
  const result = loadState();
79
95
  this.state = result.state;
80
96
  this.lastLoadedMtimeMs = result.mtimeMs;
97
+ this.loadRecovery = result.recovery ?? null;
81
98
  this.tryReconcilePRCounts();
82
99
  }
83
100
  }
@@ -97,9 +114,11 @@ export class StateManager {
97
114
  // Check if local state exists for migration
98
115
  const statePath = getStatePath();
99
116
  let result;
117
+ let localRecovery = null;
100
118
  if (fs.existsSync(statePath)) {
101
119
  // Existing user: load local state and migrate it into the Gist if no Gist exists yet.
102
120
  const localStateResult = loadState();
121
+ localRecovery = localStateResult.recovery ?? null;
103
122
  const migrationResult = await gistStore.bootstrapWithMigration(localStateResult.state);
104
123
  result = migrationResult;
105
124
  // If a new Gist was just created from local state, rename the local file
@@ -126,6 +145,9 @@ export class StateManager {
126
145
  manager.gistStore = gistStore;
127
146
  manager.gistDegraded = result.degraded ?? false;
128
147
  manager.inMemoryOnly = false; // re-enable persistence
148
+ // Surface a recovery that happened while loading the local file for Gist
149
+ // migration — the recovered (or fresh) state is what got migrated.
150
+ manager.loadRecovery = localRecovery;
129
151
  // Seed the staleness marker if bootstrap fell back to the local cache —
130
152
  // a `daily` running on a cron right after this start needs to know.
131
153
  if (result.degraded) {
@@ -335,9 +357,19 @@ export class StateManager {
335
357
  return false;
336
358
  this.state = result.state;
337
359
  this.lastLoadedMtimeMs = result.mtimeMs;
360
+ this.loadRecovery = result.recovery ?? null;
338
361
  this.tryReconcilePRCounts();
339
362
  return true;
340
363
  }
364
+ /**
365
+ * Recovery details from the most recent state load, or null when the last
366
+ * load read the state file cleanly (or no load from disk has happened,
367
+ * e.g. in-memory mode). Set when loadState() fell back to a backup or
368
+ * fresh state because the main file was corrupt or invalid (#1371).
369
+ */
370
+ getLoadRecovery() {
371
+ return this.loadRecovery;
372
+ }
341
373
  /**
342
374
  * Re-fetch state from the backing Gist (if in Gist mode).
343
375
  * Throttled to once per 30 seconds by GistStateStore. Returns true if state was refreshed.
@@ -126,6 +126,8 @@ export type MaintainerActionHint = 'demo_requested' | 'tests_requested' | 'chang
126
126
  * This is never persisted in local state — it represents a point-in-time snapshot
127
127
  * of a PR's current condition.
128
128
  */
129
+ /** Unified attention bucket (#1352) — see core/pr-attention.ts for the classifier. */
130
+ export type AttentionBucket = 'needs_attention' | 'stuck_ci' | 'dormant_followup' | 'waiting';
129
131
  export interface FetchedPR {
130
132
  id: number;
131
133
  url: string;
@@ -142,6 +144,11 @@ export interface FetchedPR {
142
144
  actionReasons?: ActionReason[];
143
145
  /** How stale the PR is based on activity age. Independent of status — a PR can be both needs_addressing and dormant. */
144
146
  stalenessTier: StalenessTier;
147
+ /** Unified attention bucket (#1352), computed by `classifyAttentionBucket()`
148
+ * from status/ciStatus/reviewDecision/daysSinceActivity. Stamped by the
149
+ * dashboard data path so the SPA renders the same taxonomy the CLI brief
150
+ * counts. Optional: absent on payloads from older producers. */
151
+ attentionBucket?: AttentionBucket;
145
152
  /** Human-readable status label for consistent display (#79). E.g., "[CI Failing]", "[Needs Response]". */
146
153
  displayLabel: string;
147
154
  /** Brief description of what's happening (#79). E.g., "3 checks failed", "@maintainer commented". */
@@ -13,9 +13,22 @@
13
13
  * 3. `extractFromFence(wrapUntrustedContent(x, label))` returns `x`
14
14
  * unchanged for any input — the wrapping is lossless.
15
15
  *
16
- * Consumers should pair this with the agent-side guidance in
17
- * `workflows/reference.md` ("Prompt Injection Awareness"), which tells
18
- * the LLM to ignore instructions inside `<github-content>` blocks.
16
+ * Runtime wiring (#1372) body fields are fenced at the last serialization
17
+ * boundary into agent-facing output:
18
+ *
19
+ * - `runComments` (commands/comments.ts): review / inline / discussion
20
+ * comment bodies in the `comments` CLI `--json` + MCP tool output.
21
+ * - `fetchPRCommentBundle` (core/pr-comments-fetcher.ts): bundle bodies
22
+ * feeding `guidelines fetch-corpus` (agent-only consumer).
23
+ * - `toDailyOutput` (commands/daily.ts): `commentedIssues[].lastResponseBody`
24
+ * / `userLastCommentBody` in the daily/startup JSON. The producer
25
+ * (`issue-conversation.ts`) stays raw because the dashboard SPA and the
26
+ * CLI text renderers consume the same objects.
27
+ *
28
+ * Human-facing display paths unwrap with {@link safeExtractFromFence}.
29
+ * This pairs with the agent-side guidance in `workflows/reference.md`
30
+ * ("Prompt Injection Awareness"), which tells the LLM to treat anything
31
+ * inside `<github-content>` blocks as data, not instructions.
19
32
  *
20
33
  * Non-goals:
21
34
  * - This is NOT a content filter. We do not detect or strip prompt-
@@ -46,3 +59,11 @@ export declare function wrapUntrustedContent(text: string, label: string, meta?:
46
59
  * markdown with this; it's for tests + symmetric reasoning only.
47
60
  */
48
61
  export declare function extractFromFence(fenced: string): string;
62
+ /**
63
+ * Tolerant unwrap for human-facing display paths (CLI text mode). Returns
64
+ * the original body when `text` is a well-formed fence, and `text` unchanged
65
+ * otherwise. Display code must not crash on unfenced input (older state,
66
+ * fields that were never wrapped), so unlike {@link extractFromFence} this
67
+ * never throws.
68
+ */
69
+ export declare function safeExtractFromFence(text: string): string;
@@ -13,9 +13,22 @@
13
13
  * 3. `extractFromFence(wrapUntrustedContent(x, label))` returns `x`
14
14
  * unchanged for any input — the wrapping is lossless.
15
15
  *
16
- * Consumers should pair this with the agent-side guidance in
17
- * `workflows/reference.md` ("Prompt Injection Awareness"), which tells
18
- * the LLM to ignore instructions inside `<github-content>` blocks.
16
+ * Runtime wiring (#1372) body fields are fenced at the last serialization
17
+ * boundary into agent-facing output:
18
+ *
19
+ * - `runComments` (commands/comments.ts): review / inline / discussion
20
+ * comment bodies in the `comments` CLI `--json` + MCP tool output.
21
+ * - `fetchPRCommentBundle` (core/pr-comments-fetcher.ts): bundle bodies
22
+ * feeding `guidelines fetch-corpus` (agent-only consumer).
23
+ * - `toDailyOutput` (commands/daily.ts): `commentedIssues[].lastResponseBody`
24
+ * / `userLastCommentBody` in the daily/startup JSON. The producer
25
+ * (`issue-conversation.ts`) stays raw because the dashboard SPA and the
26
+ * CLI text renderers consume the same objects.
27
+ *
28
+ * Human-facing display paths unwrap with {@link safeExtractFromFence}.
29
+ * This pairs with the agent-side guidance in `workflows/reference.md`
30
+ * ("Prompt Injection Awareness"), which tells the LLM to treat anything
31
+ * inside `<github-content>` blocks as data, not instructions.
19
32
  *
20
33
  * Non-goals:
21
34
  * - This is NOT a content filter. We do not detect or strip prompt-
@@ -104,3 +117,18 @@ export function extractFromFence(fenced) {
104
117
  .split(AMP_ESCAPE)
105
118
  .join('&');
106
119
  }
120
+ /**
121
+ * Tolerant unwrap for human-facing display paths (CLI text mode). Returns
122
+ * the original body when `text` is a well-formed fence, and `text` unchanged
123
+ * otherwise. Display code must not crash on unfenced input (older state,
124
+ * fields that were never wrapped), so unlike {@link extractFromFence} this
125
+ * never throws.
126
+ */
127
+ export function safeExtractFromFence(text) {
128
+ try {
129
+ return extractFromFence(text);
130
+ }
131
+ catch {
132
+ return text;
133
+ }
134
+ }
@@ -3,6 +3,7 @@
3
3
  * Provides structured output that can be consumed by scripts and plugins
4
4
  */
5
5
  import { z, type ZodType } from 'zod';
6
+ import type { AttentionSummary } from '../core/pr-attention.js';
6
7
  import type { FetchedPR, DailyDigest, AgentState, RepoGroup, CommentedIssue, ShelvedPRRef, SearchPriority, CapacityAssessment, ActionableIssue, ActionableIssueType, CompactActionableIssue, ActionMenuItem, ActionMenu } from '../core/types.js';
7
8
  import type { ContributionStats } from '../core/stats.js';
8
9
  import type { PRCheckFailure } from '../core/pr-monitor.js';
@@ -50,7 +51,7 @@ export interface CompactRepoGroup {
50
51
  * See `DailyWarning` and issue #1042 for the rationale — keeping this a
51
52
  * fixed union so downstream consumers can switch on it without drift.
52
53
  */
53
- export type DailyWarningPhase = 'fetch' | 'repo-scores' | 'analytics' | 'scout-sync' | 'partition' | 'dismiss-filter' | 'gist-checkpoint' | 'gist-staleness';
54
+ export type DailyWarningPhase = 'fetch' | 'repo-scores' | 'analytics' | 'scout-sync' | 'partition' | 'dismiss-filter' | 'gist-checkpoint' | 'gist-staleness' | 'state-load';
54
55
  /**
55
56
  * A single non-fatal failure surfaced from the `daily` pipeline. Unlike
56
57
  * `PRCheckFailure` (which is scoped to per-PR fetch errors), this covers
@@ -87,6 +88,8 @@ export interface DailyOutput {
87
88
  summary: string;
88
89
  briefSummary: string;
89
90
  actionableIssues: CompactActionableIssue[];
91
+ /** Unified attention-bucket counts (#1352) — same classifier the dashboard stamps per-PR. */
92
+ attention: AttentionSummary;
90
93
  actionMenu: ActionMenu;
91
94
  commentedIssues: CommentedIssue[];
92
95
  repoGroups: CompactRepoGroup[];
@@ -119,6 +122,8 @@ export interface CompactDailyOutput {
119
122
  capacity: CapacityAssessment;
120
123
  briefSummary: string;
121
124
  actionableIssues: CompactActionableIssue[];
125
+ /** Unified attention-bucket counts (#1352), retained under --compact. */
126
+ attention: AttentionSummary;
122
127
  actionMenu: ActionMenu;
123
128
  commentedIssues: CommentedIssue[];
124
129
  /** Number of PRs that failed to fetch. Non-zero indicates partial results. */
@@ -176,6 +181,7 @@ export declare const StatusOutputSchema: z.ZodObject<{
176
181
  "dismiss-filter": "dismiss-filter";
177
182
  "gist-checkpoint": "gist-checkpoint";
178
183
  "gist-staleness": "gist-staleness";
184
+ "state-load": "state-load";
179
185
  }>;
180
186
  operation: z.ZodString;
181
187
  message: z.ZodString;
@@ -284,6 +290,12 @@ export declare const StrategyOutputSchema: z.ZodObject<{
284
290
  message: z.ZodOptional<z.ZodString>;
285
291
  }, z.core.$strip>;
286
292
  export type StrategyCommandOutput = z.infer<typeof StrategyOutputSchema>;
293
+ export declare const AttentionSummarySchema: z.ZodObject<{
294
+ needsAttention: z.ZodNumber;
295
+ stuckCI: z.ZodNumber;
296
+ dormantFollowup: z.ZodNumber;
297
+ waiting: z.ZodNumber;
298
+ }, z.core.$strip>;
287
299
  export declare const DailyOutputSchema: z.ZodObject<{
288
300
  digest: z.ZodObject<{
289
301
  generatedAt: z.ZodString;
@@ -347,6 +359,12 @@ export declare const DailyOutputSchema: z.ZodObject<{
347
359
  label: z.ZodString;
348
360
  isNewContribution: z.ZodBoolean;
349
361
  }, z.core.$strip>>;
362
+ attention: z.ZodObject<{
363
+ needsAttention: z.ZodNumber;
364
+ stuckCI: z.ZodNumber;
365
+ dormantFollowup: z.ZodNumber;
366
+ waiting: z.ZodNumber;
367
+ }, z.core.$strip>;
350
368
  actionMenu: z.ZodObject<{
351
369
  items: z.ZodArray<z.ZodObject<{
352
370
  key: z.ZodString;
@@ -378,6 +396,7 @@ export declare const DailyOutputSchema: z.ZodObject<{
378
396
  "dismiss-filter": "dismiss-filter";
379
397
  "gist-checkpoint": "gist-checkpoint";
380
398
  "gist-staleness": "gist-staleness";
399
+ "state-load": "state-load";
381
400
  }>;
382
401
  operation: z.ZodString;
383
402
  message: z.ZodString;
@@ -491,6 +510,12 @@ export declare const CompactDailyOutputSchema: z.ZodObject<{
491
510
  label: z.ZodString;
492
511
  isNewContribution: z.ZodBoolean;
493
512
  }, z.core.$strip>>;
513
+ attention: z.ZodObject<{
514
+ needsAttention: z.ZodNumber;
515
+ stuckCI: z.ZodNumber;
516
+ dormantFollowup: z.ZodNumber;
517
+ waiting: z.ZodNumber;
518
+ }, z.core.$strip>;
494
519
  actionMenu: z.ZodObject<{
495
520
  items: z.ZodArray<z.ZodObject<{
496
521
  key: z.ZodString;
@@ -518,6 +543,7 @@ export declare const CompactDailyOutputSchema: z.ZodObject<{
518
543
  "dismiss-filter": "dismiss-filter";
519
544
  "gist-checkpoint": "gist-checkpoint";
520
545
  "gist-staleness": "gist-staleness";
546
+ "state-load": "state-load";
521
547
  }>;
522
548
  operation: z.ZodString;
523
549
  message: z.ZodString;
@@ -625,6 +651,7 @@ export declare const SearchOutputSchema: z.ZodObject<{
625
651
  }, z.core.$strip>>;
626
652
  excludedRepos: z.ZodArray<z.ZodString>;
627
653
  aiPolicyBlocklist: z.ZodArray<z.ZodString>;
654
+ hiddenOwnPRCount: z.ZodNumber;
628
655
  rateLimitWarning: z.ZodOptional<z.ZodString>;
629
656
  }, z.core.$strip>;
630
657
  export declare const FeaturesOutputSchema: z.ZodObject<{
@@ -781,7 +808,7 @@ export declare const ListMoveTierOutputSchema: z.ZodObject<{
781
808
  }>;
782
809
  fromTier: z.ZodOptional<z.ZodString>;
783
810
  count: z.ZodNumber;
784
- reason: z.ZodOptional<z.ZodString>;
811
+ reason: z.ZodOptional<z.ZodLiteral<"already in target tier">>;
785
812
  }, z.core.$strip>;
786
813
  export declare const ListMarkDoneOutputSchema: z.ZodObject<{
787
814
  marked: z.ZodBoolean;
@@ -791,6 +818,52 @@ export declare const ListMarkDoneOutputSchema: z.ZodObject<{
791
818
  remainingUnderRepo: z.ZodNumber;
792
819
  reason: z.ZodOptional<z.ZodString>;
793
820
  }, z.core.$strip>;
821
+ export declare const VerifyIssueOutputSchema: z.ZodObject<{
822
+ url: z.ZodString;
823
+ owner: z.ZodString;
824
+ repo: z.ZodString;
825
+ number: z.ZodNumber;
826
+ title: z.ZodString;
827
+ state: z.ZodEnum<{
828
+ closed: "closed";
829
+ open: "open";
830
+ }>;
831
+ stateReason: z.ZodNullable<z.ZodEnum<{
832
+ completed: "completed";
833
+ reopened: "reopened";
834
+ not_planned: "not_planned";
835
+ duplicate: "duplicate";
836
+ }>>;
837
+ closedAt: z.ZodNullable<z.ZodString>;
838
+ assignees: z.ZodArray<z.ZodString>;
839
+ linkedPRs: z.ZodArray<z.ZodObject<{
840
+ number: z.ZodNumber;
841
+ url: z.ZodString;
842
+ title: z.ZodString;
843
+ state: z.ZodEnum<{
844
+ closed: "closed";
845
+ open: "open";
846
+ merged: "merged";
847
+ }>;
848
+ isDraft: z.ZodBoolean;
849
+ author: z.ZodNullable<z.ZodString>;
850
+ isOwn: z.ZodBoolean;
851
+ linkType: z.ZodEnum<{
852
+ closing: "closing";
853
+ "cross-referenced": "cross-referenced";
854
+ }>;
855
+ updatedAt: z.ZodNullable<z.ZodString>;
856
+ }, z.core.$strip>>;
857
+ verdict: z.ZodEnum<{
858
+ closed: "closed";
859
+ "own-open-pr": "own-open-pr";
860
+ taken: "taken";
861
+ "at-risk": "at-risk";
862
+ available: "available";
863
+ }>;
864
+ verdictReason: z.ZodString;
865
+ userLogin: z.ZodString;
866
+ }, z.core.$strip>;
794
867
  export declare const PostOutputSchema: z.ZodObject<{
795
868
  commentUrl: z.ZodString;
796
869
  url: z.ZodString;
@@ -798,6 +871,7 @@ export declare const PostOutputSchema: z.ZodObject<{
798
871
  export declare const ClaimOutputSchema: z.ZodObject<{
799
872
  commentUrl: z.ZodString;
800
873
  issueUrl: z.ZodString;
874
+ gistSyncWarning: z.ZodOptional<z.ZodString>;
801
875
  }, z.core.$strip>;
802
876
  export declare const InitOutputSchema: z.ZodObject<{
803
877
  username: z.ZodString;
@@ -865,6 +939,7 @@ export declare const MoveOutputSchema: z.ZodObject<{
865
939
  shelved: "shelved";
866
940
  }>;
867
941
  description: z.ZodString;
942
+ gistSyncWarning: z.ZodOptional<z.ZodString>;
868
943
  }, z.core.$strip>;
869
944
  export declare const PRTemplateOutputSchema: z.ZodObject<{
870
945
  template: z.ZodNullable<z.ZodString>;
@@ -899,6 +974,7 @@ export declare const RepoVetOutputSchema: z.ZodObject<{
899
974
  lastCommitISO: z.ZodNullable<z.ZodString>;
900
975
  contributorsLast90d: z.ZodNumber;
901
976
  lastReleaseISO: z.ZodNullable<z.ZodString>;
977
+ releasesIncomplete: z.ZodOptional<z.ZodBoolean>;
902
978
  }, z.core.$strip>;
903
979
  communityHealth: z.ZodObject<{
904
980
  contributing: z.ZodBoolean;
@@ -1146,6 +1222,9 @@ export interface SearchOutput {
1146
1222
  excludedRepos: string[];
1147
1223
  /** Repos with known anti-AI contribution policies, filtered from search results (#108). */
1148
1224
  aiPolicyBlocklist: string[];
1225
+ /** Candidates dropped because the authenticated user already has an open PR
1226
+ * linked to the issue (#1354). Surfaced as a count so the filter is visible. */
1227
+ hiddenOwnPRCount: number;
1149
1228
  /** Present when rate limits affected the search — either low pre-flight quota or mid-search rate limit hits (#100). */
1150
1229
  rateLimitWarning?: string;
1151
1230
  }
@@ -1398,6 +1477,8 @@ export interface PostOutput {
1398
1477
  export interface ClaimOutput {
1399
1478
  commentUrl: string;
1400
1479
  issueUrl: string;
1480
+ /** Set when the post-mutation Gist checkpoint failed; the local mutation succeeded (#1370). */
1481
+ gistSyncWarning?: string;
1401
1482
  }
1402
1483
  /** Info about a local git clone (#84) */
1403
1484
  export interface LocalRepoInfo {