@evomap/evolver 1.91.1 → 1.92.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/index.js +150 -71
- package/package.json +2 -1
- package/src/canonicalIdentityLock.js +455 -0
- package/src/evolve/guards.js +1 -1
- package/src/evolve/pipeline/collect.js +1 -1
- package/src/evolve/pipeline/dispatch.js +1 -1
- package/src/evolve/pipeline/enrich.js +1 -1
- package/src/evolve/pipeline/hub.js +1 -1
- package/src/evolve/pipeline/select.js +1 -1
- package/src/evolve/pipeline/signals.js +1 -1
- package/src/evolve/utils.js +1 -1
- package/src/evolve.js +1 -1
- package/src/gep/a2aProtocol.js +1 -1
- package/src/gep/antiAbuseTelemetry.js +1 -1
- package/src/gep/assetStore.js +56 -11
- package/src/gep/autoDistillConv.js +1 -1
- package/src/gep/autoDistillLlm.js +1 -1
- package/src/gep/candidateEval.js +1 -1
- package/src/gep/candidates.js +1 -1
- package/src/gep/contentHash.js +1 -1
- package/src/gep/conversationDistiller.js +1 -1
- package/src/gep/conversationSniffer.js +1 -1
- package/src/gep/crypto.js +1 -1
- package/src/gep/curriculum.js +1 -1
- package/src/gep/deviceId.js +1 -1
- package/src/gep/envFingerprint.js +1 -1
- package/src/gep/epigenetics.js +1 -1
- package/src/gep/execBridge.js +1 -1
- package/src/gep/explore.js +1 -1
- package/src/gep/hash.js +1 -1
- package/src/gep/hubFetch.js +1 -1
- package/src/gep/hubReview.js +1 -1
- package/src/gep/hubSearch.js +1 -1
- package/src/gep/hubVerify.js +1 -1
- package/src/gep/learningSignals.js +1 -1
- package/src/gep/memoryGraph.js +1 -1
- package/src/gep/memoryGraphAdapter.js +1 -1
- package/src/gep/mutation.js +1 -1
- package/src/gep/narrativeMemory.js +1 -1
- package/src/gep/openPRRegistry.js +1 -1
- package/src/gep/personality.js +1 -1
- package/src/gep/policyCheck.js +1 -1
- package/src/gep/prompt.js +1 -1
- package/src/gep/recallInject.js +1 -1
- package/src/gep/recallVerifier.js +1 -1
- package/src/gep/reflection.js +1 -1
- package/src/gep/savingsCore.js +1 -1
- package/src/gep/schemas/gene.js +2 -0
- package/src/gep/selector.js +1 -1
- package/src/gep/skillDistiller.js +1 -1
- package/src/gep/solidify.js +1 -1
- package/src/gep/strategy.js +1 -1
- package/src/gep/syncAsset.js +1 -0
- package/src/gep/tokenSavings.js +1 -1
- package/src/gep/trajectoryExport.js +1 -1
- package/src/gep/workspaceKeychain.js +1 -1
- package/src/proxy/extensions/traceControl.js +1 -1
- package/src/proxy/index.js +136 -8
- package/src/proxy/inject.js +1 -1
- package/src/proxy/lifecycle/manager.js +279 -18
- package/src/proxy/mailbox/state.js +60 -29
- package/src/proxy/trace/extractor.js +1 -1
- package/src/proxy/trace/usage.js +1 -1
- package/src/webui/client/clientJs/bootstrap.js +6 -0
- package/src/webui/client/clientJs/github.js +225 -0
- package/src/webui/client/clientJs/i18n.js +14 -0
- package/src/webui/client/clientJs/index.js +3 -0
- package/src/webui/client/clientJs/pipelines.js +3 -1
- package/src/webui/client/indexHtml.js +10 -0
- package/src/webui/client/stylesCss.js +61 -0
- package/src/webui/observer/github.js +340 -0
- package/src/webui/observer/index.js +4 -0
- package/src/webui/server/routes.js +11 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// observer/github — read-only GitHub PR status for the Web UI hovercard.
|
|
5
|
+
//
|
|
6
|
+
// Mirrors the Claude Code "hover a #123 → floating PR status card" behaviour:
|
|
7
|
+
// the client linkifies PR references and, on hover, asks this observer for the
|
|
8
|
+
// live status of one PR (state, title, author, +additions/-deletions, changed
|
|
9
|
+
// files, timestamps). It also feeds a dedicated "Pull Requests" panel with the
|
|
10
|
+
// repo's open PRs.
|
|
11
|
+
//
|
|
12
|
+
// Data source precedence (per the approved plan):
|
|
13
|
+
// 1. `gh pr view N --json ...` — argv form, NO shell (mirrors selfPR.runGh),
|
|
14
|
+
// reuses the operator's existing `gh` auth.
|
|
15
|
+
// 2. GitHub REST `/repos/{slug}/pulls/{n}` — fallback when `gh` is missing,
|
|
16
|
+
// unauthenticated, times out, or errors. Token optional (public repos
|
|
17
|
+
// read without one), resolved like issueReporter.getGithubToken().
|
|
18
|
+
//
|
|
19
|
+
// The module is graceful by construction: EVERY failure path returns
|
|
20
|
+
// `{ number, available: false, reason }` rather than throwing, so a flaky
|
|
21
|
+
// network or a non-GitHub checkout can never break the dashboard. A short
|
|
22
|
+
// negative cache stops hover-jitter from amplifying failed lookups into a
|
|
23
|
+
// request storm. EVOLVER_WEBUI_GITHUB=0 short-circuits the whole feature.
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
// Note: do not destructure execFileSync / fetch at module load — tests stub
|
|
27
|
+
// them at call time via the live references (child_process.execFileSync and
|
|
28
|
+
// global.fetch), matching the openPRRegistry / issueReporter test style.
|
|
29
|
+
const { envInt } = require('../../config');
|
|
30
|
+
const { getRepoRoot } = require('../../gep/paths');
|
|
31
|
+
|
|
32
|
+
const MAX_EXEC_BUFFER = 10 * 1024 * 1024;
|
|
33
|
+
const GH_TIMEOUT_MS = 5000;
|
|
34
|
+
const API_TIMEOUT_MS = 10000;
|
|
35
|
+
const NEG_CACHE_TTL_MS = 10000; // failed lookups: cache briefly to damp hover jitter
|
|
36
|
+
const MAX_CACHE_ENTRIES = 200; // bound memory; LRU-evict oldest beyond this
|
|
37
|
+
|
|
38
|
+
// gh's --json field list. Kept identical to the REST fields we normalise from
|
|
39
|
+
// so both sources produce the same shape.
|
|
40
|
+
const GH_PR_FIELDS = 'number,title,state,isDraft,author,additions,deletions,changedFiles,createdAt,updatedAt,mergedAt,closedAt,url';
|
|
41
|
+
|
|
42
|
+
const _prCache = new Map(); // number -> { data, at }
|
|
43
|
+
const _inflight = new Map(); // number -> Promise<data>
|
|
44
|
+
let _ghMissingWarned = false;
|
|
45
|
+
|
|
46
|
+
function _now() { return Date.now(); }
|
|
47
|
+
|
|
48
|
+
function _isFeatureEnabled() {
|
|
49
|
+
return String(process.env.EVOLVER_WEBUI_GITHUB || '1') !== '0';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _getTtlMs() {
|
|
53
|
+
return envInt('EVOLVER_WEBUI_GITHUB_TTL_MS', 60000);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function _getGithubToken() {
|
|
57
|
+
return process.env.GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_PAT || '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// A PR number is untrusted route input. Accept only a positive *safe* integer;
|
|
61
|
+
// never let anything else reach `gh` or the REST URL. The safe-integer bound
|
|
62
|
+
// applies to both branches so a huge numeric input (e.g. Number('9'.repeat(23))
|
|
63
|
+
// === 1e23, whose String() is the non-numeric '1e+23') is rejected cleanly
|
|
64
|
+
// rather than handed to `gh pr view 1e+23`.
|
|
65
|
+
function _normalizeNumber(input) {
|
|
66
|
+
if (typeof input === 'number') {
|
|
67
|
+
return Number.isSafeInteger(input) && input > 0 ? input : null;
|
|
68
|
+
}
|
|
69
|
+
const s = String(input == null ? '' : input).trim();
|
|
70
|
+
if (!/^\d+$/.test(s)) return null;
|
|
71
|
+
const n = Number(s);
|
|
72
|
+
return Number.isSafeInteger(n) && n > 0 ? n : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// --- Repo slug resolution ---------------------------------------------------
|
|
76
|
+
// Precedence: EVOLVER_GITHUB_REPO → `git remote get-url origin` → SELF_PR_REPO.
|
|
77
|
+
let _slugCache; // undefined = unresolved; string|null once resolved
|
|
78
|
+
|
|
79
|
+
function _parseSlugFromRemote(url) {
|
|
80
|
+
const m = String(url || '').trim().match(/github\.com[:/]([^/]+)\/([^/\s]+?)(?:\.git)?$/i);
|
|
81
|
+
return m ? m[1] + '/' + m[2] : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function _resolveRepoSlug() {
|
|
85
|
+
if (_slugCache !== undefined) return _slugCache;
|
|
86
|
+
const envSlug = String(process.env.EVOLVER_GITHUB_REPO || '').trim();
|
|
87
|
+
if (envSlug) { _slugCache = envSlug; return _slugCache; }
|
|
88
|
+
try {
|
|
89
|
+
const out = require('child_process').execFileSync('git', ['remote', 'get-url', 'origin'], {
|
|
90
|
+
cwd: getRepoRoot(),
|
|
91
|
+
encoding: 'utf8',
|
|
92
|
+
timeout: GH_TIMEOUT_MS,
|
|
93
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
94
|
+
maxBuffer: MAX_EXEC_BUFFER,
|
|
95
|
+
});
|
|
96
|
+
const parsed = _parseSlugFromRemote(out);
|
|
97
|
+
if (parsed) { _slugCache = parsed; return _slugCache; }
|
|
98
|
+
} catch (_) { /* fall through to config default */ }
|
|
99
|
+
try {
|
|
100
|
+
_slugCache = require('../../config').SELF_PR_REPO || null;
|
|
101
|
+
} catch (_) {
|
|
102
|
+
_slugCache = null;
|
|
103
|
+
}
|
|
104
|
+
return _slugCache;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- Normalisation ----------------------------------------------------------
|
|
108
|
+
// gh: state ∈ {OPEN, CLOSED, MERGED} + isDraft. REST: state ∈ {open, closed} +
|
|
109
|
+
// merged_at + draft. Collapse both to merged|open|closed|draft.
|
|
110
|
+
function _normalizeState(rawState, isDraft, mergedAt) {
|
|
111
|
+
if (mergedAt) return 'merged';
|
|
112
|
+
const s = String(rawState || '').toLowerCase();
|
|
113
|
+
if (s === 'merged') return 'merged';
|
|
114
|
+
if (s === 'closed') return 'closed';
|
|
115
|
+
if (isDraft) return 'draft';
|
|
116
|
+
return s === 'open' ? 'open' : (s || 'open');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function _normalizeGh(raw) {
|
|
120
|
+
const author = raw.author && typeof raw.author === 'object'
|
|
121
|
+
? String(raw.author.login || raw.author.name || '')
|
|
122
|
+
: '';
|
|
123
|
+
return {
|
|
124
|
+
number: raw.number,
|
|
125
|
+
title: String(raw.title || ''),
|
|
126
|
+
state: _normalizeState(raw.state, raw.isDraft, raw.mergedAt),
|
|
127
|
+
author,
|
|
128
|
+
additions: Number(raw.additions) || 0,
|
|
129
|
+
deletions: Number(raw.deletions) || 0,
|
|
130
|
+
changedFiles: Number(raw.changedFiles) || 0,
|
|
131
|
+
createdAt: raw.createdAt || null,
|
|
132
|
+
updatedAt: raw.updatedAt || null,
|
|
133
|
+
mergedAt: raw.mergedAt || null,
|
|
134
|
+
closedAt: raw.closedAt || null,
|
|
135
|
+
url: String(raw.url || ''),
|
|
136
|
+
source: 'gh',
|
|
137
|
+
available: true,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function _normalizeApi(raw) {
|
|
142
|
+
const author = raw.user && typeof raw.user === 'object' ? String(raw.user.login || '') : '';
|
|
143
|
+
return {
|
|
144
|
+
number: raw.number,
|
|
145
|
+
title: String(raw.title || ''),
|
|
146
|
+
state: _normalizeState(raw.state, raw.draft, raw.merged_at),
|
|
147
|
+
author,
|
|
148
|
+
additions: Number(raw.additions) || 0,
|
|
149
|
+
deletions: Number(raw.deletions) || 0,
|
|
150
|
+
changedFiles: Number(raw.changed_files) || 0,
|
|
151
|
+
createdAt: raw.created_at || null,
|
|
152
|
+
updatedAt: raw.updated_at || null,
|
|
153
|
+
mergedAt: raw.merged_at || null,
|
|
154
|
+
closedAt: raw.closed_at || null,
|
|
155
|
+
url: String(raw.html_url || ''),
|
|
156
|
+
source: 'api',
|
|
157
|
+
available: true,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// --- Fetch paths ------------------------------------------------------------
|
|
162
|
+
// Returns a normalized PR object, or null if this source could not answer
|
|
163
|
+
// (so the caller can try the next source).
|
|
164
|
+
function _fetchViaGh(n) {
|
|
165
|
+
let raw;
|
|
166
|
+
try {
|
|
167
|
+
raw = require('child_process').execFileSync(
|
|
168
|
+
'gh',
|
|
169
|
+
['pr', 'view', String(n), '--json', GH_PR_FIELDS],
|
|
170
|
+
{
|
|
171
|
+
cwd: getRepoRoot(),
|
|
172
|
+
encoding: 'utf8',
|
|
173
|
+
timeout: GH_TIMEOUT_MS,
|
|
174
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
175
|
+
maxBuffer: MAX_EXEC_BUFFER,
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
} catch (e) {
|
|
179
|
+
const msg = e && e.message ? String(e.message) : '';
|
|
180
|
+
// Distinguish "gh binary is absent" (spawn failure) from "gh ran but
|
|
181
|
+
// couldn't answer" (unknown/private PR → non-zero exit). Only the former
|
|
182
|
+
// should latch the fall-back-to-REST warning. Match the spawn-failure
|
|
183
|
+
// signals (`e.code === 'ENOENT'`, or "command not found" from a shell
|
|
184
|
+
// wrapper) — NOT a bare "not found", which also appears in gh's own
|
|
185
|
+
// "no pull requests found" message for a real but missing PR.
|
|
186
|
+
const ghMissing = (e && e.code === 'ENOENT') || /\bENOENT\b|command not found/i.test(msg);
|
|
187
|
+
if (ghMissing && !_ghMissingWarned) {
|
|
188
|
+
_ghMissingWarned = true;
|
|
189
|
+
console.warn('[WebUI/GitHub] gh CLI not available — falling back to REST API. Install gh or set a GITHUB_TOKEN.');
|
|
190
|
+
}
|
|
191
|
+
// Non-zero exit (unknown PR, auth) also lands here: signal "gh can't answer"
|
|
192
|
+
// so the caller falls through to the REST path.
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
let parsed;
|
|
196
|
+
try {
|
|
197
|
+
parsed = JSON.parse(raw || '{}');
|
|
198
|
+
} catch (_) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
if (!parsed || typeof parsed !== 'object' || parsed.number == null) return null;
|
|
202
|
+
return _normalizeGh(parsed);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function _fetchViaApi(n) {
|
|
206
|
+
const slug = _resolveRepoSlug();
|
|
207
|
+
if (!slug) return { number: n, available: false, reason: 'no_repo_slug' };
|
|
208
|
+
const token = _getGithubToken();
|
|
209
|
+
const headers = {
|
|
210
|
+
'Accept': 'application/vnd.github+json',
|
|
211
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
212
|
+
'User-Agent': 'evolver-webui',
|
|
213
|
+
};
|
|
214
|
+
if (token) headers['Authorization'] = 'Bearer ' + token;
|
|
215
|
+
const url = 'https://api.github.com/repos/' + slug + '/pulls/' + n;
|
|
216
|
+
let res;
|
|
217
|
+
try {
|
|
218
|
+
res = await fetch(url, { method: 'GET', headers, signal: AbortSignal.timeout(API_TIMEOUT_MS) });
|
|
219
|
+
} catch (_) {
|
|
220
|
+
return { number: n, available: false, reason: 'network_error' };
|
|
221
|
+
}
|
|
222
|
+
if (res.status === 404) return { number: n, available: false, reason: 'not_found' };
|
|
223
|
+
if (res.status === 403 || res.status === 429) return { number: n, available: false, reason: 'rate_limited' };
|
|
224
|
+
if (!res.ok) return { number: n, available: false, reason: 'http_' + res.status };
|
|
225
|
+
let data;
|
|
226
|
+
try {
|
|
227
|
+
data = await res.json();
|
|
228
|
+
} catch (_) {
|
|
229
|
+
return { number: n, available: false, reason: 'bad_json' };
|
|
230
|
+
}
|
|
231
|
+
if (!data || data.number == null) return { number: n, available: false, reason: 'bad_json' };
|
|
232
|
+
return _normalizeApi(data);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// --- Cache helpers ----------------------------------------------------------
|
|
236
|
+
function _cacheGet(n) {
|
|
237
|
+
const hit = _prCache.get(n);
|
|
238
|
+
if (!hit) return null;
|
|
239
|
+
const ttl = hit.data && hit.data.available ? _getTtlMs() : NEG_CACHE_TTL_MS;
|
|
240
|
+
if (_now() - hit.at >= ttl) {
|
|
241
|
+
_prCache.delete(n);
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
// LRU touch: re-insert so recently-read entries survive eviction.
|
|
245
|
+
_prCache.delete(n);
|
|
246
|
+
_prCache.set(n, hit);
|
|
247
|
+
return hit.data;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function _cacheSet(n, data) {
|
|
251
|
+
_prCache.set(n, { data, at: _now() });
|
|
252
|
+
while (_prCache.size > MAX_CACHE_ENTRIES) {
|
|
253
|
+
const oldest = _prCache.keys().next().value;
|
|
254
|
+
_prCache.delete(oldest);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// --- Public API -------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
// Get one PR's normalized status. Never throws. Shape on success:
|
|
261
|
+
// { number, title, state, author, additions, deletions, changedFiles,
|
|
262
|
+
// createdAt, updatedAt, mergedAt, closedAt, url, source, available:true }
|
|
263
|
+
// On any failure (or feature disabled): { number, available:false, reason }.
|
|
264
|
+
async function getPrStatus(input) {
|
|
265
|
+
const n = _normalizeNumber(input);
|
|
266
|
+
if (n == null) return { number: null, available: false, reason: 'invalid_number' };
|
|
267
|
+
if (!_isFeatureEnabled()) return { number: n, available: false, reason: 'feature_disabled' };
|
|
268
|
+
|
|
269
|
+
const cached = _cacheGet(n);
|
|
270
|
+
if (cached) return cached;
|
|
271
|
+
|
|
272
|
+
// Single-flight: concurrent hovers on the same #N share one lookup.
|
|
273
|
+
const pending = _inflight.get(n);
|
|
274
|
+
if (pending) return pending;
|
|
275
|
+
|
|
276
|
+
const promise = (async () => {
|
|
277
|
+
let data = _fetchViaGh(n);
|
|
278
|
+
if (!data) data = await _fetchViaApi(n);
|
|
279
|
+
if (!data) data = { number: n, available: false, reason: 'unavailable' };
|
|
280
|
+
_cacheSet(n, data);
|
|
281
|
+
return data;
|
|
282
|
+
})().finally(() => { _inflight.delete(n); });
|
|
283
|
+
|
|
284
|
+
_inflight.set(n, promise);
|
|
285
|
+
return promise;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Open PRs for the dedicated panel. Reuses the existing open-PR registry so we
|
|
289
|
+
// don't spawn a second gh path. Returns [] on any failure.
|
|
290
|
+
//
|
|
291
|
+
// Note: openPRRegistry.getOpenPRs() is independently gated by
|
|
292
|
+
// EVOLVE_OPEN_PR_DEDUP — if an operator has the hovercard on
|
|
293
|
+
// (EVOLVER_WEBUI_GITHUB=1) but that dedup flag off (EVOLVE_OPEN_PR_DEDUP=0),
|
|
294
|
+
// this returns [] and the panel shows its "no open PRs (or gh unavailable)"
|
|
295
|
+
// empty state. That's intentional: we don't override another module's feature
|
|
296
|
+
// flag from here. Hovering any #N reference still fetches live status via the
|
|
297
|
+
// independent getPrStatus() path, so the core capability is unaffected.
|
|
298
|
+
function getOpenPrs() {
|
|
299
|
+
if (!_isFeatureEnabled()) return [];
|
|
300
|
+
try {
|
|
301
|
+
const { getOpenPRs } = require('../../gep/openPRRegistry');
|
|
302
|
+
const prs = getOpenPRs() || [];
|
|
303
|
+
return prs.map((pr) => ({
|
|
304
|
+
number: pr.number,
|
|
305
|
+
title: String(pr.title || ''),
|
|
306
|
+
headRefName: String(pr.headRefName || ''),
|
|
307
|
+
fileCount: Array.isArray(pr.files) ? pr.files.length : 0,
|
|
308
|
+
}));
|
|
309
|
+
} catch (_) {
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Repo slug + PR URL base, so the client can build hrefs and linkify #N.
|
|
315
|
+
function getRepoInfo() {
|
|
316
|
+
const slug = _isFeatureEnabled() ? _resolveRepoSlug() : null;
|
|
317
|
+
return {
|
|
318
|
+
slug: slug || null,
|
|
319
|
+
prUrlBase: slug ? 'https://github.com/' + slug + '/pull' : null,
|
|
320
|
+
available: !!slug,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Internal: reset caches for tests.
|
|
325
|
+
function _resetForTesting() {
|
|
326
|
+
_prCache.clear();
|
|
327
|
+
_inflight.clear();
|
|
328
|
+
_slugCache = undefined;
|
|
329
|
+
_ghMissingWarned = false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
module.exports = {
|
|
333
|
+
getPrStatus,
|
|
334
|
+
getOpenPrs,
|
|
335
|
+
getRepoInfo,
|
|
336
|
+
_resetForTesting,
|
|
337
|
+
_normalizeNumber,
|
|
338
|
+
_parseSlugFromRemote,
|
|
339
|
+
_normalizeState,
|
|
340
|
+
};
|
|
@@ -7,6 +7,7 @@ const { getAssetOverview, listGenes, listCapsules, listEvents, listCandidates, l
|
|
|
7
7
|
const { getInteractions } = require('./interactions');
|
|
8
8
|
const { getPersonality, getMemoryGraph } = require('./personality');
|
|
9
9
|
const { listSkills } = require('./skills');
|
|
10
|
+
const { getPrStatus, getOpenPrs, getRepoInfo } = require('./github');
|
|
10
11
|
const { getObserverPaths } = require('./paths');
|
|
11
12
|
const { tailText } = require('./jsonl');
|
|
12
13
|
const { redactText } = require('./redact');
|
|
@@ -34,4 +35,7 @@ module.exports = {
|
|
|
34
35
|
getMemoryGraph,
|
|
35
36
|
listSkills,
|
|
36
37
|
getEvolverLog,
|
|
38
|
+
getPrStatus,
|
|
39
|
+
getOpenPrs,
|
|
40
|
+
getRepoInfo,
|
|
37
41
|
};
|
|
@@ -24,6 +24,17 @@ function buildWebUiRoutes() {
|
|
|
24
24
|
'GET /webui/memory-graph': async ({ query }) => ({ body: observer.getMemoryGraph(query) }),
|
|
25
25
|
'GET /webui/skills': async () => ({ body: observer.listSkills() }),
|
|
26
26
|
'GET /webui/logs/evolver': async ({ query }) => ({ body: observer.getEvolverLog(query) }),
|
|
27
|
+
'GET /webui/github/repo': async () => ({ body: observer.getRepoInfo() }),
|
|
28
|
+
'GET /webui/github/prs': async () => ({ body: { data: observer.getOpenPrs() } }),
|
|
29
|
+
'GET /webui/github/pr/:number': async ({ params }) => {
|
|
30
|
+
// PR number is untrusted route input: only a positive integer may reach
|
|
31
|
+
// the observer (which shells `gh`). Reject anything else with a clean 400
|
|
32
|
+
// rather than letting it fall through to a lookup.
|
|
33
|
+
if (!/^\d+$/.test(String(params.number))) {
|
|
34
|
+
throw httpError(400, 'INVALID_PR_NUMBER', 'PR number must be a positive integer');
|
|
35
|
+
}
|
|
36
|
+
return { body: await observer.getPrStatus(Number(params.number)) };
|
|
37
|
+
},
|
|
27
38
|
};
|
|
28
39
|
}
|
|
29
40
|
|