@oss-autopilot/core 3.9.0 → 3.11.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 +29 -5
- package/dist/cli.bundle.cjs +114 -114
- 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.js +47 -2
- package/dist/commands/dashboard-data.d.ts +17 -0
- package/dist/commands/dashboard-data.js +49 -0
- package/dist/commands/dashboard-server.js +93 -24
- 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 +3 -1
- package/dist/commands/index.js +2 -0
- 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 +20 -2
- package/dist/commands/shelve.d.ts +4 -0
- package/dist/commands/shelve.js +4 -4
- package/dist/core/gist-state-store.js +42 -7
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/issue-conversation.js +15 -2
- package/dist/core/paths.d.ts +12 -0
- package/dist/core/paths.js +16 -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/untrusted-content.d.ts +24 -3
- package/dist/core/untrusted-content.js +31 -3
- package/dist/formatters/json.d.ts +15 -1
- package/dist/formatters/json.js +20 -0
- package/package.json +7 -7
|
@@ -45,16 +45,41 @@ export declare function migrateV3ToV4(rawState: Record<string, unknown>): Record
|
|
|
45
45
|
* Leverages Zod schema defaults to produce a complete state.
|
|
46
46
|
*/
|
|
47
47
|
export declare function createFreshState(): AgentState;
|
|
48
|
+
/**
|
|
49
|
+
* Details about a recovery that happened during {@link loadState} — set when
|
|
50
|
+
* the main state file was unreadable (JSON parse error) or structurally
|
|
51
|
+
* invalid (Zod rejection) and the loader fell back to a backup or fresh state.
|
|
52
|
+
* Surfaced through StateManager.getLoadRecovery() so commands can include the
|
|
53
|
+
* event in structured output instead of only stderr warn() lines (#1371).
|
|
54
|
+
*/
|
|
55
|
+
export interface LoadRecoveryInfo {
|
|
56
|
+
/** Where the loaded state came from after the original file was rejected. */
|
|
57
|
+
source: 'backup' | 'fresh';
|
|
58
|
+
/** Path of the preserved `.rejected-<ts>` copy, or null if preservation failed. */
|
|
59
|
+
rejectedPath: string | null;
|
|
60
|
+
/** Short summary of why the original state file was rejected. */
|
|
61
|
+
reason: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Result of {@link loadState}: the loaded state, the file's mtime for change
|
|
65
|
+
* detection, and (only when the main file was rejected) recovery details.
|
|
66
|
+
*/
|
|
67
|
+
export interface LoadStateResult {
|
|
68
|
+
state: AgentState;
|
|
69
|
+
mtimeMs: number;
|
|
70
|
+
recovery?: LoadRecoveryInfo;
|
|
71
|
+
}
|
|
48
72
|
/**
|
|
49
73
|
* Load state from file, or create initial state if none exists.
|
|
50
74
|
* If the main state file is corrupted, attempts to restore from the most recent backup.
|
|
51
75
|
* Performs migration from legacy ./data/ location if needed.
|
|
52
|
-
* @returns Object with the loaded state
|
|
76
|
+
* @returns Object with the loaded state, the file's mtime (for change detection),
|
|
77
|
+
* and recovery details when the main file was rejected.
|
|
78
|
+
* @throws Error when the state file exists but cannot be read due to a
|
|
79
|
+
* permission error (EACCES/EPERM) — that is an environment problem, not
|
|
80
|
+
* corruption, so restore-or-fresh would mask it and risk clobbering data.
|
|
53
81
|
*/
|
|
54
|
-
export declare function loadState():
|
|
55
|
-
state: AgentState;
|
|
56
|
-
mtimeMs: number;
|
|
57
|
-
};
|
|
82
|
+
export declare function loadState(): LoadStateResult;
|
|
58
83
|
/**
|
|
59
84
|
* Persist state to disk, creating a timestamped backup of the previous
|
|
60
85
|
* state file first. Retains at most 10 backup files.
|
|
@@ -79,7 +104,4 @@ export declare function saveState(state: Readonly<AgentState>, expectedMtimeMs?:
|
|
|
79
104
|
* Uses mtime comparison (single statSync call) to avoid unnecessary JSON parsing.
|
|
80
105
|
* @returns The new state and mtime if reloaded, or null if no change detected.
|
|
81
106
|
*/
|
|
82
|
-
export declare function reloadStateIfChanged(lastLoadedMtimeMs: number):
|
|
83
|
-
state: AgentState;
|
|
84
|
-
mtimeMs: number;
|
|
85
|
-
} | null;
|
|
107
|
+
export declare function reloadStateIfChanged(lastLoadedMtimeMs: number): LoadStateResult | null;
|
|
@@ -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.
|
|
@@ -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
|
+
}
|
|
@@ -50,7 +50,7 @@ export interface CompactRepoGroup {
|
|
|
50
50
|
* See `DailyWarning` and issue #1042 for the rationale — keeping this a
|
|
51
51
|
* fixed union so downstream consumers can switch on it without drift.
|
|
52
52
|
*/
|
|
53
|
-
export type DailyWarningPhase = 'fetch' | 'repo-scores' | 'analytics' | 'scout-sync' | 'partition' | 'dismiss-filter' | 'gist-checkpoint' | 'gist-staleness';
|
|
53
|
+
export type DailyWarningPhase = 'fetch' | 'repo-scores' | 'analytics' | 'scout-sync' | 'partition' | 'dismiss-filter' | 'gist-checkpoint' | 'gist-staleness' | 'state-load';
|
|
54
54
|
/**
|
|
55
55
|
* A single non-fatal failure surfaced from the `daily` pipeline. Unlike
|
|
56
56
|
* `PRCheckFailure` (which is scoped to per-PR fetch errors), this covers
|
|
@@ -176,6 +176,7 @@ export declare const StatusOutputSchema: z.ZodObject<{
|
|
|
176
176
|
"dismiss-filter": "dismiss-filter";
|
|
177
177
|
"gist-checkpoint": "gist-checkpoint";
|
|
178
178
|
"gist-staleness": "gist-staleness";
|
|
179
|
+
"state-load": "state-load";
|
|
179
180
|
}>;
|
|
180
181
|
operation: z.ZodString;
|
|
181
182
|
message: z.ZodString;
|
|
@@ -378,6 +379,7 @@ export declare const DailyOutputSchema: z.ZodObject<{
|
|
|
378
379
|
"dismiss-filter": "dismiss-filter";
|
|
379
380
|
"gist-checkpoint": "gist-checkpoint";
|
|
380
381
|
"gist-staleness": "gist-staleness";
|
|
382
|
+
"state-load": "state-load";
|
|
381
383
|
}>;
|
|
382
384
|
operation: z.ZodString;
|
|
383
385
|
message: z.ZodString;
|
|
@@ -518,6 +520,7 @@ export declare const CompactDailyOutputSchema: z.ZodObject<{
|
|
|
518
520
|
"dismiss-filter": "dismiss-filter";
|
|
519
521
|
"gist-checkpoint": "gist-checkpoint";
|
|
520
522
|
"gist-staleness": "gist-staleness";
|
|
523
|
+
"state-load": "state-load";
|
|
521
524
|
}>;
|
|
522
525
|
operation: z.ZodString;
|
|
523
526
|
message: z.ZodString;
|
|
@@ -620,6 +623,8 @@ export declare const SearchOutputSchema: z.ZodObject<{
|
|
|
620
623
|
updatedAt: z.ZodOptional<z.ZodString>;
|
|
621
624
|
isStalled: z.ZodBoolean;
|
|
622
625
|
}, z.core.$strip>>;
|
|
626
|
+
boostScore: z.ZodOptional<z.ZodNumber>;
|
|
627
|
+
boostReasons: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
623
628
|
}, z.core.$strip>>;
|
|
624
629
|
excludedRepos: z.ZodArray<z.ZodString>;
|
|
625
630
|
aiPolicyBlocklist: z.ZodArray<z.ZodString>;
|
|
@@ -676,6 +681,8 @@ export declare const FeaturesOutputSchema: z.ZodObject<{
|
|
|
676
681
|
updatedAt: z.ZodOptional<z.ZodString>;
|
|
677
682
|
isStalled: z.ZodBoolean;
|
|
678
683
|
}, z.core.$strip>>;
|
|
684
|
+
boostScore: z.ZodOptional<z.ZodNumber>;
|
|
685
|
+
boostReasons: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
679
686
|
horizon: z.ZodEnum<{
|
|
680
687
|
"quick-win": "quick-win";
|
|
681
688
|
"bigger-bet": "bigger-bet";
|
|
@@ -731,6 +738,8 @@ export declare const FeaturesOutputSchema: z.ZodObject<{
|
|
|
731
738
|
updatedAt: z.ZodOptional<z.ZodString>;
|
|
732
739
|
isStalled: z.ZodBoolean;
|
|
733
740
|
}, z.core.$strip>>;
|
|
741
|
+
boostScore: z.ZodOptional<z.ZodNumber>;
|
|
742
|
+
boostReasons: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
734
743
|
horizon: z.ZodEnum<{
|
|
735
744
|
"quick-win": "quick-win";
|
|
736
745
|
"bigger-bet": "bigger-bet";
|
|
@@ -792,6 +801,7 @@ export declare const PostOutputSchema: z.ZodObject<{
|
|
|
792
801
|
export declare const ClaimOutputSchema: z.ZodObject<{
|
|
793
802
|
commentUrl: z.ZodString;
|
|
794
803
|
issueUrl: z.ZodString;
|
|
804
|
+
gistSyncWarning: z.ZodOptional<z.ZodString>;
|
|
795
805
|
}, z.core.$strip>;
|
|
796
806
|
export declare const InitOutputSchema: z.ZodObject<{
|
|
797
807
|
username: z.ZodString;
|
|
@@ -859,6 +869,7 @@ export declare const MoveOutputSchema: z.ZodObject<{
|
|
|
859
869
|
shelved: "shelved";
|
|
860
870
|
}>;
|
|
861
871
|
description: z.ZodString;
|
|
872
|
+
gistSyncWarning: z.ZodOptional<z.ZodString>;
|
|
862
873
|
}, z.core.$strip>;
|
|
863
874
|
export declare const PRTemplateOutputSchema: z.ZodObject<{
|
|
864
875
|
template: z.ZodNullable<z.ZodString>;
|
|
@@ -893,6 +904,7 @@ export declare const RepoVetOutputSchema: z.ZodObject<{
|
|
|
893
904
|
lastCommitISO: z.ZodNullable<z.ZodString>;
|
|
894
905
|
contributorsLast90d: z.ZodNumber;
|
|
895
906
|
lastReleaseISO: z.ZodNullable<z.ZodString>;
|
|
907
|
+
releasesIncomplete: z.ZodOptional<z.ZodBoolean>;
|
|
896
908
|
}, z.core.$strip>;
|
|
897
909
|
communityHealth: z.ZodObject<{
|
|
898
910
|
contributing: z.ZodBoolean;
|
|
@@ -1392,6 +1404,8 @@ export interface PostOutput {
|
|
|
1392
1404
|
export interface ClaimOutput {
|
|
1393
1405
|
commentUrl: string;
|
|
1394
1406
|
issueUrl: string;
|
|
1407
|
+
/** Set when the post-mutation Gist checkpoint failed; the local mutation succeeded (#1370). */
|
|
1408
|
+
gistSyncWarning?: string;
|
|
1395
1409
|
}
|
|
1396
1410
|
/** Info about a local git clone (#84) */
|
|
1397
1411
|
export interface LocalRepoInfo {
|
package/dist/formatters/json.js
CHANGED
|
@@ -87,6 +87,7 @@ const DailyWarningPhaseSchema = z.enum([
|
|
|
87
87
|
'dismiss-filter',
|
|
88
88
|
'gist-checkpoint',
|
|
89
89
|
'gist-staleness',
|
|
90
|
+
'state-load',
|
|
90
91
|
]);
|
|
91
92
|
const DailyWarningSchema = z.object({
|
|
92
93
|
phase: DailyWarningPhaseSchema,
|
|
@@ -335,6 +336,14 @@ const SearchCandidateSchema = z.object({
|
|
|
335
336
|
})
|
|
336
337
|
.optional(),
|
|
337
338
|
linkedPR: CandidateLinkedPRSchema.optional(),
|
|
339
|
+
/**
|
|
340
|
+
* Personalization sort-tier signal threaded up from oss-scout (#1244).
|
|
341
|
+
* Present only when local strategy data biased the search and this
|
|
342
|
+
* candidate matched. `boostReasons` is the human-readable explanation
|
|
343
|
+
* (e.g. `"repo affinity: vercel/next.js"`, `"language match: TypeScript"`).
|
|
344
|
+
*/
|
|
345
|
+
boostScore: z.number().optional(),
|
|
346
|
+
boostReasons: z.array(z.string()).optional(),
|
|
338
347
|
});
|
|
339
348
|
export const SearchOutputSchema = z.object({
|
|
340
349
|
candidates: z.array(SearchCandidateSchema),
|
|
@@ -409,6 +418,8 @@ export const PostOutputSchema = z.object({
|
|
|
409
418
|
export const ClaimOutputSchema = z.object({
|
|
410
419
|
commentUrl: z.string(),
|
|
411
420
|
issueUrl: z.string(),
|
|
421
|
+
// Post-mutation Gist checkpoint failure (#1370). Optional: absent on clean runs.
|
|
422
|
+
gistSyncWarning: z.string().optional(),
|
|
412
423
|
});
|
|
413
424
|
export const InitOutputSchema = z.object({
|
|
414
425
|
username: z.string(),
|
|
@@ -484,6 +495,8 @@ export const MoveOutputSchema = z.object({
|
|
|
484
495
|
url: z.string(),
|
|
485
496
|
target: z.enum(['attention', 'waiting', 'shelved', 'auto']),
|
|
486
497
|
description: z.string(),
|
|
498
|
+
// Post-mutation Gist checkpoint failure (#1370). Optional: absent on clean runs.
|
|
499
|
+
gistSyncWarning: z.string().optional(),
|
|
487
500
|
});
|
|
488
501
|
export const PRTemplateOutputSchema = z.object({
|
|
489
502
|
template: z.string().nullable(),
|
|
@@ -527,6 +540,13 @@ export const RepoVetOutputSchema = z.object({
|
|
|
527
540
|
lastCommitISO: z.string().nullable(),
|
|
528
541
|
contributorsLast90d: z.number().int().nonnegative(),
|
|
529
542
|
lastReleaseISO: z.string().nullable(),
|
|
543
|
+
/**
|
|
544
|
+
* True when the release listing failed for a non-404 reason (5xx,
|
|
545
|
+
* network), so a `null` lastReleaseISO means "could not determine"
|
|
546
|
+
* rather than "confirmed no releases". The release-list analogue of
|
|
547
|
+
* `communityHealth.incomplete` (#1373).
|
|
548
|
+
*/
|
|
549
|
+
releasesIncomplete: z.boolean().optional(),
|
|
530
550
|
}),
|
|
531
551
|
communityHealth: z.object({
|
|
532
552
|
contributing: z.boolean(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oss-autopilot/core",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.11.0",
|
|
4
4
|
"description": "CLI and core library for managing open source contributions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -54,18 +54,18 @@
|
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@octokit/plugin-throttling": "^11.0.3",
|
|
56
56
|
"@octokit/rest": "^22.0.1",
|
|
57
|
-
"@oss-scout/core": "^0.
|
|
58
|
-
"commander": "^
|
|
57
|
+
"@oss-scout/core": "^0.11.0",
|
|
58
|
+
"commander": "^15.0.0",
|
|
59
59
|
"zod": "^4.4.3"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@types/node": "^25.
|
|
63
|
-
"@vitest/coverage-v8": "^4.1.
|
|
62
|
+
"@types/node": "^25.9.1",
|
|
63
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
64
64
|
"esbuild": "^0.28.0",
|
|
65
|
-
"tsx": "^4.
|
|
65
|
+
"tsx": "^4.22.4",
|
|
66
66
|
"typedoc": "^0.28.19",
|
|
67
67
|
"typescript": "^5.9.3",
|
|
68
|
-
"vitest": "^4.1.
|
|
68
|
+
"vitest": "^4.1.8"
|
|
69
69
|
},
|
|
70
70
|
"scripts": {
|
|
71
71
|
"build": "tsc",
|