@oss-autopilot/core 3.1.0 → 3.3.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.js +113 -3
- package/dist/cli.bundle.cjs +96 -92
- package/dist/commands/check-integration.js +8 -8
- package/dist/commands/comments.js +3 -0
- package/dist/commands/config.js +14 -7
- package/dist/commands/daily-render.js +10 -5
- package/dist/commands/daily.js +6 -1
- package/dist/commands/dashboard-lifecycle.js +1 -1
- package/dist/commands/dashboard-process.js +4 -4
- package/dist/commands/dashboard-server.js +7 -6
- package/dist/commands/dashboard.js +2 -2
- package/dist/commands/detect-formatters.js +3 -3
- package/dist/commands/doctor.js +5 -5
- package/dist/commands/guidelines.d.ts +67 -0
- package/dist/commands/guidelines.js +159 -0
- package/dist/commands/index.d.ts +9 -0
- package/dist/commands/index.js +9 -0
- package/dist/commands/list-move-tier.js +5 -5
- package/dist/commands/local-repos.js +9 -9
- package/dist/commands/parse-list.js +10 -10
- package/dist/commands/scout-bridge.js +2 -2
- package/dist/commands/setup.js +24 -13
- package/dist/commands/skip-add.js +6 -3
- package/dist/commands/skip-file-parser.js +3 -3
- package/dist/commands/startup.js +11 -8
- package/dist/commands/state-cmd.js +1 -1
- package/dist/commands/status.js +7 -0
- package/dist/commands/validation.js +3 -3
- package/dist/commands/vet-list.js +12 -8
- package/dist/commands/vet.js +1 -2
- package/dist/core/__fixtures__/prompt-injection-payloads.d.ts +22 -0
- package/dist/core/__fixtures__/prompt-injection-payloads.js +109 -0
- package/dist/core/anti-llm-policy.js +5 -5
- package/dist/core/auth.js +5 -5
- package/dist/core/daily-logic.js +8 -4
- package/dist/core/dates.js +3 -3
- package/dist/core/errors.d.ts +29 -0
- package/dist/core/errors.js +63 -0
- package/dist/core/formatter-detection.js +9 -9
- package/dist/core/gist-state-store.d.ts +19 -3
- package/dist/core/gist-state-store.js +81 -15
- package/dist/core/guidelines-store.d.ts +74 -0
- package/dist/core/guidelines-store.js +130 -0
- package/dist/core/http-cache.js +6 -6
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/issue-conversation.js +3 -1
- package/dist/core/paths.js +4 -4
- package/dist/core/pr-comments-fetcher.d.ts +67 -0
- package/dist/core/pr-comments-fetcher.js +125 -0
- package/dist/core/pr-monitor.js +1 -2
- package/dist/core/pr-template.js +1 -1
- package/dist/core/state-persistence.d.ts +6 -0
- package/dist/core/state-persistence.js +27 -9
- package/dist/core/state-schema.d.ts +5 -1
- package/dist/core/state-schema.js +7 -1
- package/dist/core/state.d.ts +60 -0
- package/dist/core/state.js +136 -13
- package/dist/core/types.d.ts +1 -1
- package/dist/core/types.js +2 -2
- package/dist/core/untrusted-content.d.ts +48 -0
- package/dist/core/untrusted-content.js +106 -0
- package/dist/core/urls.js +2 -2
- package/dist/formatters/json.d.ts +53 -3
- package/dist/formatters/json.js +49 -14
- package/package.json +1 -1
package/dist/core/state.d.ts
CHANGED
|
@@ -26,6 +26,24 @@ export declare function maybeCheckpoint(stateManager: StateManager, callerModule
|
|
|
26
26
|
* Retains lightweight CRUD operations for config, issues, shelving, dismissal,
|
|
27
27
|
* and status overrides.
|
|
28
28
|
*/
|
|
29
|
+
/**
|
|
30
|
+
* Surfaced when the in-memory cached state is no longer in sync with the
|
|
31
|
+
* canonical Gist — typically because `refreshFromGist()` failed (network
|
|
32
|
+
* blip, rate limit, expired token) or because the bootstrap fell back to
|
|
33
|
+
* the local cache file (#1193). Commands include this in their `--json`
|
|
34
|
+
* envelope so cron/dashboard consumers can react instead of silently
|
|
35
|
+
* operating on stale data.
|
|
36
|
+
*/
|
|
37
|
+
export interface StalenessInfo {
|
|
38
|
+
/** Why we're operating on cached data. Forward-compatible with future sources. */
|
|
39
|
+
source: 'cache';
|
|
40
|
+
/** Human-readable reason from the underlying error. */
|
|
41
|
+
reason: string;
|
|
42
|
+
/** ISO timestamp of the most recent successful refresh, or null if never. */
|
|
43
|
+
lastSuccessfulRefresh: string | null;
|
|
44
|
+
/** ISO timestamp when this staleness marker was first set. */
|
|
45
|
+
detectedAt: string;
|
|
46
|
+
}
|
|
29
47
|
export declare class StateManager {
|
|
30
48
|
protected state: AgentState;
|
|
31
49
|
protected inMemoryOnly: boolean;
|
|
@@ -34,6 +52,8 @@ export declare class StateManager {
|
|
|
34
52
|
private _batchDirty;
|
|
35
53
|
protected gistStore: GistStateStore | null;
|
|
36
54
|
protected gistDegraded: boolean;
|
|
55
|
+
private staleness;
|
|
56
|
+
private lastSuccessfulRefreshAt;
|
|
37
57
|
/**
|
|
38
58
|
* Create a new StateManager instance.
|
|
39
59
|
* @param inMemoryOnly - When true, state is held only in memory and never read from or
|
|
@@ -105,6 +125,29 @@ export declare class StateManager {
|
|
|
105
125
|
isGistMode(): boolean;
|
|
106
126
|
/** Whether the Gist is in degraded mode (using local cache fallback). */
|
|
107
127
|
isGistDegraded(): boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Whether per-repo guidelines (#867) are available. True iff the Gist store
|
|
130
|
+
* is initialized — in local-only mode, guidelines are unavailable and
|
|
131
|
+
* write operations would throw {@link GuidelinesNotAvailableError}.
|
|
132
|
+
*/
|
|
133
|
+
isGuidelinesAvailable(): boolean;
|
|
134
|
+
/**
|
|
135
|
+
* Read the per-repo guidelines for `repo` (#867). Returns null when in
|
|
136
|
+
* local mode, when no file exists, or when the file is empty (tombstoned).
|
|
137
|
+
*/
|
|
138
|
+
getGuidelines(repo: string): string | null;
|
|
139
|
+
/**
|
|
140
|
+
* Persist per-repo guidelines for `repo`. Throws when not in Gist mode or
|
|
141
|
+
* when content exceeds the byte budget.
|
|
142
|
+
*/
|
|
143
|
+
setGuidelines(repo: string, content: string): void;
|
|
144
|
+
/**
|
|
145
|
+
* Tombstone the guidelines file for `repo` so subsequent reads return null.
|
|
146
|
+
* Throws when not in Gist mode.
|
|
147
|
+
*/
|
|
148
|
+
deleteGuidelines(repo: string): void;
|
|
149
|
+
/** List repos with non-empty guidelines stored in the Gist. */
|
|
150
|
+
listGuidelinesRepos(): string[];
|
|
108
151
|
/**
|
|
109
152
|
* Get the current state as a read-only snapshot.
|
|
110
153
|
*/
|
|
@@ -119,6 +162,13 @@ export declare class StateManager {
|
|
|
119
162
|
* Throttled to once per 30 seconds by GistStateStore. Returns true if state was refreshed.
|
|
120
163
|
*/
|
|
121
164
|
refreshFromGist(): Promise<boolean>;
|
|
165
|
+
/**
|
|
166
|
+
* Returns a staleness marker when the in-memory state diverged from the
|
|
167
|
+
* canonical Gist (refresh failure or degraded bootstrap), or `null` when
|
|
168
|
+
* state is current. Commands surface this via their `--json` warnings
|
|
169
|
+
* envelope (#1193).
|
|
170
|
+
*/
|
|
171
|
+
getStateStaleness(): StalenessInfo | null;
|
|
122
172
|
/**
|
|
123
173
|
* Store the latest daily digest and update the digest timestamp.
|
|
124
174
|
* @param digest - The daily digest to store
|
|
@@ -176,6 +226,16 @@ export declare class StateManager {
|
|
|
176
226
|
};
|
|
177
227
|
/** Returns the most recent close date, used as a watermark for incremental fetching. */
|
|
178
228
|
getClosedPRWatermark(): string | undefined;
|
|
229
|
+
/**
|
|
230
|
+
* Stamp `commentsFetchedAt` on the merged or closed PR matching `url` (#867).
|
|
231
|
+
* No-op when no PR with that URL is stored.
|
|
232
|
+
*/
|
|
233
|
+
markPRCommentsFetched(url: string, fetchedAt: string): void;
|
|
234
|
+
/**
|
|
235
|
+
* Stamp `learningsExtractedAt` on the merged or closed PR matching `url` (#867).
|
|
236
|
+
* No-op when no PR with that URL is stored.
|
|
237
|
+
*/
|
|
238
|
+
markPRLearningsExtracted(url: string, extractedAt: string): void;
|
|
179
239
|
/**
|
|
180
240
|
* Merge partial config updates into the current configuration.
|
|
181
241
|
* @param config - Partial config object to merge
|
package/dist/core/state.js
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
* Thin coordinator that delegates persistence to state-persistence.ts
|
|
4
4
|
* and scoring logic to repo-score-manager.ts.
|
|
5
5
|
*/
|
|
6
|
-
import * as fs from 'fs';
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
7
|
import { AgentStateSchema } from './state-schema.js';
|
|
8
8
|
import { loadState, saveState, reloadStateIfChanged, createFreshState, atomicWriteFileSync, } from './state-persistence.js';
|
|
9
9
|
import * as repoScoring from './repo-score-manager.js';
|
|
10
10
|
import { debug, warn } from './logger.js';
|
|
11
|
-
import { errorMessage, ConfigurationError, ConcurrencyError } from './errors.js';
|
|
11
|
+
import { errorMessage, ConfigurationError, ConcurrencyError, isTransientNetworkError } from './errors.js';
|
|
12
12
|
import { GistStateStore } from './gist-state-store.js';
|
|
13
|
+
import * as guidelinesStoreModule from './guidelines-store.js';
|
|
13
14
|
import { getStatePath, getStateCachePath } from './paths.js';
|
|
14
15
|
import { parseGitHubUrl } from './urls.js';
|
|
15
16
|
export { acquireLock, releaseLock, atomicWriteFileSync } from './state-persistence.js';
|
|
@@ -52,13 +53,6 @@ export async function maybeCheckpoint(stateManager, callerModule) {
|
|
|
52
53
|
warn(callerModule, `Gist checkpoint failed (local mutation succeeded, will retry on next push): ${errorMessage(err)}`);
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
|
-
/**
|
|
56
|
-
* Singleton manager for persistent agent state stored in ~/.oss-autopilot/state.json.
|
|
57
|
-
*
|
|
58
|
-
* Delegates file I/O to state-persistence.ts and scoring logic to repo-score-manager.ts.
|
|
59
|
-
* Retains lightweight CRUD operations for config, issues, shelving, dismissal,
|
|
60
|
-
* and status overrides.
|
|
61
|
-
*/
|
|
62
56
|
export class StateManager {
|
|
63
57
|
state;
|
|
64
58
|
inMemoryOnly;
|
|
@@ -67,6 +61,8 @@ export class StateManager {
|
|
|
67
61
|
_batchDirty = false;
|
|
68
62
|
gistStore = null;
|
|
69
63
|
gistDegraded = false;
|
|
64
|
+
staleness = null;
|
|
65
|
+
lastSuccessfulRefreshAt = null;
|
|
70
66
|
/**
|
|
71
67
|
* Create a new StateManager instance.
|
|
72
68
|
* @param inMemoryOnly - When true, state is held only in memory and never read from or
|
|
@@ -130,6 +126,16 @@ export class StateManager {
|
|
|
130
126
|
manager.gistStore = gistStore;
|
|
131
127
|
manager.gistDegraded = result.degraded ?? false;
|
|
132
128
|
manager.inMemoryOnly = false; // re-enable persistence
|
|
129
|
+
// Seed the staleness marker if bootstrap fell back to the local cache —
|
|
130
|
+
// a `daily` running on a cron right after this start needs to know.
|
|
131
|
+
if (result.degraded) {
|
|
132
|
+
manager.staleness = {
|
|
133
|
+
source: 'cache',
|
|
134
|
+
reason: 'initial Gist bootstrap fell back to local cache',
|
|
135
|
+
lastSuccessfulRefresh: null,
|
|
136
|
+
detectedAt: new Date().toISOString(),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
133
139
|
return manager;
|
|
134
140
|
}
|
|
135
141
|
/**
|
|
@@ -274,6 +280,41 @@ export class StateManager {
|
|
|
274
280
|
isGistDegraded() {
|
|
275
281
|
return this.gistDegraded;
|
|
276
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Whether per-repo guidelines (#867) are available. True iff the Gist store
|
|
285
|
+
* is initialized — in local-only mode, guidelines are unavailable and
|
|
286
|
+
* write operations would throw {@link GuidelinesNotAvailableError}.
|
|
287
|
+
*/
|
|
288
|
+
isGuidelinesAvailable() {
|
|
289
|
+
return this.gistStore !== null;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Read the per-repo guidelines for `repo` (#867). Returns null when in
|
|
293
|
+
* local mode, when no file exists, or when the file is empty (tombstoned).
|
|
294
|
+
*/
|
|
295
|
+
getGuidelines(repo) {
|
|
296
|
+
return guidelinesStoreModule.getGuidelines(this.gistStore, repo);
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Persist per-repo guidelines for `repo`. Throws when not in Gist mode or
|
|
300
|
+
* when content exceeds the byte budget.
|
|
301
|
+
*/
|
|
302
|
+
setGuidelines(repo, content) {
|
|
303
|
+
guidelinesStoreModule.setGuidelines(this.gistStore, repo, content);
|
|
304
|
+
this.autoSave();
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Tombstone the guidelines file for `repo` so subsequent reads return null.
|
|
308
|
+
* Throws when not in Gist mode.
|
|
309
|
+
*/
|
|
310
|
+
deleteGuidelines(repo) {
|
|
311
|
+
guidelinesStoreModule.deleteGuidelines(this.gistStore, repo);
|
|
312
|
+
this.autoSave();
|
|
313
|
+
}
|
|
314
|
+
/** List repos with non-empty guidelines stored in the Gist. */
|
|
315
|
+
listGuidelinesRepos() {
|
|
316
|
+
return guidelinesStoreModule.listGuidelinesRepos(this.gistStore);
|
|
317
|
+
}
|
|
277
318
|
/**
|
|
278
319
|
* Get the current state as a read-only snapshot.
|
|
279
320
|
*/
|
|
@@ -309,6 +350,15 @@ export class StateManager {
|
|
|
309
350
|
const raw = this.gistStore.cachedFiles.get('state.json');
|
|
310
351
|
if (!raw) {
|
|
311
352
|
warn(MODULE, 'Gist refreshed but state.json missing from cache');
|
|
353
|
+
// HTTP fetch succeeded but the Gist body is missing state.json — we
|
|
354
|
+
// still have stale in-memory data, so surface a marker rather than
|
|
355
|
+
// silently returning false (#1193 review).
|
|
356
|
+
this.staleness = {
|
|
357
|
+
source: 'cache',
|
|
358
|
+
reason: 'Gist refresh returned no state.json file',
|
|
359
|
+
lastSuccessfulRefresh: this.lastSuccessfulRefreshAt,
|
|
360
|
+
detectedAt: new Date().toISOString(),
|
|
361
|
+
};
|
|
312
362
|
return false;
|
|
313
363
|
}
|
|
314
364
|
try {
|
|
@@ -317,11 +367,41 @@ export class StateManager {
|
|
|
317
367
|
}
|
|
318
368
|
catch (err) {
|
|
319
369
|
warn(MODULE, `Failed to parse refreshed Gist state: ${errorMessage(err)}`);
|
|
370
|
+
// Same reasoning as the missing-file branch: parse failure leaves us
|
|
371
|
+
// on stale in-memory state, so flag it.
|
|
372
|
+
this.staleness = {
|
|
373
|
+
source: 'cache',
|
|
374
|
+
reason: `Gist refresh succeeded but payload was invalid: ${errorMessage(err)}`,
|
|
375
|
+
lastSuccessfulRefresh: this.lastSuccessfulRefreshAt,
|
|
376
|
+
detectedAt: new Date().toISOString(),
|
|
377
|
+
};
|
|
320
378
|
return false;
|
|
321
379
|
}
|
|
380
|
+
// Successful refresh clears any prior staleness (#1193).
|
|
381
|
+
this.lastSuccessfulRefreshAt = new Date().toISOString();
|
|
382
|
+
this.staleness = null;
|
|
383
|
+
}
|
|
384
|
+
else if (this.gistStore.lastRefreshError) {
|
|
385
|
+
// Distinguish "fetch failed" (set marker) from "throttled" (preserve
|
|
386
|
+
// any existing marker, set nothing new).
|
|
387
|
+
this.staleness = {
|
|
388
|
+
source: 'cache',
|
|
389
|
+
reason: errorMessage(this.gistStore.lastRefreshError),
|
|
390
|
+
lastSuccessfulRefresh: this.lastSuccessfulRefreshAt,
|
|
391
|
+
detectedAt: new Date().toISOString(),
|
|
392
|
+
};
|
|
322
393
|
}
|
|
323
394
|
return refreshed;
|
|
324
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* Returns a staleness marker when the in-memory state diverged from the
|
|
398
|
+
* canonical Gist (refresh failure or degraded bootstrap), or `null` when
|
|
399
|
+
* state is current. Commands surface this via their `--json` warnings
|
|
400
|
+
* envelope (#1193).
|
|
401
|
+
*/
|
|
402
|
+
getStateStaleness() {
|
|
403
|
+
return this.staleness;
|
|
404
|
+
}
|
|
325
405
|
// === Dashboard Data Setters ===
|
|
326
406
|
/**
|
|
327
407
|
* Store the latest daily digest and update the digest timestamp.
|
|
@@ -434,6 +514,40 @@ export class StateManager {
|
|
|
434
514
|
getClosedPRWatermark() {
|
|
435
515
|
return this.state.closedPRs?.[0]?.closedAt || undefined;
|
|
436
516
|
}
|
|
517
|
+
/**
|
|
518
|
+
* Stamp `commentsFetchedAt` on the merged or closed PR matching `url` (#867).
|
|
519
|
+
* No-op when no PR with that URL is stored.
|
|
520
|
+
*/
|
|
521
|
+
markPRCommentsFetched(url, fetchedAt) {
|
|
522
|
+
const merged = this.state.mergedPRs?.find((pr) => pr.url === url);
|
|
523
|
+
if (merged) {
|
|
524
|
+
merged.commentsFetchedAt = fetchedAt;
|
|
525
|
+
this.autoSave();
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const closed = this.state.closedPRs?.find((pr) => pr.url === url);
|
|
529
|
+
if (closed) {
|
|
530
|
+
closed.commentsFetchedAt = fetchedAt;
|
|
531
|
+
this.autoSave();
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Stamp `learningsExtractedAt` on the merged or closed PR matching `url` (#867).
|
|
536
|
+
* No-op when no PR with that URL is stored.
|
|
537
|
+
*/
|
|
538
|
+
markPRLearningsExtracted(url, extractedAt) {
|
|
539
|
+
const merged = this.state.mergedPRs?.find((pr) => pr.url === url);
|
|
540
|
+
if (merged) {
|
|
541
|
+
merged.learningsExtractedAt = extractedAt;
|
|
542
|
+
this.autoSave();
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const closed = this.state.closedPRs?.find((pr) => pr.url === url);
|
|
546
|
+
if (closed) {
|
|
547
|
+
closed.learningsExtractedAt = extractedAt;
|
|
548
|
+
this.autoSave();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
437
551
|
// === Configuration ===
|
|
438
552
|
/**
|
|
439
553
|
* Merge partial config updates into the current configuration.
|
|
@@ -777,11 +891,20 @@ export async function getStateManagerAsync(token) {
|
|
|
777
891
|
})
|
|
778
892
|
.catch((err) => {
|
|
779
893
|
asyncManagerPromise = null;
|
|
780
|
-
// Configuration errors (e.g. GistPermissionError)
|
|
894
|
+
// Configuration errors (e.g. GistPermissionError, GistCorruptError)
|
|
895
|
+
// must surface — falling back to local-only would silently split state
|
|
896
|
+
// across machines (#1202).
|
|
781
897
|
if (err instanceof ConfigurationError)
|
|
782
898
|
throw err;
|
|
783
|
-
|
|
784
|
-
|
|
899
|
+
// Only fall back on actual network/server errors. Other failures
|
|
900
|
+
// (auth, schema, concurrency conflicts) indicate the Gist mode is
|
|
901
|
+
// broken in a way the user needs to address — silently falling back
|
|
902
|
+
// would write subsequent mutations to the local file while the Gist
|
|
903
|
+
// marker stays in config, causing permanent cross-machine divergence.
|
|
904
|
+
if (!isTransientNetworkError(err))
|
|
905
|
+
throw err;
|
|
906
|
+
warn(MODULE, `Gist initialization failed (transient network error), falling back to local-only mode: ${err}`);
|
|
907
|
+
return getStateManager();
|
|
785
908
|
});
|
|
786
909
|
return asyncManagerPromise;
|
|
787
910
|
}
|
|
@@ -810,7 +933,7 @@ export async function ensureGistPersistence(token) {
|
|
|
810
933
|
return;
|
|
811
934
|
let persistence;
|
|
812
935
|
try {
|
|
813
|
-
const raw = fs.readFileSync(getStatePath(), '
|
|
936
|
+
const raw = fs.readFileSync(getStatePath(), 'utf8');
|
|
814
937
|
persistence = JSON.parse(raw)?.config?.persistence;
|
|
815
938
|
}
|
|
816
939
|
catch {
|
package/dist/core/types.d.ts
CHANGED
|
@@ -233,7 +233,7 @@ interface CommentedIssueWithoutResponse extends CommentedIssueBase {
|
|
|
233
233
|
export type CommentedIssue = CommentedIssueWithResponse | CommentedIssueWithoutResponse;
|
|
234
234
|
/** Default configuration applied to new state files. All fields can be overridden via `/setup-oss`. */
|
|
235
235
|
export declare const DEFAULT_CONFIG: AgentConfig;
|
|
236
|
-
/** Initial state written to `~/.oss-autopilot/state.json` on first run. Uses
|
|
236
|
+
/** Initial state written to `~/.oss-autopilot/state.json` on first run. Uses v4 architecture. */
|
|
237
237
|
export declare const INITIAL_STATE: AgentState;
|
|
238
238
|
export declare const PROJECT_CATEGORIES: ("nonprofit" | "devtools" | "infrastructure" | "web-frameworks" | "data-ml" | "education")[];
|
|
239
239
|
export declare const ISSUE_SCOPES: ("advanced" | "beginner" | "intermediate")[];
|
package/dist/core/types.js
CHANGED
|
@@ -12,8 +12,8 @@ export function isBelowMinStars(stargazersCount, minStars) {
|
|
|
12
12
|
// ── Schema-derived constants ─────────────────────────────────────────
|
|
13
13
|
/** Default configuration applied to new state files. All fields can be overridden via `/setup-oss`. */
|
|
14
14
|
export const DEFAULT_CONFIG = AgentConfigSchema.parse({});
|
|
15
|
-
/** Initial state written to `~/.oss-autopilot/state.json` on first run. Uses
|
|
16
|
-
export const INITIAL_STATE = AgentStateSchema.parse({ version:
|
|
15
|
+
/** Initial state written to `~/.oss-autopilot/state.json` on first run. Uses v4 architecture. */
|
|
16
|
+
export const INITIAL_STATE = AgentStateSchema.parse({ version: 4 });
|
|
17
17
|
// ── Const arrays (derived from Zod schemas for runtime iteration) ────
|
|
18
18
|
export const PROJECT_CATEGORIES = ProjectCategorySchema.options;
|
|
19
19
|
export const ISSUE_SCOPES = IssueScopeSchema.options;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrap GitHub-sourced text (PR titles, PR bodies, issue bodies, review
|
|
3
|
+
* comments) in a fenced delimiter so the LLM treats it as data, not
|
|
4
|
+
* instructions (#1192).
|
|
5
|
+
*
|
|
6
|
+
* The contract this module pins, exercised by `untrusted-content.test.ts`
|
|
7
|
+
* and the prompt-injection corpus:
|
|
8
|
+
*
|
|
9
|
+
* 1. Output starts with the open tag and ends with the close tag.
|
|
10
|
+
* 2. The close-tag literal NEVER appears inside the wrapped body — any
|
|
11
|
+
* occurrence in the input is escaped to a sentinel so an attacker
|
|
12
|
+
* cannot close the fence early and inject instructions after it.
|
|
13
|
+
* 3. `extractFromFence(wrapUntrustedContent(x, label))` returns `x`
|
|
14
|
+
* unchanged for any input — the wrapping is lossless.
|
|
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.
|
|
19
|
+
*
|
|
20
|
+
* Non-goals:
|
|
21
|
+
* - This is NOT a content filter. We do not detect or strip prompt-
|
|
22
|
+
* injection payloads; the human-in-the-loop gate on `post`/`claim`
|
|
23
|
+
* remains the primary control.
|
|
24
|
+
*/
|
|
25
|
+
export declare const UNTRUSTED_OPEN_TAG_NAME = "github-content";
|
|
26
|
+
export declare const UNTRUSTED_CLOSE_TAG = "</github-content>";
|
|
27
|
+
export interface UntrustedContentMeta {
|
|
28
|
+
/** GitHub login of the content's author (e.g. PR comment author). */
|
|
29
|
+
author?: string;
|
|
30
|
+
/** GitHub author_association (OWNER, MEMBER, CONTRIBUTOR, NONE, ...). */
|
|
31
|
+
association?: string;
|
|
32
|
+
/** Free-form provenance label (e.g. "pr-body", "review-comment"). */
|
|
33
|
+
source?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Wrap `text` in a `<github-content>` fence labeled with `label` and the
|
|
37
|
+
* optional metadata. Any occurrence of the close-tag literal inside `text`
|
|
38
|
+
* is replaced with a zero-width-joined sentinel that round-trips losslessly
|
|
39
|
+
* via {@link extractFromFence}.
|
|
40
|
+
*/
|
|
41
|
+
export declare function wrapUntrustedContent(text: string, label: string, meta?: UntrustedContentMeta): string;
|
|
42
|
+
/**
|
|
43
|
+
* Reverse of {@link wrapUntrustedContent}. Extracts the original body text
|
|
44
|
+
* (un-escaping any sentinel-encoded close tags). Throws if the input is not
|
|
45
|
+
* a single well-formed fence — callers shouldn't be parsing arbitrary
|
|
46
|
+
* markdown with this; it's for tests + symmetric reasoning only.
|
|
47
|
+
*/
|
|
48
|
+
export declare function extractFromFence(fenced: string): string;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrap GitHub-sourced text (PR titles, PR bodies, issue bodies, review
|
|
3
|
+
* comments) in a fenced delimiter so the LLM treats it as data, not
|
|
4
|
+
* instructions (#1192).
|
|
5
|
+
*
|
|
6
|
+
* The contract this module pins, exercised by `untrusted-content.test.ts`
|
|
7
|
+
* and the prompt-injection corpus:
|
|
8
|
+
*
|
|
9
|
+
* 1. Output starts with the open tag and ends with the close tag.
|
|
10
|
+
* 2. The close-tag literal NEVER appears inside the wrapped body — any
|
|
11
|
+
* occurrence in the input is escaped to a sentinel so an attacker
|
|
12
|
+
* cannot close the fence early and inject instructions after it.
|
|
13
|
+
* 3. `extractFromFence(wrapUntrustedContent(x, label))` returns `x`
|
|
14
|
+
* unchanged for any input — the wrapping is lossless.
|
|
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.
|
|
19
|
+
*
|
|
20
|
+
* Non-goals:
|
|
21
|
+
* - This is NOT a content filter. We do not detect or strip prompt-
|
|
22
|
+
* injection payloads; the human-in-the-loop gate on `post`/`claim`
|
|
23
|
+
* remains the primary control.
|
|
24
|
+
*/
|
|
25
|
+
export const UNTRUSTED_OPEN_TAG_NAME = 'github-content';
|
|
26
|
+
export const UNTRUSTED_CLOSE_TAG = `</${UNTRUSTED_OPEN_TAG_NAME}>`;
|
|
27
|
+
/**
|
|
28
|
+
* Sentinels used to neutralize any literal open- or close-tag substring
|
|
29
|
+
* inside the wrapped body. We use HTML entity escapes (`<` / `>`) so
|
|
30
|
+
* the result is pure ASCII, lints cleanly, and round-trips losslessly. The
|
|
31
|
+
* `&` itself is escaped first so an input containing the literal text
|
|
32
|
+
* `</github-content>` survives the wrap/unwrap round-trip without
|
|
33
|
+
* collision.
|
|
34
|
+
*/
|
|
35
|
+
const AMP_ESCAPE = '&';
|
|
36
|
+
const CLOSE_TAG_ESCAPE = `</${UNTRUSTED_OPEN_TAG_NAME}>`;
|
|
37
|
+
const OPEN_TAG_PATTERN = new RegExp(`<${UNTRUSTED_OPEN_TAG_NAME}\\b`, 'g');
|
|
38
|
+
const OPEN_TAG_ESCAPE = `<${UNTRUSTED_OPEN_TAG_NAME}`;
|
|
39
|
+
function escapeAttr(value) {
|
|
40
|
+
// Newlines/carriage returns can't break out of a quoted attribute (no `"`)
|
|
41
|
+
// but they visually fragment the open tag in the prompt text the LLM
|
|
42
|
+
// reads, so encode them as numeric entities for defense-in-depth.
|
|
43
|
+
return value
|
|
44
|
+
.replace(/&/g, '&')
|
|
45
|
+
.replace(/"/g, '"')
|
|
46
|
+
.replace(/</g, '<')
|
|
47
|
+
.replace(/>/g, '>')
|
|
48
|
+
.replace(/\n/g, ' ')
|
|
49
|
+
.replace(/\r/g, ' ');
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Wrap `text` in a `<github-content>` fence labeled with `label` and the
|
|
53
|
+
* optional metadata. Any occurrence of the close-tag literal inside `text`
|
|
54
|
+
* is replaced with a zero-width-joined sentinel that round-trips losslessly
|
|
55
|
+
* via {@link extractFromFence}.
|
|
56
|
+
*/
|
|
57
|
+
export function wrapUntrustedContent(text, label, meta = {}) {
|
|
58
|
+
// Escape order matters for round-trip correctness:
|
|
59
|
+
// 1. `&` first — so any pre-existing entity in the input gets a literal
|
|
60
|
+
// `&` that survives the unwrap pass below.
|
|
61
|
+
// 2. Close-tag literals — replaced with the entity-escaped form.
|
|
62
|
+
// 3. Open-tag literals (matched only at boundaries via OPEN_TAG_PATTERN
|
|
63
|
+
// so we don't accidentally rewrite the close-tag's `</github-content`).
|
|
64
|
+
const escapedBody = text
|
|
65
|
+
.split('&')
|
|
66
|
+
.join(AMP_ESCAPE)
|
|
67
|
+
.split(UNTRUSTED_CLOSE_TAG)
|
|
68
|
+
.join(CLOSE_TAG_ESCAPE)
|
|
69
|
+
.replace(OPEN_TAG_PATTERN, OPEN_TAG_ESCAPE);
|
|
70
|
+
const attrs = [`label="${escapeAttr(label)}"`];
|
|
71
|
+
if (meta.author !== undefined)
|
|
72
|
+
attrs.push(`author="${escapeAttr(meta.author)}"`);
|
|
73
|
+
if (meta.association !== undefined)
|
|
74
|
+
attrs.push(`association="${escapeAttr(meta.association)}"`);
|
|
75
|
+
if (meta.source !== undefined)
|
|
76
|
+
attrs.push(`source="${escapeAttr(meta.source)}"`);
|
|
77
|
+
return `<${UNTRUSTED_OPEN_TAG_NAME} ${attrs.join(' ')}>${escapedBody}${UNTRUSTED_CLOSE_TAG}`;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Reverse of {@link wrapUntrustedContent}. Extracts the original body text
|
|
81
|
+
* (un-escaping any sentinel-encoded close tags). Throws if the input is not
|
|
82
|
+
* a single well-formed fence — callers shouldn't be parsing arbitrary
|
|
83
|
+
* markdown with this; it's for tests + symmetric reasoning only.
|
|
84
|
+
*/
|
|
85
|
+
export function extractFromFence(fenced) {
|
|
86
|
+
const openMatch = fenced.match(new RegExp(`^<${UNTRUSTED_OPEN_TAG_NAME}\\b[^>]*>`));
|
|
87
|
+
if (!openMatch) {
|
|
88
|
+
throw new Error('extractFromFence: input does not start with a <github-content> open tag');
|
|
89
|
+
}
|
|
90
|
+
if (!fenced.endsWith(UNTRUSTED_CLOSE_TAG)) {
|
|
91
|
+
throw new Error('extractFromFence: input does not end with </github-content>');
|
|
92
|
+
}
|
|
93
|
+
const inner = fenced.slice(openMatch[0].length, fenced.length - UNTRUSTED_CLOSE_TAG.length);
|
|
94
|
+
if (inner.includes(UNTRUSTED_CLOSE_TAG)) {
|
|
95
|
+
throw new Error('extractFromFence: nested </github-content> found in body — fence escaping is broken');
|
|
96
|
+
}
|
|
97
|
+
// Reverse the escapes in opposite order: tag entities first (so an
|
|
98
|
+
// already-`&`-prefixed entity survives), then unescape `&`.
|
|
99
|
+
return inner
|
|
100
|
+
.split(CLOSE_TAG_ESCAPE)
|
|
101
|
+
.join(UNTRUSTED_CLOSE_TAG)
|
|
102
|
+
.split(OPEN_TAG_ESCAPE)
|
|
103
|
+
.join(`<${UNTRUSTED_OPEN_TAG_NAME}`)
|
|
104
|
+
.split(AMP_ESCAPE)
|
|
105
|
+
.join('&');
|
|
106
|
+
}
|
package/dist/core/urls.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* owner/repo characters. Extracted from utils.ts under #1116.
|
|
6
6
|
*/
|
|
7
7
|
// Validation patterns for GitHub owner and repo names
|
|
8
|
-
const OWNER_PATTERN = /^[
|
|
9
|
-
const REPO_PATTERN = /^[
|
|
8
|
+
const OWNER_PATTERN = /^[\w-]+$/;
|
|
9
|
+
const REPO_PATTERN = /^[\w.-]+$/;
|
|
10
10
|
function isValidOwnerRepo(owner, repo) {
|
|
11
11
|
return OWNER_PATTERN.test(owner) && REPO_PATTERN.test(repo);
|
|
12
12
|
}
|
|
@@ -3,10 +3,9 @@
|
|
|
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 { FetchedPR, DailyDigest, AgentState, RepoGroup, CommentedIssue, ShelvedPRRef } from '../core/types.js';
|
|
6
|
+
import type { FetchedPR, DailyDigest, AgentState, RepoGroup, CommentedIssue, ShelvedPRRef, SearchPriority, CapacityAssessment, ActionableIssue, ActionableIssueType, CompactActionableIssue, ActionMenuItem, ActionMenu } from '../core/types.js';
|
|
7
7
|
import type { ContributionStats } from '../core/stats.js';
|
|
8
8
|
import type { PRCheckFailure } from '../core/pr-monitor.js';
|
|
9
|
-
import type { SearchPriority, CapacityAssessment, ActionableIssue, ActionableIssueType, CompactActionableIssue, ActionMenuItem, ActionMenu } from '../core/types.js';
|
|
10
9
|
import type { CIFormatterDiagnosis, FormatterDetectionResult } from '../core/formatter-detection.js';
|
|
11
10
|
export type { CapacityAssessment, ActionableIssue, ActionableIssueType, CompactActionableIssue, ActionMenuItem, ActionMenu, };
|
|
12
11
|
export type ErrorCode = 'AUTH_REQUIRED' | 'RATE_LIMITED' | 'VALIDATION' | 'CONFIGURATION' | 'NETWORK' | 'NOT_FOUND' | 'STATE_CORRUPTED' | 'CONCURRENCY' | 'UNKNOWN';
|
|
@@ -51,18 +50,37 @@ export interface CompactRepoGroup {
|
|
|
51
50
|
* See `DailyWarning` and issue #1042 for the rationale — keeping this a
|
|
52
51
|
* fixed union so downstream consumers can switch on it without drift.
|
|
53
52
|
*/
|
|
54
|
-
export type DailyWarningPhase = 'fetch' | 'repo-scores' | 'analytics' | 'scout-sync' | 'partition' | 'dismiss-filter' | 'gist-checkpoint';
|
|
53
|
+
export type DailyWarningPhase = 'fetch' | 'repo-scores' | 'analytics' | 'scout-sync' | 'partition' | 'dismiss-filter' | 'gist-checkpoint' | 'gist-staleness';
|
|
55
54
|
/**
|
|
56
55
|
* A single non-fatal failure surfaced from the `daily` pipeline. Unlike
|
|
57
56
|
* `PRCheckFailure` (which is scoped to per-PR fetch errors), this covers
|
|
58
57
|
* ancillary fetches that previously demoted to a log-only `warn()` — repo
|
|
59
58
|
* metadata, monthly analytics, scout sync, Gist checkpoint, etc.
|
|
59
|
+
*
|
|
60
|
+
* `timestamp` and `details` are optional structured extensions added in
|
|
61
|
+
* #1193 so staleness warnings can carry `lastSuccessfulRefresh` /
|
|
62
|
+
* `detectedAt` without bespoke schemas. Existing producers don't need to
|
|
63
|
+
* supply them.
|
|
60
64
|
*/
|
|
61
65
|
export interface DailyWarning {
|
|
62
66
|
phase: DailyWarningPhase;
|
|
63
67
|
operation: string;
|
|
64
68
|
message: string;
|
|
69
|
+
timestamp?: string;
|
|
70
|
+
details?: Record<string, unknown>;
|
|
65
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Build a warning entry from a {@link StalenessInfo} marker (#1193).
|
|
74
|
+
* Co-located with `DailyWarning` so every command (`daily`, `status`,
|
|
75
|
+
* `comments`) builds the same shape from the same source.
|
|
76
|
+
*/
|
|
77
|
+
export interface StalenessLike {
|
|
78
|
+
source: string;
|
|
79
|
+
reason: string;
|
|
80
|
+
lastSuccessfulRefresh: string | null;
|
|
81
|
+
detectedAt: string;
|
|
82
|
+
}
|
|
83
|
+
export declare function buildStalenessWarning(info: StalenessLike): DailyWarning;
|
|
66
84
|
export interface DailyOutput {
|
|
67
85
|
digest: DailyDigestCompact;
|
|
68
86
|
capacity: CapacityAssessment;
|
|
@@ -139,6 +157,22 @@ export declare const StatusOutputSchema: z.ZodObject<{
|
|
|
139
157
|
lastRunAt: z.ZodString;
|
|
140
158
|
offline: z.ZodOptional<z.ZodBoolean>;
|
|
141
159
|
lastUpdated: z.ZodOptional<z.ZodString>;
|
|
160
|
+
warnings: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
161
|
+
phase: z.ZodEnum<{
|
|
162
|
+
fetch: "fetch";
|
|
163
|
+
"repo-scores": "repo-scores";
|
|
164
|
+
analytics: "analytics";
|
|
165
|
+
"scout-sync": "scout-sync";
|
|
166
|
+
partition: "partition";
|
|
167
|
+
"dismiss-filter": "dismiss-filter";
|
|
168
|
+
"gist-checkpoint": "gist-checkpoint";
|
|
169
|
+
"gist-staleness": "gist-staleness";
|
|
170
|
+
}>;
|
|
171
|
+
operation: z.ZodString;
|
|
172
|
+
message: z.ZodString;
|
|
173
|
+
timestamp: z.ZodOptional<z.ZodString>;
|
|
174
|
+
details: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
175
|
+
}, z.core.$strip>>>;
|
|
142
176
|
}, z.core.$strip>;
|
|
143
177
|
export type StatusOutput = z.infer<typeof StatusOutputSchema>;
|
|
144
178
|
export declare const DailyOutputSchema: z.ZodObject<{
|
|
@@ -234,9 +268,12 @@ export declare const DailyOutputSchema: z.ZodObject<{
|
|
|
234
268
|
partition: "partition";
|
|
235
269
|
"dismiss-filter": "dismiss-filter";
|
|
236
270
|
"gist-checkpoint": "gist-checkpoint";
|
|
271
|
+
"gist-staleness": "gist-staleness";
|
|
237
272
|
}>;
|
|
238
273
|
operation: z.ZodString;
|
|
239
274
|
message: z.ZodString;
|
|
275
|
+
timestamp: z.ZodOptional<z.ZodString>;
|
|
276
|
+
details: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
240
277
|
}, z.core.$strip>>;
|
|
241
278
|
}, z.core.$strip>;
|
|
242
279
|
export declare const CompactDailyOutputSchema: z.ZodObject<{
|
|
@@ -327,9 +364,12 @@ export declare const CompactDailyOutputSchema: z.ZodObject<{
|
|
|
327
364
|
partition: "partition";
|
|
328
365
|
"dismiss-filter": "dismiss-filter";
|
|
329
366
|
"gist-checkpoint": "gist-checkpoint";
|
|
367
|
+
"gist-staleness": "gist-staleness";
|
|
330
368
|
}>;
|
|
331
369
|
operation: z.ZodString;
|
|
332
370
|
message: z.ZodString;
|
|
371
|
+
timestamp: z.ZodOptional<z.ZodString>;
|
|
372
|
+
details: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
333
373
|
}, z.core.$strip>>;
|
|
334
374
|
}, z.core.$strip>;
|
|
335
375
|
export declare const SearchOutputSchema: z.ZodObject<{
|
|
@@ -426,6 +466,14 @@ export declare const InitOutputSchema: z.ZodObject<{
|
|
|
426
466
|
username: z.ZodString;
|
|
427
467
|
message: z.ZodString;
|
|
428
468
|
}, z.core.$strip>;
|
|
469
|
+
export declare const ManifestOutputSchema: z.ZodObject<{
|
|
470
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
471
|
+
cliVersion: z.ZodString;
|
|
472
|
+
commands: z.ZodArray<z.ZodObject<{
|
|
473
|
+
name: z.ZodString;
|
|
474
|
+
localOnly: z.ZodBoolean;
|
|
475
|
+
}, z.core.$strip>>;
|
|
476
|
+
}, z.core.$strip>;
|
|
429
477
|
export declare const CheckSetupOutputSchema: z.ZodObject<{
|
|
430
478
|
setupComplete: z.ZodBoolean;
|
|
431
479
|
username: z.ZodString;
|
|
@@ -791,6 +839,8 @@ export interface CommentsOutput {
|
|
|
791
839
|
inlineCommentCount: number;
|
|
792
840
|
discussionCommentCount: number;
|
|
793
841
|
};
|
|
842
|
+
/** Non-fatal warnings (e.g. stale-cache fallback, #1193). Always present; empty on clean runs. */
|
|
843
|
+
warnings?: DailyWarning[];
|
|
794
844
|
}
|
|
795
845
|
/** Output of the post command */
|
|
796
846
|
export interface PostOutput {
|