@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.
- package/dist/cli-registry.d.ts +7 -0
- package/dist/cli-registry.js +58 -5
- package/dist/cli.bundle.cjs +165 -112
- package/dist/cli.js +11 -3
- package/dist/commands/comments.js +31 -15
- package/dist/commands/compliance-score.js +12 -4
- package/dist/commands/daily-render.d.ts +2 -1
- package/dist/commands/daily-render.js +8 -2
- package/dist/commands/daily.d.ts +3 -1
- package/dist/commands/daily.js +54 -4
- package/dist/commands/dashboard-data.d.ts +17 -0
- package/dist/commands/dashboard-data.js +62 -4
- package/dist/commands/dashboard-server.js +100 -26
- package/dist/commands/dismiss.d.ts +4 -0
- package/dist/commands/dismiss.js +4 -4
- package/dist/commands/guidelines.d.ts +19 -0
- package/dist/commands/guidelines.js +23 -4
- package/dist/commands/index.d.ts +5 -1
- package/dist/commands/index.js +4 -0
- package/dist/commands/list-move-tier.d.ts +11 -3
- package/dist/commands/list-move-tier.js +18 -7
- package/dist/commands/move.d.ts +2 -0
- package/dist/commands/move.js +12 -8
- package/dist/commands/repo-vet.js +30 -8
- package/dist/commands/search.js +17 -3
- package/dist/commands/shelve.d.ts +4 -0
- package/dist/commands/shelve.js +4 -4
- package/dist/commands/verify-issue.d.ts +20 -0
- package/dist/commands/verify-issue.js +32 -0
- package/dist/core/daily-logic.js +65 -52
- package/dist/core/gist-state-store.js +42 -7
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.js +3 -1
- package/dist/core/issue-conversation.js +15 -2
- package/dist/core/issue-verification.d.ts +91 -0
- package/dist/core/issue-verification.js +270 -0
- package/dist/core/paths.d.ts +12 -0
- package/dist/core/paths.js +16 -0
- package/dist/core/pr-attention.d.ts +52 -0
- package/dist/core/pr-attention.js +76 -0
- package/dist/core/pr-comments-fetcher.d.ts +10 -2
- package/dist/core/pr-comments-fetcher.js +22 -4
- package/dist/core/state-persistence.d.ts +31 -9
- package/dist/core/state-persistence.js +51 -16
- package/dist/core/state.d.ts +18 -1
- package/dist/core/state.js +35 -3
- package/dist/core/types.d.ts +7 -0
- package/dist/core/untrusted-content.d.ts +24 -3
- package/dist/core/untrusted-content.js +31 -3
- package/dist/formatters/json.d.ts +83 -2
- package/dist/formatters/json.js +55 -1
- 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
|
|
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
|
-
|
|
359
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|
package/dist/core/state.d.ts
CHANGED
|
@@ -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<
|
|
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.
|
package/dist/core/state.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
package/dist/core/types.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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.
|
|
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 {
|