engineering-memory 1.7.0 → 1.9.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/dispatcher/sections.mjs +2 -0
- package/install/commands.mjs +39 -10
- package/package.json +1 -1
- package/runtime/dist/src/config.js +9 -1
- package/runtime/dist/src/git/git-inspector.js +140 -17
- package/runtime/dist/src/git/pre-commit.js +16 -0
- package/runtime/dist/src/git/verification-gate.js +14 -1
- package/runtime/dist/src/mcp/tool-definitions.js +101 -5
- package/runtime/dist/src/project/repository.js +1 -1
- package/runtime/dist/src/runtime/active-context-store.js +13 -7
- package/runtime/dist/src/runtime/api-client.js +62 -12
- package/runtime/dist/src/runtime/bridge-service.js +357 -21
- package/runtime/dist/src/runtime/principal-state.js +3 -2
- package/runtime/dist/src/runtime/recovery-error.js +9 -0
- package/runtime/dist/src/runtime/task-branch-store.js +111 -0
- package/skill/SKILL.md +2 -0
- package/skill/references/lifecycle.md +15 -9
- package/skill/references/memory-updates.md +3 -1
- package/skill/references/questionnaires.md +6 -4
- package/skill/references/scaffolding.md +9 -8
- package/bin/entry-point.test.mjs +0 -29
|
@@ -1,4 +1,47 @@
|
|
|
1
1
|
import { sha256, stableStringify } from '../utilities/hash.js';
|
|
2
|
+
export const backendRecoveryOperationNames = [
|
|
3
|
+
'auth.signin_browser',
|
|
4
|
+
'auth.web_csrf',
|
|
5
|
+
'auth.web_signin',
|
|
6
|
+
'audit.list',
|
|
7
|
+
'organization.member_list',
|
|
8
|
+
'organization.member_upsert',
|
|
9
|
+
'project.setup',
|
|
10
|
+
'project.resolve',
|
|
11
|
+
'project.list',
|
|
12
|
+
'project.restore',
|
|
13
|
+
'project.member_list',
|
|
14
|
+
'organization.list',
|
|
15
|
+
'project.member_add',
|
|
16
|
+
'work_item.list',
|
|
17
|
+
'work_item.get',
|
|
18
|
+
'session.bootstrap',
|
|
19
|
+
'session.resume',
|
|
20
|
+
'context.refresh',
|
|
21
|
+
'context.prepare_change',
|
|
22
|
+
'memory.query',
|
|
23
|
+
'memory.history',
|
|
24
|
+
'memory.propose_revision',
|
|
25
|
+
'memory.list_proposals',
|
|
26
|
+
'memory.review_proposal',
|
|
27
|
+
'architecture.plan',
|
|
28
|
+
'architecture.record_application',
|
|
29
|
+
'task.checkpoint',
|
|
30
|
+
'task.reconcile',
|
|
31
|
+
'task.self_review',
|
|
32
|
+
'task.verify',
|
|
33
|
+
'task.close',
|
|
34
|
+
'task.abandon',
|
|
35
|
+
'task.branch',
|
|
36
|
+
'task.resolve_pending_delivery',
|
|
37
|
+
];
|
|
38
|
+
const backendRecoveryOperations = new Set(backendRecoveryOperationNames);
|
|
39
|
+
const browserSigninRecovery = 'auth.signin_browser';
|
|
40
|
+
export function normalizeBackendRecovery(value) {
|
|
41
|
+
return typeof value === 'string' && backendRecoveryOperations.has(value)
|
|
42
|
+
? value
|
|
43
|
+
: null;
|
|
44
|
+
}
|
|
2
45
|
export class BackendUnavailableError extends Error {
|
|
3
46
|
causeValue;
|
|
4
47
|
retryable = true;
|
|
@@ -14,14 +57,17 @@ export class ApiResponseError extends Error {
|
|
|
14
57
|
retryable;
|
|
15
58
|
envelope;
|
|
16
59
|
recovery;
|
|
17
|
-
constructor(message, httpStatus, code, retryable, envelope, recovery
|
|
18
|
-
|
|
60
|
+
constructor(message, httpStatus, code, retryable, envelope, recovery) {
|
|
61
|
+
const normalizedRecovery = normalizeBackendRecovery(recovery) ?? (httpStatus === 401 ? browserSigninRecovery : null);
|
|
62
|
+
super(normalizedRecovery && !message.includes('Recovery: call ')
|
|
63
|
+
? `${message} Recovery: call ${normalizedRecovery}.`
|
|
64
|
+
: message);
|
|
19
65
|
this.httpStatus = httpStatus;
|
|
20
66
|
this.code = code;
|
|
21
67
|
this.retryable = retryable;
|
|
22
68
|
this.envelope = envelope;
|
|
23
|
-
this.recovery = recovery;
|
|
24
69
|
this.name = 'ApiResponseError';
|
|
70
|
+
this.recovery = normalizedRecovery;
|
|
25
71
|
}
|
|
26
72
|
}
|
|
27
73
|
export class ApiClient {
|
|
@@ -53,7 +99,7 @@ export class ApiClient {
|
|
|
53
99
|
if (request.authenticated !== false) {
|
|
54
100
|
const accessToken = await this.options.credentials.get('access-token');
|
|
55
101
|
if (!accessToken) {
|
|
56
|
-
throw new ApiResponseError('Authentication is required', 401, null, false, null);
|
|
102
|
+
throw new ApiResponseError('Authentication is required. Recovery: call auth.signin_browser.', 401, null, false, null, browserSigninRecovery);
|
|
57
103
|
}
|
|
58
104
|
headers.Authorization = `Bearer ${accessToken}`;
|
|
59
105
|
}
|
|
@@ -108,9 +154,9 @@ export class ApiClient {
|
|
|
108
154
|
this.responseSources.set(cached.value, 'stale_cache');
|
|
109
155
|
return cached.value;
|
|
110
156
|
}
|
|
111
|
-
const recovery = envelope.errorModel?.recovery ?? null;
|
|
157
|
+
const recovery = envelope.errorModel?.recovery ?? (response.status === 401 ? browserSigninRecovery : null);
|
|
112
158
|
const refusal = envelope.errorModel?.text ?? envelope.message ?? `Backend returned ${response.status}`;
|
|
113
|
-
throw new ApiResponseError(
|
|
159
|
+
throw new ApiResponseError(refusal, response.status, envelope.errorModel?.code ?? null, retryableResponse(response.status, envelope.errorModel?.retryable), envelope, recovery);
|
|
114
160
|
}
|
|
115
161
|
if (request.cacheKey) {
|
|
116
162
|
await this.options.cache.set(cacheKey, response.headers.get('etag'), envelope);
|
|
@@ -146,7 +192,7 @@ export class ApiClient {
|
|
|
146
192
|
const refreshToken = await this.options.credentials.get('refresh-token');
|
|
147
193
|
if (!refreshToken) {
|
|
148
194
|
await this.options.credentials.clear();
|
|
149
|
-
throw new ApiResponseError('Authentication has expired', 401, null, false, null);
|
|
195
|
+
throw new ApiResponseError('Authentication has expired. Recovery: call auth.signin_browser.', 401, null, false, null, browserSigninRecovery);
|
|
150
196
|
}
|
|
151
197
|
const controller = new AbortController();
|
|
152
198
|
const timeout = setTimeout(() => controller.abort(), this.options.timeoutMs);
|
|
@@ -172,18 +218,19 @@ export class ApiClient {
|
|
|
172
218
|
catch (error) {
|
|
173
219
|
if (isDefinitiveRefreshRejection(response.status)) {
|
|
174
220
|
await this.options.credentials.clear();
|
|
221
|
+
throw new ApiResponseError('Authentication has expired. Recovery: call auth.signin_browser.', response.status, null, false, null, browserSigninRecovery);
|
|
175
222
|
}
|
|
176
223
|
throw error;
|
|
177
224
|
}
|
|
178
225
|
if (!response.ok || envelope.status === 'error') {
|
|
179
226
|
if (isDefinitiveRefreshRejection(response.status)) {
|
|
180
227
|
await this.options.credentials.clear();
|
|
181
|
-
throw new ApiResponseError('Authentication has expired', response.status, null, false, envelope);
|
|
228
|
+
throw new ApiResponseError('Authentication has expired. Recovery: call auth.signin_browser.', response.status, null, false, envelope, browserSigninRecovery);
|
|
182
229
|
}
|
|
183
|
-
throw new ApiResponseError(envelope.errorModel?.text ?? envelope.message ?? 'Token refresh failed', response.status, envelope.errorModel?.code ?? null, response.status
|
|
230
|
+
throw new ApiResponseError(envelope.errorModel?.text ?? envelope.message ?? 'Token refresh failed', response.status, envelope.errorModel?.code ?? null, retryableResponse(response.status, envelope.errorModel?.retryable), envelope, null);
|
|
184
231
|
}
|
|
185
232
|
if (!isRefreshTokens(envelope.data)) {
|
|
186
|
-
throw new ApiResponseError('Token refresh response is missing credentials', response.status, null, false, envelope);
|
|
233
|
+
throw new ApiResponseError('Token refresh response is missing credentials. Recovery: call auth.signin_browser.', response.status, null, false, envelope, browserSigninRecovery);
|
|
187
234
|
}
|
|
188
235
|
await this.options.credentials.set('access-token', envelope.data.accessToken);
|
|
189
236
|
if (envelope.data.refreshToken) {
|
|
@@ -196,10 +243,10 @@ export class ApiClient {
|
|
|
196
243
|
value = await response.json();
|
|
197
244
|
}
|
|
198
245
|
catch {
|
|
199
|
-
throw new ApiResponseError(`Backend returned a non-JSON response with status ${response.status}`, response.status, null, response.status >= 500, null);
|
|
246
|
+
throw new ApiResponseError(`Backend returned a non-JSON response with status ${response.status}`, response.status, null, response.status >= 500, null, null);
|
|
200
247
|
}
|
|
201
248
|
if (!isEnvelope(value)) {
|
|
202
|
-
throw new ApiResponseError('Backend response does not match the Engineering Memory envelope', response.status, null, false, null);
|
|
249
|
+
throw new ApiResponseError('Backend response does not match the Engineering Memory envelope', response.status, null, false, null, null);
|
|
203
250
|
}
|
|
204
251
|
return value;
|
|
205
252
|
}
|
|
@@ -223,6 +270,9 @@ function isEnvelope(value) {
|
|
|
223
270
|
'message' in candidate &&
|
|
224
271
|
'errorModel' in candidate);
|
|
225
272
|
}
|
|
273
|
+
function retryableResponse(status, declared) {
|
|
274
|
+
return declared ?? (status === 408 || status === 429 || status >= 500);
|
|
275
|
+
}
|
|
226
276
|
function isRefreshTokens(value) {
|
|
227
277
|
return (value !== null &&
|
|
228
278
|
!Array.isArray(value) &&
|