@oss-autopilot/core 3.13.0 → 3.13.2
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.bundle.cjs +114 -99
- package/dist/commands/dashboard-data.js +24 -4
- package/dist/commands/dashboard-server.js +83 -16
- package/dist/commands/features.js +5 -1
- package/dist/commands/list-move-tier.js +11 -1
- package/dist/commands/scout-bridge.js +12 -0
- package/dist/commands/search.js +13 -4
- package/dist/core/auth.js +33 -6
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +1 -1
- package/dist/core/issue-grading.d.ts +2 -5
- package/dist/core/issue-grading.js +12 -1
- package/dist/core/state.d.ts +15 -1
- package/dist/core/state.js +38 -28
- package/dist/core/strategy.js +31 -4
- package/dist/core/untrusted-content.d.ts +24 -0
- package/dist/core/untrusted-content.js +25 -0
- package/dist/formatters/json.js +5 -1
- package/package.json +3 -3
package/dist/core/state.js
CHANGED
|
@@ -924,13 +924,27 @@ export function getStateManager() {
|
|
|
924
924
|
* StateManager and Gist checkpoints will be no-ops.
|
|
925
925
|
*/
|
|
926
926
|
export async function getStateManagerAsync(token) {
|
|
927
|
-
|
|
927
|
+
// #1415: a LOCAL-mode singleton does not short-circuit when a token is
|
|
928
|
+
// provided. The only token-bearing caller is ensureGistPersistence, which
|
|
929
|
+
// already verified the config says `persistence: gist` — so a local
|
|
930
|
+
// singleton here means a previous transient init failure fell back (or a
|
|
931
|
+
// tool body lazily created the local manager before auth resolved), and
|
|
932
|
+
// this call is the retry that must be allowed to upgrade it. The old
|
|
933
|
+
// unconditional early return made every retry a dead no-op, permanently
|
|
934
|
+
// latching gist-configured processes into silent local-only writes.
|
|
935
|
+
if (stateManager && (!token || stateManager.isGistMode()))
|
|
928
936
|
return stateManager;
|
|
929
937
|
if (asyncManagerPromise)
|
|
930
938
|
return asyncManagerPromise;
|
|
931
939
|
if (token) {
|
|
932
940
|
asyncManagerPromise = StateManager.createWithGist(token)
|
|
933
941
|
.then((mgr) => {
|
|
942
|
+
// Upgrade note: replacing a transient-fallback local singleton fixes
|
|
943
|
+
// FUTURE operations. Mutations made during the degraded window stay
|
|
944
|
+
// in the local state.json only — when a Gist already exists they are
|
|
945
|
+
// NOT merged into it by this upgrade (bootstrapWithMigration seeds a
|
|
946
|
+
// Gist from local state only on first creation). The per-call MCP
|
|
947
|
+
// warning is the signal that those writes were at risk.
|
|
934
948
|
stateManager = mgr;
|
|
935
949
|
asyncManagerPromise = null;
|
|
936
950
|
return mgr;
|
|
@@ -949,6 +963,9 @@ export async function getStateManagerAsync(token) {
|
|
|
949
963
|
// marker stays in config, causing permanent cross-machine divergence.
|
|
950
964
|
if (!isTransientNetworkError(err))
|
|
951
965
|
throw err;
|
|
966
|
+
// Intentional CLI semantics: a one-shot process warns and proceeds
|
|
967
|
+
// local-only. Long-lived callers (MCP) detect this via the
|
|
968
|
+
// ensureGistPersistence return status and retry on later calls.
|
|
952
969
|
warn(MODULE, `Gist initialization failed (transient network error), falling back to local-only mode: ${err}`);
|
|
953
970
|
return getStateManager();
|
|
954
971
|
});
|
|
@@ -956,39 +973,32 @@ export async function getStateManagerAsync(token) {
|
|
|
956
973
|
}
|
|
957
974
|
return getStateManager();
|
|
958
975
|
}
|
|
959
|
-
/**
|
|
960
|
-
* Bootstrap helper for processes that may run in Gist persistence mode.
|
|
961
|
-
*
|
|
962
|
-
* Peeks at the state file to check if Gist mode is configured. If so and a
|
|
963
|
-
* valid token is provided, pre-sets the singleton via {@link getStateManagerAsync}
|
|
964
|
-
* so subsequent synchronous {@link getStateManager} calls return the Gist-backed
|
|
965
|
-
* instance. No-op when the state file is absent, unparseable, or not in Gist mode.
|
|
966
|
-
*
|
|
967
|
-
* Consolidates identical filesystem-peek + getStateManagerAsync logic that
|
|
968
|
-
* was duplicated between the CLI bootstrap (`cli.ts`) and MCP tool bootstrap
|
|
969
|
-
* (`mcp-server/src/tools.ts`) — #1000.
|
|
970
|
-
*
|
|
971
|
-
* @param token - GitHub token with `gist` scope, or `null` to skip activation
|
|
972
|
-
*
|
|
973
|
-
* @example
|
|
974
|
-
* // CLI bootstrap
|
|
975
|
-
* await ensureGistPersistence(token);
|
|
976
|
-
*/
|
|
977
976
|
export async function ensureGistPersistence(token) {
|
|
978
|
-
if (!token)
|
|
979
|
-
return;
|
|
980
977
|
let persistence;
|
|
981
978
|
try {
|
|
982
979
|
const raw = fs.readFileSync(getStatePath(), 'utf8');
|
|
983
980
|
persistence = JSON.parse(raw)?.config?.persistence;
|
|
984
981
|
}
|
|
985
|
-
catch {
|
|
986
|
-
//
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
982
|
+
catch (err) {
|
|
983
|
+
// A missing file is the genuine "fresh local-mode user" case. Anything
|
|
984
|
+
// else (EACCES, EMFILE, corrupt JSON) must not be silently classified
|
|
985
|
+
// as a local-mode CHOICE — that would re-create the #1415 latch through
|
|
986
|
+
// a third door when the caller memoizes the answer.
|
|
987
|
+
if (err.code === 'ENOENT')
|
|
988
|
+
return 'local-mode';
|
|
989
|
+
warn(MODULE, `State file unreadable during gist-persistence check (will retry): ${errorMessage(err)}`);
|
|
990
|
+
return 'state-unreadable';
|
|
991
|
+
}
|
|
992
|
+
if (persistence !== 'gist')
|
|
993
|
+
return 'local-mode';
|
|
994
|
+
// #1415: report the distinction the MCP layer needs — "gist configured but
|
|
995
|
+
// token missing" and "init fell back to local" both mean the process is
|
|
996
|
+
// NOT writing to the Gist despite the config saying it should, and a
|
|
997
|
+
// long-lived caller must keep retrying instead of memoizing the outcome.
|
|
998
|
+
if (!token)
|
|
999
|
+
return 'no-token';
|
|
1000
|
+
const mgr = await getStateManagerAsync(token);
|
|
1001
|
+
return mgr.isGistMode() ? 'gist' : 'degraded';
|
|
992
1002
|
}
|
|
993
1003
|
/**
|
|
994
1004
|
* Reset the singleton StateManager instance to null. Intended for test isolation.
|
package/dist/core/strategy.js
CHANGED
|
@@ -105,6 +105,25 @@ function recommendForOverExtension(openPRCount, dormantPRCount, overExtended) {
|
|
|
105
105
|
return 'open_more';
|
|
106
106
|
return null;
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Read-only mirror of the status-override lookup (#1416): same staleness
|
|
110
|
+
* rule as `StateManager.getStatusOverride` (an override recorded against
|
|
111
|
+
* older activity than the PR's `updatedAt` is ignored), but with NO
|
|
112
|
+
* auto-clear write — this module stays pure/no-I/O, and the daily and
|
|
113
|
+
* dashboard pipelines own clearing stale overrides.
|
|
114
|
+
*/
|
|
115
|
+
function effectiveStatus(pr, state) {
|
|
116
|
+
if (typeof pr.url !== 'string')
|
|
117
|
+
return pr.status;
|
|
118
|
+
const override = state.config.statusOverrides?.[pr.url];
|
|
119
|
+
if (!override)
|
|
120
|
+
return pr.status;
|
|
121
|
+
if (typeof pr.updatedAt === 'string' && pr.updatedAt > override.lastActivityAt)
|
|
122
|
+
return pr.status;
|
|
123
|
+
if (override.status !== 'needs_addressing' && override.status !== 'waiting_on_maintainer')
|
|
124
|
+
return pr.status;
|
|
125
|
+
return override.status;
|
|
126
|
+
}
|
|
108
127
|
/**
|
|
109
128
|
* Compute the deterministic strategy signal from agent state. Returns
|
|
110
129
|
* null when state is thinner than {@link STRATEGY_MIN_PRS} merged PRs —
|
|
@@ -158,12 +177,20 @@ export function computeStrategy(state) {
|
|
|
158
177
|
// Capacity: read from the last digest. When no digest exists yet,
|
|
159
178
|
// default to zero (the agent has nothing to recommend without a
|
|
160
179
|
// digest). The DailyDigest schema does not store a `dormantCount`
|
|
161
|
-
// directly — derive it from the
|
|
162
|
-
//
|
|
163
|
-
//
|
|
180
|
+
// directly — derive it from the "awaiting maintainer review" bucket.
|
|
181
|
+
//
|
|
182
|
+
// Status-basis reconciliation (#1416): dashboard-written digests keep RAW
|
|
183
|
+
// statuses in `openPRs` (so override CLEARS stay visible on rebuild) while
|
|
184
|
+
// `summary.totalActivePRs` is override-basis. Re-derive the waiting bucket
|
|
185
|
+
// from `openPRs` through the override map so both capacity inputs share
|
|
186
|
+
// one basis; fall back to the stored array for digests without `openPRs`
|
|
187
|
+
// (which daily.ts builds post-override anyway).
|
|
164
188
|
const summary = state.lastDigest?.summary;
|
|
165
189
|
const openPRCount = summary?.totalActivePRs ?? 0;
|
|
166
|
-
const
|
|
190
|
+
const openPRs = (state.lastDigest?.openPRs ?? []);
|
|
191
|
+
const waiting = openPRs.length > 0
|
|
192
|
+
? openPRs.filter((pr) => effectiveStatus(pr, state) === 'waiting_on_maintainer')
|
|
193
|
+
: (state.lastDigest?.waitingOnMaintainerPRs ?? []);
|
|
167
194
|
const dormantPRCount = waiting.length;
|
|
168
195
|
// Overextended: dormant PRs spread across 2+ repos. Same definition
|
|
169
196
|
// the issue body uses.
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
*
|
|
19
19
|
* - `runComments` (commands/comments.ts): review / inline / discussion
|
|
20
20
|
* comment bodies in the `comments` CLI `--json` + MCP tool output.
|
|
21
|
+
* - `deduplicateDigest` (formatters/json.ts) and the MCP PR resources:
|
|
22
|
+
* `openPRs[].lastMaintainerComment.body` via {@link fenceFetchedPR}
|
|
23
|
+
* (#1420).
|
|
21
24
|
* - `fetchPRCommentBundle` (core/pr-comments-fetcher.ts): bundle bodies
|
|
22
25
|
* feeding `guidelines fetch-corpus` (agent-only consumer).
|
|
23
26
|
* - `toDailyOutput` (commands/daily.ts): `commentedIssues[].lastResponseBody`
|
|
@@ -67,3 +70,24 @@ export declare function extractFromFence(fenced: string): string;
|
|
|
67
70
|
* never throws.
|
|
68
71
|
*/
|
|
69
72
|
export declare function safeExtractFromFence(text: string): string;
|
|
73
|
+
/**
|
|
74
|
+
* Fence the attacker-controllable maintainer-comment excerpt on a FetchedPR
|
|
75
|
+
* for agent-facing serialization (#1420). Applied at the serialization
|
|
76
|
+
* boundary (daily/startup --json via deduplicateDigest, MCP PR resources)
|
|
77
|
+
* rather than at fetch time: extractMaintainerActionHints parses the RAW
|
|
78
|
+
* body inside the pipeline, and human surfaces (dashboard SPA, CLI text
|
|
79
|
+
* mode) must not render fence markup. Returns a copy; never mutates.
|
|
80
|
+
*/
|
|
81
|
+
export declare function fenceFetchedPR<T extends FenceablePR>(pr: T): T;
|
|
82
|
+
/** Structural slice of FetchedPR that {@link fenceFetchedPR} needs — kept
|
|
83
|
+
* structural to avoid an import cycle with types.ts consumers. */
|
|
84
|
+
interface FenceablePR {
|
|
85
|
+
repo: string;
|
|
86
|
+
number: number;
|
|
87
|
+
lastMaintainerComment?: {
|
|
88
|
+
author: string;
|
|
89
|
+
body: string;
|
|
90
|
+
createdAt: string;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export {};
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
*
|
|
19
19
|
* - `runComments` (commands/comments.ts): review / inline / discussion
|
|
20
20
|
* comment bodies in the `comments` CLI `--json` + MCP tool output.
|
|
21
|
+
* - `deduplicateDigest` (formatters/json.ts) and the MCP PR resources:
|
|
22
|
+
* `openPRs[].lastMaintainerComment.body` via {@link fenceFetchedPR}
|
|
23
|
+
* (#1420).
|
|
21
24
|
* - `fetchPRCommentBundle` (core/pr-comments-fetcher.ts): bundle bodies
|
|
22
25
|
* feeding `guidelines fetch-corpus` (agent-only consumer).
|
|
23
26
|
* - `toDailyOutput` (commands/daily.ts): `commentedIssues[].lastResponseBody`
|
|
@@ -132,3 +135,25 @@ export function safeExtractFromFence(text) {
|
|
|
132
135
|
return text;
|
|
133
136
|
}
|
|
134
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Fence the attacker-controllable maintainer-comment excerpt on a FetchedPR
|
|
140
|
+
* for agent-facing serialization (#1420). Applied at the serialization
|
|
141
|
+
* boundary (daily/startup --json via deduplicateDigest, MCP PR resources)
|
|
142
|
+
* rather than at fetch time: extractMaintainerActionHints parses the RAW
|
|
143
|
+
* body inside the pipeline, and human surfaces (dashboard SPA, CLI text
|
|
144
|
+
* mode) must not render fence markup. Returns a copy; never mutates.
|
|
145
|
+
*/
|
|
146
|
+
export function fenceFetchedPR(pr) {
|
|
147
|
+
if (!pr.lastMaintainerComment)
|
|
148
|
+
return pr;
|
|
149
|
+
return {
|
|
150
|
+
...pr,
|
|
151
|
+
lastMaintainerComment: {
|
|
152
|
+
...pr.lastMaintainerComment,
|
|
153
|
+
body: wrapUntrustedContent(pr.lastMaintainerComment.body, `${pr.repo}#${pr.number}`, {
|
|
154
|
+
author: pr.lastMaintainerComment.author,
|
|
155
|
+
source: 'pr-comment',
|
|
156
|
+
}),
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
package/dist/formatters/json.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Provides structured output that can be consumed by scripts and plugins
|
|
4
4
|
*/
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
+
import { fenceFetchedPR } from '../core/untrusted-content.js';
|
|
6
7
|
export function buildStalenessWarning(info) {
|
|
7
8
|
return {
|
|
8
9
|
phase: 'gist-staleness',
|
|
@@ -43,7 +44,10 @@ export function deduplicateDigest(digest) {
|
|
|
43
44
|
const toUrls = (prs) => prs.map((pr) => pr.url);
|
|
44
45
|
return {
|
|
45
46
|
generatedAt: digest.generatedAt,
|
|
46
|
-
|
|
47
|
+
// lastMaintainerComment.body is attacker-controllable on any public PR;
|
|
48
|
+
// fence it at this agent-facing serialization boundary (#1420). The
|
|
49
|
+
// digest itself keeps the raw body for pipeline parsing and human UIs.
|
|
50
|
+
openPRs: digest.openPRs.map(fenceFetchedPR),
|
|
47
51
|
needsAddressingPRs: toUrls(digest.needsAddressingPRs),
|
|
48
52
|
waitingOnMaintainerPRs: toUrls(digest.waitingOnMaintainerPRs),
|
|
49
53
|
recentlyClosedPRs: digest.recentlyClosedPRs,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oss-autopilot/core",
|
|
3
|
-
"version": "3.13.
|
|
3
|
+
"version": "3.13.2",
|
|
4
4
|
"description": "CLI and core library for managing open source contributions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@octokit/plugin-throttling": "^11.0.3",
|
|
56
56
|
"@octokit/rest": "^22.0.1",
|
|
57
|
-
"@oss-scout/core": "^
|
|
57
|
+
"@oss-scout/core": "^1.1.0",
|
|
58
58
|
"commander": "^15.0.0",
|
|
59
59
|
"zod": "^4.4.3"
|
|
60
60
|
},
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"bundle": "esbuild src/cli.ts --bundle --platform=node --target=node22 --format=cjs --minify --sourcemap --outfile=dist/cli.bundle.cjs",
|
|
73
73
|
"start": "tsx src/cli.ts",
|
|
74
74
|
"dev": "tsx watch src/cli.ts",
|
|
75
|
-
"typecheck": "tsc --noEmit",
|
|
75
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.tests.json",
|
|
76
76
|
"test": "vitest run",
|
|
77
77
|
"test:coverage": "vitest run --coverage",
|
|
78
78
|
"test:watch": "vitest",
|