engineering-memory 1.11.26 → 1.11.28
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/package.json +1 -1
- package/runtime/build.json +1 -1
- package/runtime/dist/src/config.js +1 -0
- package/runtime/dist/src/git/git-inspector.js +27 -8
- package/runtime/dist/src/localization/catalogue.generated.js +44 -0
- package/runtime/dist/src/mcp/delivery-tools.js +85 -0
- package/runtime/dist/src/mcp/tool-annotations.js +3 -0
- package/runtime/dist/src/mcp/tool-definitions.js +3 -0
- package/runtime/dist/src/providers/gitlab-token-page.js +226 -0
- package/runtime/dist/src/providers/gitlab.js +417 -0
- package/runtime/dist/src/runtime/active-context-store.js +19 -9
- package/runtime/dist/src/runtime/api-client.js +1 -0
- package/runtime/dist/src/runtime/bridge-service.js +157 -11
- package/runtime/dist/src/runtime/create-bridge-service.js +16 -2
- package/runtime/dist/src/runtime/merge-request-sync.js +281 -0
- package/runtime/dist/src/runtime/principal-state.js +4 -1
- package/runtime/dist/src/runtime/worktree-pool.js +9 -0
- package/skill/references/lifecycle.md +6 -2
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { isIP } from 'node:net';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import * as z from 'zod/v4';
|
|
4
|
+
import { principalFingerprint } from '../runtime/principal-state.js';
|
|
5
|
+
import { readJson, removeFile, writeJson } from '../utilities/files.js';
|
|
6
|
+
import { sha256 } from '../utilities/hash.js';
|
|
7
|
+
export function gitLabRemote(remote) {
|
|
8
|
+
const trimmed = remote.trim();
|
|
9
|
+
const scpLike = /^(?:[^@/:]+@)?([^/:]+):(.+)$/.exec(trimmed);
|
|
10
|
+
let scheme = 'ssh';
|
|
11
|
+
let hostname;
|
|
12
|
+
let port = '';
|
|
13
|
+
let path;
|
|
14
|
+
if (scpLike &&
|
|
15
|
+
!/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(trimmed) &&
|
|
16
|
+
!/^[A-Za-z]:[\\/]/.test(trimmed)) {
|
|
17
|
+
hostname = scpLike[1].toLowerCase();
|
|
18
|
+
path = scpLike[2];
|
|
19
|
+
}
|
|
20
|
+
else if (URL.canParse(trimmed)) {
|
|
21
|
+
const url = new URL(trimmed);
|
|
22
|
+
scheme = url.protocol.slice(0, -1).toLowerCase();
|
|
23
|
+
hostname = url.hostname.toLowerCase();
|
|
24
|
+
port = url.port;
|
|
25
|
+
path = url.pathname;
|
|
26
|
+
}
|
|
27
|
+
else
|
|
28
|
+
return { refusal: 'local' };
|
|
29
|
+
if (!hostname || scheme === 'file')
|
|
30
|
+
return { refusal: 'local' };
|
|
31
|
+
if (isIP(hostname.replace(/^\[|\]$/g, '')))
|
|
32
|
+
return { refusal: 'ip' };
|
|
33
|
+
if (hostname === 'github.com' || hostname.endsWith('.github.com'))
|
|
34
|
+
return { refusal: 'github' };
|
|
35
|
+
const segments = path
|
|
36
|
+
.replace(/^\/+|\/+$/g, '')
|
|
37
|
+
.replace(/\.git$/i, '')
|
|
38
|
+
.split('/');
|
|
39
|
+
if (segments.length < 2 || !segments.every(pathSegment))
|
|
40
|
+
return { refusal: 'unsupported' };
|
|
41
|
+
if (scheme === 'https' || scheme === 'http') {
|
|
42
|
+
const host = port ? `${hostname}:${port}` : hostname;
|
|
43
|
+
return {
|
|
44
|
+
host,
|
|
45
|
+
address: scheme === 'http' && !loopback(hostname) ? `https://${hostname}` : `${scheme}://${host}`,
|
|
46
|
+
path: segments.join('/'),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (['ssh', 'git+ssh', 'ssh+git', 'git'].includes(scheme))
|
|
50
|
+
return { host: hostname, address: `https://${hostname}`, path: segments.join('/') };
|
|
51
|
+
return { refusal: 'unsupported' };
|
|
52
|
+
}
|
|
53
|
+
export function gitLabAddress(value) {
|
|
54
|
+
const trimmed = value.trim();
|
|
55
|
+
if (trimmed.length > 300 || !URL.canParse(trimmed))
|
|
56
|
+
return null;
|
|
57
|
+
const url = new URL(trimmed);
|
|
58
|
+
const hostname = url.hostname.toLowerCase();
|
|
59
|
+
const path = url.pathname.replace(/\/+$/, '');
|
|
60
|
+
if ((url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback(hostname))) ||
|
|
61
|
+
url.username ||
|
|
62
|
+
url.password ||
|
|
63
|
+
url.search ||
|
|
64
|
+
url.hash ||
|
|
65
|
+
isIP(hostname.replace(/^\[|\]$/g, '')) ||
|
|
66
|
+
hostname === 'github.com' ||
|
|
67
|
+
hostname.endsWith('.github.com') ||
|
|
68
|
+
!path.split('/').slice(1).every(pathSegment))
|
|
69
|
+
return null;
|
|
70
|
+
return `${url.protocol}//${url.host.toLowerCase()}${path}`;
|
|
71
|
+
}
|
|
72
|
+
export function projectPath(address, path) {
|
|
73
|
+
const prefix = new URL(address).pathname.replace(/^\/+|\/+$/g, '');
|
|
74
|
+
return prefix && path.startsWith(prefix + '/') ? path.slice(prefix.length + 1) : path;
|
|
75
|
+
}
|
|
76
|
+
function pathSegment(segment) {
|
|
77
|
+
return /^[\w.-]+$/.test(segment) && segment !== '.' && segment !== '..';
|
|
78
|
+
}
|
|
79
|
+
function loopback(hostname) {
|
|
80
|
+
return hostname === 'localhost' || hostname.endsWith('.localhost');
|
|
81
|
+
}
|
|
82
|
+
export class GitLabError extends Error {
|
|
83
|
+
failure;
|
|
84
|
+
address;
|
|
85
|
+
status;
|
|
86
|
+
detail;
|
|
87
|
+
constructor(failure, address, status, detail) {
|
|
88
|
+
super(`GitLab at ${address}: ${failure}${status ? ` (${status})` : ''}${detail ? `: ${detail}` : ''}`);
|
|
89
|
+
this.failure = failure;
|
|
90
|
+
this.address = address;
|
|
91
|
+
this.status = status;
|
|
92
|
+
this.detail = detail;
|
|
93
|
+
this.name = 'GitLabError';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const certificateCodes = new Set([
|
|
97
|
+
'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
|
|
98
|
+
'UNABLE_TO_GET_ISSUER_CERT',
|
|
99
|
+
'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',
|
|
100
|
+
'SELF_SIGNED_CERT_IN_CHAIN',
|
|
101
|
+
'DEPTH_ZERO_SELF_SIGNED_CERT',
|
|
102
|
+
'CERT_HAS_EXPIRED',
|
|
103
|
+
'CERT_NOT_YET_VALID',
|
|
104
|
+
'CERT_UNTRUSTED',
|
|
105
|
+
'CERT_SIGNATURE_FAILURE',
|
|
106
|
+
'ERR_TLS_CERT_ALTNAME_INVALID',
|
|
107
|
+
]);
|
|
108
|
+
const connectCodes = new Set([
|
|
109
|
+
'ENOTFOUND',
|
|
110
|
+
'EAI_AGAIN',
|
|
111
|
+
'ECONNREFUSED',
|
|
112
|
+
'EHOSTUNREACH',
|
|
113
|
+
'ENETUNREACH',
|
|
114
|
+
'EADDRNOTAVAIL',
|
|
115
|
+
'UND_ERR_CONNECT_TIMEOUT',
|
|
116
|
+
]);
|
|
117
|
+
function networkCode(error) {
|
|
118
|
+
let current = error;
|
|
119
|
+
for (let depth = 0; depth < 5 && current && typeof current === 'object'; depth++) {
|
|
120
|
+
const code = current.code;
|
|
121
|
+
if (typeof code === 'string')
|
|
122
|
+
return code;
|
|
123
|
+
const tried = current.errors;
|
|
124
|
+
current = current.cause ?? (Array.isArray(tried) ? tried[0] : null);
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
function gitLabMessage(body) {
|
|
129
|
+
if (!body)
|
|
130
|
+
return null;
|
|
131
|
+
let message = body;
|
|
132
|
+
try {
|
|
133
|
+
const parsed = JSON.parse(body);
|
|
134
|
+
message = parsed.message ?? parsed.error ?? null;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const text = typeof message === 'string' ? message : message ? JSON.stringify(message) : '';
|
|
140
|
+
return text.replace(/[\u0000-\u001f\u007f]+/g, ' ').slice(0, 200) || null;
|
|
141
|
+
}
|
|
142
|
+
export const mergeRequestSchema = z.object({
|
|
143
|
+
iid: z.number().int().positive(),
|
|
144
|
+
web_url: z.string().min(1).max(500),
|
|
145
|
+
state: z.string(),
|
|
146
|
+
draft: z.boolean().optional(),
|
|
147
|
+
work_in_progress: z.boolean().optional(),
|
|
148
|
+
sha: z.string().nullish(),
|
|
149
|
+
source_branch: z.string(),
|
|
150
|
+
target_branch: z.string(),
|
|
151
|
+
project_id: z.number().optional(),
|
|
152
|
+
source_project_id: z.number().optional(),
|
|
153
|
+
});
|
|
154
|
+
export function reviewState(request) {
|
|
155
|
+
if (request.state === 'merged')
|
|
156
|
+
return 'merged';
|
|
157
|
+
if (request.state === 'closed')
|
|
158
|
+
return 'closed';
|
|
159
|
+
return request.draft || request.work_in_progress ? 'draft' : 'ready';
|
|
160
|
+
}
|
|
161
|
+
export class GitLabClient {
|
|
162
|
+
timeoutMs;
|
|
163
|
+
constructor(timeoutMs) {
|
|
164
|
+
this.timeoutMs = timeoutMs;
|
|
165
|
+
}
|
|
166
|
+
async user(address, token) {
|
|
167
|
+
await this.call(address, token, 'GET', '/user');
|
|
168
|
+
}
|
|
169
|
+
async scopes(address, token) {
|
|
170
|
+
try {
|
|
171
|
+
const self = z
|
|
172
|
+
.object({ scopes: z.array(z.string()) })
|
|
173
|
+
.safeParse(await this.call(address, token, 'GET', '/personal_access_tokens/self'));
|
|
174
|
+
return self.success ? self.data.scopes : null;
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
if (error instanceof GitLabError &&
|
|
178
|
+
['not_found', 'not_allowed', 'invalid_request'].includes(error.failure))
|
|
179
|
+
return null;
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async project(address, token, project) {
|
|
184
|
+
return await this.found(async () => this.shape(address, z.object({ id: z.number() }), await this.call(address, token, 'GET', `/projects/${encodeURIComponent(project)}`)));
|
|
185
|
+
}
|
|
186
|
+
async branch(address, token, project, branch) {
|
|
187
|
+
const found = await this.found(async () => this.shape(address, z.object({ commit: z.object({ id: z.string(), title: z.string().nullish() }) }), await this.call(address, token, 'GET', `/projects/${encodeURIComponent(project)}/repository/branches/${encodeURIComponent(branch)}`)));
|
|
188
|
+
return found ? { commit: found.commit.id, title: found.commit.title ?? '' } : null;
|
|
189
|
+
}
|
|
190
|
+
async contains(address, token, project, branch, commit) {
|
|
191
|
+
const query = new URLSearchParams([
|
|
192
|
+
['refs[]', commit],
|
|
193
|
+
['refs[]', branch],
|
|
194
|
+
]);
|
|
195
|
+
try {
|
|
196
|
+
const base = this.shape(address, z.object({ id: z.string() }), await this.call(address, token, 'GET', `/projects/${encodeURIComponent(project)}/repository/merge_base?${query}`));
|
|
197
|
+
return base.id === commit;
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
if (error instanceof GitLabError &&
|
|
201
|
+
(error.failure === 'not_found' || error.failure === 'invalid_request'))
|
|
202
|
+
return false;
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async mergeRequests(address, token, project, branch, state) {
|
|
207
|
+
const query = new URLSearchParams({
|
|
208
|
+
source_branch: branch,
|
|
209
|
+
order_by: 'created_at',
|
|
210
|
+
sort: 'desc',
|
|
211
|
+
per_page: '20',
|
|
212
|
+
...(state ? { state } : {}),
|
|
213
|
+
});
|
|
214
|
+
const listed = this.shape(address, z.array(mergeRequestSchema), await this.call(address, token, 'GET', `/projects/${encodeURIComponent(project.path)}/merge_requests?${query}`));
|
|
215
|
+
return listed.filter((request) => request.source_branch === branch &&
|
|
216
|
+
(request.source_project_id === undefined || request.source_project_id === project.id));
|
|
217
|
+
}
|
|
218
|
+
async mergeRequest(address, token, project, number) {
|
|
219
|
+
return this.shape(address, mergeRequestSchema, await this.call(address, token, 'GET', `/projects/${encodeURIComponent(project)}/merge_requests/${number}`));
|
|
220
|
+
}
|
|
221
|
+
async createMergeRequest(address, token, project, request) {
|
|
222
|
+
return this.shape(address, mergeRequestSchema, await this.call(address, token, 'POST', `/projects/${encodeURIComponent(project)}/merge_requests`, {
|
|
223
|
+
source_branch: request.source,
|
|
224
|
+
target_branch: request.target,
|
|
225
|
+
title: request.title,
|
|
226
|
+
...(request.description ? { description: request.description } : {}),
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
async found(read) {
|
|
230
|
+
try {
|
|
231
|
+
return await read();
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
if (error instanceof GitLabError && error.failure === 'not_found')
|
|
235
|
+
return null;
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
shape(address, schema, value) {
|
|
240
|
+
const parsed = schema.safeParse(value);
|
|
241
|
+
if (!parsed.success)
|
|
242
|
+
throw new GitLabError('unexpected_answer', address, null, null);
|
|
243
|
+
return parsed.data;
|
|
244
|
+
}
|
|
245
|
+
async call(address, token, method, path, body) {
|
|
246
|
+
let status;
|
|
247
|
+
let text;
|
|
248
|
+
try {
|
|
249
|
+
const response = await fetch(`${address}/api/v4${path}`, {
|
|
250
|
+
method,
|
|
251
|
+
redirect: 'manual',
|
|
252
|
+
headers: {
|
|
253
|
+
'PRIVATE-TOKEN': token,
|
|
254
|
+
Accept: 'application/json',
|
|
255
|
+
...(body ? { 'Content-Type': 'application/json' } : {}),
|
|
256
|
+
},
|
|
257
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
258
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
259
|
+
});
|
|
260
|
+
status = response.status;
|
|
261
|
+
text = await response.text();
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
const code = networkCode(error);
|
|
265
|
+
if (code && certificateCodes.has(code))
|
|
266
|
+
throw new GitLabError('untrusted_certificate', address, null, code);
|
|
267
|
+
if ((code && connectCodes.has(code)) || method === 'GET')
|
|
268
|
+
throw new GitLabError('unreachable', address, null, code);
|
|
269
|
+
throw new GitLabError('unknown_outcome', address, null, code);
|
|
270
|
+
}
|
|
271
|
+
if (status >= 200 && status < 300) {
|
|
272
|
+
try {
|
|
273
|
+
return JSON.parse(text);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
throw new GitLabError('unexpected_answer', address, status, null);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const failure = status >= 300 && status < 400
|
|
280
|
+
? 'redirected'
|
|
281
|
+
: status === 401
|
|
282
|
+
? 'token_rejected'
|
|
283
|
+
: status === 403
|
|
284
|
+
? 'not_allowed'
|
|
285
|
+
: status === 404
|
|
286
|
+
? 'not_found'
|
|
287
|
+
: status === 409
|
|
288
|
+
? 'conflict'
|
|
289
|
+
: status === 429
|
|
290
|
+
? 'rate_limited'
|
|
291
|
+
: status >= 500
|
|
292
|
+
? 'server_error'
|
|
293
|
+
: 'invalid_request';
|
|
294
|
+
throw new GitLabError(failure, address, status, gitLabMessage(text));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const savedAddresses = z.object({
|
|
298
|
+
schemaVersion: z.literal(1),
|
|
299
|
+
principalHash: z.string().min(1),
|
|
300
|
+
addresses: z.array(z.object({ host: z.string().min(1), address: z.string().min(1) })),
|
|
301
|
+
});
|
|
302
|
+
export class GitLabTokens {
|
|
303
|
+
stateRoot;
|
|
304
|
+
credentials;
|
|
305
|
+
path;
|
|
306
|
+
queue = Promise.resolve();
|
|
307
|
+
constructor(stateRoot, credentials) {
|
|
308
|
+
this.stateRoot = stateRoot;
|
|
309
|
+
this.credentials = credentials;
|
|
310
|
+
this.path = join(stateRoot, 'gitlab-addresses.json');
|
|
311
|
+
}
|
|
312
|
+
async address(host) {
|
|
313
|
+
return (await this.current())?.addresses.find((entry) => entry.host === host)?.address ?? null;
|
|
314
|
+
}
|
|
315
|
+
async forLink(host, repository) {
|
|
316
|
+
const addresses = [...new Set((await this.current())?.addresses.map((entry) => entry.address))]
|
|
317
|
+
.filter((address) => {
|
|
318
|
+
const url = new URL(address);
|
|
319
|
+
const prefix = url.pathname.replace(/^\/+/, '');
|
|
320
|
+
return url.host === host && (!prefix || repository.startsWith(prefix + '/'));
|
|
321
|
+
})
|
|
322
|
+
.sort((left, right) => right.length - left.length);
|
|
323
|
+
return addresses[0]
|
|
324
|
+
? { address: addresses[0], project: projectPath(addresses[0], repository) }
|
|
325
|
+
: null;
|
|
326
|
+
}
|
|
327
|
+
async token(address) {
|
|
328
|
+
const principal = await this.principal();
|
|
329
|
+
return principal ? await this.credentials.get(tokenName(principal, address)) : null;
|
|
330
|
+
}
|
|
331
|
+
async any() {
|
|
332
|
+
return Boolean((await this.current())?.addresses.length);
|
|
333
|
+
}
|
|
334
|
+
async save(host, address, token) {
|
|
335
|
+
await this.exclusive(async () => {
|
|
336
|
+
const principal = await this.principal();
|
|
337
|
+
if (!principal)
|
|
338
|
+
throw new Error('Sign in to Engineering Memory before saving a GitLab token.');
|
|
339
|
+
let saved = await this.read();
|
|
340
|
+
if (saved && saved.principalHash !== principal) {
|
|
341
|
+
await this.deleteTokens(saved);
|
|
342
|
+
saved = null;
|
|
343
|
+
}
|
|
344
|
+
const previous = saved?.addresses.find((entry) => entry.host === host)?.address;
|
|
345
|
+
await this.credentials.set(tokenName(principal, address), token);
|
|
346
|
+
const addresses = [
|
|
347
|
+
...(saved?.addresses ?? []).filter((entry) => entry.host !== host),
|
|
348
|
+
{ host, address },
|
|
349
|
+
];
|
|
350
|
+
await writeJson(this.path, { schemaVersion: 1, principalHash: principal, addresses }, this.stateRoot);
|
|
351
|
+
if (previous && !addresses.some((entry) => entry.address === previous))
|
|
352
|
+
await this.credentials.delete(tokenName(principal, previous));
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
async remove(address) {
|
|
356
|
+
await this.exclusive(async () => {
|
|
357
|
+
const principal = await this.principal();
|
|
358
|
+
if (!principal)
|
|
359
|
+
return;
|
|
360
|
+
await this.credentials.delete(tokenName(principal, address));
|
|
361
|
+
const saved = await this.read();
|
|
362
|
+
if (saved?.principalHash !== principal)
|
|
363
|
+
return;
|
|
364
|
+
await writeJson(this.path, { ...saved, addresses: saved.addresses.filter((entry) => entry.address !== address) }, this.stateRoot);
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
async clear() {
|
|
368
|
+
await this.exclusive(async () => {
|
|
369
|
+
const saved = await this.read();
|
|
370
|
+
if (saved)
|
|
371
|
+
await this.deleteTokens(saved);
|
|
372
|
+
await removeFile(this.path, this.stateRoot);
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
async deleteTokens(saved) {
|
|
376
|
+
for (const address of new Set(saved.addresses.map((entry) => entry.address)))
|
|
377
|
+
await this.credentials.delete(tokenName(saved.principalHash, address));
|
|
378
|
+
}
|
|
379
|
+
async current() {
|
|
380
|
+
const principal = await this.principal();
|
|
381
|
+
const saved = principal ? await this.read() : null;
|
|
382
|
+
return saved?.principalHash === principal ? saved : null;
|
|
383
|
+
}
|
|
384
|
+
async read() {
|
|
385
|
+
const parsed = savedAddresses.safeParse(await readJson(this.path, this.stateRoot));
|
|
386
|
+
return parsed.success ? parsed.data : null;
|
|
387
|
+
}
|
|
388
|
+
async principal() {
|
|
389
|
+
const accessToken = await this.credentials.get('access-token');
|
|
390
|
+
if (!accessToken)
|
|
391
|
+
return null;
|
|
392
|
+
try {
|
|
393
|
+
return principalFingerprint(accessToken);
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async exclusive(action) {
|
|
400
|
+
const previous = this.queue;
|
|
401
|
+
let release = () => undefined;
|
|
402
|
+
this.queue = new Promise((resolvePromise) => {
|
|
403
|
+
release = resolvePromise;
|
|
404
|
+
});
|
|
405
|
+
await previous;
|
|
406
|
+
try {
|
|
407
|
+
await action();
|
|
408
|
+
}
|
|
409
|
+
finally {
|
|
410
|
+
release();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function tokenName(principal, address) {
|
|
415
|
+
return `gitlab-token-${sha256(`${principal}\n${address}`).slice(0, 32)}`;
|
|
416
|
+
}
|
|
417
|
+
//# sourceMappingURL=gitlab.js.map
|
|
@@ -122,7 +122,7 @@ export class ActiveContextStore {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
-
async setChangeBaseline(repoFingerprint, sessionId, manifest, leasePaths, taskSnapshot) {
|
|
125
|
+
async setChangeBaseline(repoFingerprint, sessionId, manifest, leasePaths, taskSnapshot, selfReviewRequired) {
|
|
126
126
|
const pointer = taskSnapshot
|
|
127
127
|
? await this.loadForTask(repoFingerprint, taskSnapshot.taskId)
|
|
128
128
|
: await this.loadForSession(repoFingerprint, sessionId);
|
|
@@ -140,13 +140,12 @@ export class ActiveContextStore {
|
|
|
140
140
|
lastSequence: taskSnapshot.lastSequence,
|
|
141
141
|
}
|
|
142
142
|
: {}),
|
|
143
|
-
changeBaseline:
|
|
144
|
-
|
|
145
|
-
:
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
},
|
|
143
|
+
changeBaseline: {
|
|
144
|
+
diffHash: pointer.changeBaseline?.diffHash ?? manifest.diffHash,
|
|
145
|
+
changedPaths: pointer.changeBaseline?.changedPaths ?? manifest.changedPaths,
|
|
146
|
+
leasePaths,
|
|
147
|
+
...(selfReviewRequired ? { selfReviewRequired } : {}),
|
|
148
|
+
},
|
|
150
149
|
});
|
|
151
150
|
}
|
|
152
151
|
async setVerificationIntent(repoFingerprint, input) {
|
|
@@ -399,7 +398,11 @@ export class ActiveContextStore {
|
|
|
399
398
|
pointer.changeBaseline.changedPaths.some((entry) => !isChangedPath(entry)) ||
|
|
400
399
|
!Array.isArray(pointer.changeBaseline.leasePaths) ||
|
|
401
400
|
pointer.changeBaseline.leasePaths.some((path) => !isRepositoryRelative(path)) ||
|
|
402
|
-
new Set(pointer.changeBaseline.leasePaths).size !==
|
|
401
|
+
new Set(pointer.changeBaseline.leasePaths).size !==
|
|
402
|
+
pointer.changeBaseline.leasePaths.length ||
|
|
403
|
+
(pointer.changeBaseline.selfReviewRequired !== undefined &&
|
|
404
|
+
(!Array.isArray(pointer.changeBaseline.selfReviewRequired) ||
|
|
405
|
+
!pointer.changeBaseline.selfReviewRequired.every(isSelfReviewRecord)))) {
|
|
403
406
|
throw new Error('Active Engineering Memory change baseline is invalid');
|
|
404
407
|
}
|
|
405
408
|
}
|
|
@@ -429,6 +432,13 @@ function isDurableIntent(value) {
|
|
|
429
432
|
typeof value.createdAt === 'string' &&
|
|
430
433
|
Number.isFinite(Date.parse(value.createdAt)));
|
|
431
434
|
}
|
|
435
|
+
function isSelfReviewRecord(value) {
|
|
436
|
+
return (typeof value?.resourceId === 'string' &&
|
|
437
|
+
typeof value.resourceKey === 'string' &&
|
|
438
|
+
typeof value.title === 'string' &&
|
|
439
|
+
Array.isArray(value.paths) &&
|
|
440
|
+
value.paths.every((path) => typeof path === 'string'));
|
|
441
|
+
}
|
|
432
442
|
function isChangedPath(value) {
|
|
433
443
|
return (typeof value.path === 'string' &&
|
|
434
444
|
isRepositoryRelative(value.path) &&
|
|
@@ -51,6 +51,7 @@ export const backendRecoveryOperationNames = [
|
|
|
51
51
|
'task.branch',
|
|
52
52
|
'task.resolve_pending_delivery',
|
|
53
53
|
'task.delivery',
|
|
54
|
+
'task.review_request',
|
|
54
55
|
];
|
|
55
56
|
const backendRecoveryOperations = new Set(backendRecoveryOperationNames);
|
|
56
57
|
const browserSigninRecovery = 'auth.signin_browser';
|