@oss-autopilot/core 3.2.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 +33 -3
- package/dist/cli.bundle.cjs +96 -93
- 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.js +15 -3
- 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 +18 -2
- package/dist/core/gist-state-store.js +73 -13
- package/dist/core/guidelines-store.js +2 -2
- package/dist/core/http-cache.js +6 -6
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/issue-conversation.js +3 -1
- package/dist/core/paths.js +4 -4
- package/dist/core/pr-monitor.js +1 -2
- package/dist/core/pr-template.js +1 -1
- package/dist/core/state-persistence.js +7 -7
- package/dist/core/state.d.ts +27 -0
- package/dist/core/state.js +66 -13
- 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.js
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
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
13
|
import * as guidelinesStoreModule from './guidelines-store.js';
|
|
14
14
|
import { getStatePath, getStateCachePath } from './paths.js';
|
|
@@ -53,13 +53,6 @@ export async function maybeCheckpoint(stateManager, callerModule) {
|
|
|
53
53
|
warn(callerModule, `Gist checkpoint failed (local mutation succeeded, will retry on next push): ${errorMessage(err)}`);
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
/**
|
|
57
|
-
* Singleton manager for persistent agent state stored in ~/.oss-autopilot/state.json.
|
|
58
|
-
*
|
|
59
|
-
* Delegates file I/O to state-persistence.ts and scoring logic to repo-score-manager.ts.
|
|
60
|
-
* Retains lightweight CRUD operations for config, issues, shelving, dismissal,
|
|
61
|
-
* and status overrides.
|
|
62
|
-
*/
|
|
63
56
|
export class StateManager {
|
|
64
57
|
state;
|
|
65
58
|
inMemoryOnly;
|
|
@@ -68,6 +61,8 @@ export class StateManager {
|
|
|
68
61
|
_batchDirty = false;
|
|
69
62
|
gistStore = null;
|
|
70
63
|
gistDegraded = false;
|
|
64
|
+
staleness = null;
|
|
65
|
+
lastSuccessfulRefreshAt = null;
|
|
71
66
|
/**
|
|
72
67
|
* Create a new StateManager instance.
|
|
73
68
|
* @param inMemoryOnly - When true, state is held only in memory and never read from or
|
|
@@ -131,6 +126,16 @@ export class StateManager {
|
|
|
131
126
|
manager.gistStore = gistStore;
|
|
132
127
|
manager.gistDegraded = result.degraded ?? false;
|
|
133
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
|
+
}
|
|
134
139
|
return manager;
|
|
135
140
|
}
|
|
136
141
|
/**
|
|
@@ -345,6 +350,15 @@ export class StateManager {
|
|
|
345
350
|
const raw = this.gistStore.cachedFiles.get('state.json');
|
|
346
351
|
if (!raw) {
|
|
347
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
|
+
};
|
|
348
362
|
return false;
|
|
349
363
|
}
|
|
350
364
|
try {
|
|
@@ -353,11 +367,41 @@ export class StateManager {
|
|
|
353
367
|
}
|
|
354
368
|
catch (err) {
|
|
355
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
|
+
};
|
|
356
378
|
return false;
|
|
357
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
|
+
};
|
|
358
393
|
}
|
|
359
394
|
return refreshed;
|
|
360
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
|
+
}
|
|
361
405
|
// === Dashboard Data Setters ===
|
|
362
406
|
/**
|
|
363
407
|
* Store the latest daily digest and update the digest timestamp.
|
|
@@ -847,11 +891,20 @@ export async function getStateManagerAsync(token) {
|
|
|
847
891
|
})
|
|
848
892
|
.catch((err) => {
|
|
849
893
|
asyncManagerPromise = null;
|
|
850
|
-
// 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).
|
|
851
897
|
if (err instanceof ConfigurationError)
|
|
852
898
|
throw err;
|
|
853
|
-
|
|
854
|
-
|
|
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();
|
|
855
908
|
});
|
|
856
909
|
return asyncManagerPromise;
|
|
857
910
|
}
|
|
@@ -880,7 +933,7 @@ export async function ensureGistPersistence(token) {
|
|
|
880
933
|
return;
|
|
881
934
|
let persistence;
|
|
882
935
|
try {
|
|
883
|
-
const raw = fs.readFileSync(getStatePath(), '
|
|
936
|
+
const raw = fs.readFileSync(getStatePath(), 'utf8');
|
|
884
937
|
persistence = JSON.parse(raw)?.config?.persistence;
|
|
885
938
|
}
|
|
886
939
|
catch {
|
|
@@ -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 {
|
package/dist/formatters/json.js
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
* Provides structured output that can be consumed by scripts and plugins
|
|
4
4
|
*/
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
+
export function buildStalenessWarning(info) {
|
|
7
|
+
return {
|
|
8
|
+
phase: 'gist-staleness',
|
|
9
|
+
operation: 'state refresh',
|
|
10
|
+
message: `Operating on ${info.source} state — Gist refresh failed: ${info.reason}`,
|
|
11
|
+
timestamp: info.detectedAt,
|
|
12
|
+
details: {
|
|
13
|
+
source: info.source,
|
|
14
|
+
lastSuccessfulRefresh: info.lastSuccessfulRefresh,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
6
18
|
/**
|
|
7
19
|
* Strip a full DailyOutput down to the compact subset (#763).
|
|
8
20
|
* Omits summary, repoGroups, and full failures array. Retains a failureCount
|
|
@@ -61,6 +73,27 @@ export function compactRepoGroups(groups) {
|
|
|
61
73
|
prUrls: group.prs.map((pr) => pr.url),
|
|
62
74
|
}));
|
|
63
75
|
}
|
|
76
|
+
// DailyWarning schema lives here (rather than further down with the rest of
|
|
77
|
+
// the daily schemas) so StatusOutputSchema below can reference it directly
|
|
78
|
+
// without `z.lazy()`. The runtime shape is shared across daily / status /
|
|
79
|
+
// comments warnings — see `DailyWarning` interface above.
|
|
80
|
+
const DailyWarningPhaseSchema = z.enum([
|
|
81
|
+
'fetch',
|
|
82
|
+
'repo-scores',
|
|
83
|
+
'analytics',
|
|
84
|
+
'scout-sync',
|
|
85
|
+
'partition',
|
|
86
|
+
'dismiss-filter',
|
|
87
|
+
'gist-checkpoint',
|
|
88
|
+
'gist-staleness',
|
|
89
|
+
]);
|
|
90
|
+
const DailyWarningSchema = z.object({
|
|
91
|
+
phase: DailyWarningPhaseSchema,
|
|
92
|
+
operation: z.string(),
|
|
93
|
+
message: z.string(),
|
|
94
|
+
timestamp: z.string().optional(),
|
|
95
|
+
details: z.record(z.string(), z.unknown()).optional(),
|
|
96
|
+
});
|
|
64
97
|
export const StatusOutputSchema = z.object({
|
|
65
98
|
stats: z.object({
|
|
66
99
|
mergedPRs: z.number().int().nonnegative(),
|
|
@@ -73,6 +106,7 @@ export const StatusOutputSchema = z.object({
|
|
|
73
106
|
lastRunAt: z.string(),
|
|
74
107
|
offline: z.boolean().optional(),
|
|
75
108
|
lastUpdated: z.string().optional(),
|
|
109
|
+
warnings: z.array(DailyWarningSchema).optional(),
|
|
76
110
|
});
|
|
77
111
|
// ── Daily output schemas (#1146) ─────────────────────────────────────
|
|
78
112
|
//
|
|
@@ -162,20 +196,8 @@ const CompactRepoGroupSchema = z.object({
|
|
|
162
196
|
repo: z.string(),
|
|
163
197
|
prUrls: z.array(z.string()),
|
|
164
198
|
});
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
'repo-scores',
|
|
168
|
-
'analytics',
|
|
169
|
-
'scout-sync',
|
|
170
|
-
'partition',
|
|
171
|
-
'dismiss-filter',
|
|
172
|
-
'gist-checkpoint',
|
|
173
|
-
]);
|
|
174
|
-
const DailyWarningSchema = z.object({
|
|
175
|
-
phase: DailyWarningPhaseSchema,
|
|
176
|
-
operation: z.string(),
|
|
177
|
-
message: z.string(),
|
|
178
|
-
});
|
|
199
|
+
// DailyWarning schemas were hoisted above StatusOutputSchema (#1193) so the
|
|
200
|
+
// status output can reference them without `z.lazy()`.
|
|
179
201
|
export const DailyOutputSchema = z.object({
|
|
180
202
|
digest: DailyDigestCompactSchema,
|
|
181
203
|
capacity: CapacityAssessmentSchema,
|
|
@@ -278,6 +300,19 @@ export const InitOutputSchema = z.object({
|
|
|
278
300
|
username: z.string(),
|
|
279
301
|
message: z.string(),
|
|
280
302
|
});
|
|
303
|
+
// ── #1190: plugin → CLI contract ─────────────────────────────────────
|
|
304
|
+
//
|
|
305
|
+
// Pinned shape lets the plugin's session-start hook verify that the bundled
|
|
306
|
+
// CLI exposes the subcommands the markdown layer expects. Bumping
|
|
307
|
+
// `schemaVersion` is a breaking change to that contract.
|
|
308
|
+
export const ManifestOutputSchema = z.object({
|
|
309
|
+
schemaVersion: z.literal(1),
|
|
310
|
+
cliVersion: z.string().regex(/^\d+\.\d+\.\d+/),
|
|
311
|
+
commands: z.array(z.object({
|
|
312
|
+
name: z.string(),
|
|
313
|
+
localOnly: z.boolean(),
|
|
314
|
+
})),
|
|
315
|
+
});
|
|
281
316
|
export const CheckSetupOutputSchema = z.object({
|
|
282
317
|
setupComplete: z.boolean(),
|
|
283
318
|
username: z.string(),
|