engineering-memory 1.6.2 → 1.6.4
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.
|
@@ -69,6 +69,9 @@ async function main(argumentList) {
|
|
|
69
69
|
skillSource,
|
|
70
70
|
'--bridge-runtime-source',
|
|
71
71
|
staged,
|
|
72
|
+
...(rest.includes('--client-version')
|
|
73
|
+
? []
|
|
74
|
+
: ['--client-version', await publishedVersion()]),
|
|
72
75
|
]);
|
|
73
76
|
} finally {
|
|
74
77
|
await rm(staged, { recursive: true, force: true });
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import test from 'node:test';
|
|
4
|
+
|
|
5
|
+
const entryPoint = new URL('./engineering-memory.mjs', import.meta.url);
|
|
6
|
+
const manifest = new URL('../package.json', import.meta.url);
|
|
7
|
+
|
|
8
|
+
test('the published entry point tells the installer which version it is', async () => {
|
|
9
|
+
const source = await readFile(entryPoint, 'utf8');
|
|
10
|
+
|
|
11
|
+
assert.match(
|
|
12
|
+
source,
|
|
13
|
+
/'--client-version',\s*await publishedVersion\(\)/,
|
|
14
|
+
'The installer is never told the version, so every install reports itself as unknown and no update is ever offered',
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('the published entry point tells the installer which backend it was built for', async () => {
|
|
19
|
+
const source = await readFile(entryPoint, 'utf8');
|
|
20
|
+
|
|
21
|
+
assert.match(source, /'--api-url',\s*await publishedApiUrl\(\)/);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('the manifest carries a version the entry point can stamp', async () => {
|
|
25
|
+
const declared = JSON.parse(await readFile(manifest, 'utf8'));
|
|
26
|
+
|
|
27
|
+
assert.match(String(declared.version), /^\d+\.\d+\.\d+$/);
|
|
28
|
+
assert.equal(declared.name, 'engineering-memory');
|
|
29
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engineering-memory",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.4",
|
|
4
4
|
"description": "Installs the Engineering Memory skill and its local MCP bridge. Sign in after installing; your organization and project are resolved from your account.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -91,7 +91,7 @@ const discoverableKindNames = [
|
|
|
91
91
|
];
|
|
92
92
|
const reconciliationEntry = z.object({
|
|
93
93
|
resourceId: z.string().min(1),
|
|
94
|
-
type: z.enum(['approved_revision', 'no_semantic_memory_change']),
|
|
94
|
+
type: z.enum(['approved_revision', 'no_semantic_memory_change', 'scaffold_applied']),
|
|
95
95
|
proposalId: z.string().optional(),
|
|
96
96
|
revisionId: z.string().optional(),
|
|
97
97
|
reason: z.string().optional(),
|
|
@@ -130,6 +130,7 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
130
130
|
externalTaskId: z.string().min(2),
|
|
131
131
|
objective: z.string().min(2),
|
|
132
132
|
taskKind: z.string().min(2),
|
|
133
|
+
workItemKey: z.string().min(1).optional(),
|
|
133
134
|
mode: z.enum(['write', 'read_only', 'scaffold']).optional(),
|
|
134
135
|
knownRevisions: z.record(z.string(), z.number().int().min(0)).optional(),
|
|
135
136
|
}),
|
|
@@ -287,7 +288,9 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
287
288
|
taskId: z.string().min(1),
|
|
288
289
|
entries: z.array(reconciliationEntry).min(1).max(50).optional(),
|
|
289
290
|
resourceId: z.string().min(1).optional(),
|
|
290
|
-
type: z
|
|
291
|
+
type: z
|
|
292
|
+
.enum(['approved_revision', 'no_semantic_memory_change', 'scaffold_applied'])
|
|
293
|
+
.optional(),
|
|
291
294
|
proposalId: z.string().optional(),
|
|
292
295
|
revisionId: z.string().optional(),
|
|
293
296
|
reason: z.string().optional(),
|
|
@@ -326,6 +329,12 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
326
329
|
message: 'No-semantic-change reconciliation requires a reason of at least 8 characters',
|
|
327
330
|
});
|
|
328
331
|
}
|
|
332
|
+
if (entry.type === 'scaffold_applied' && (!entry.reason || entry.reason.length < 8)) {
|
|
333
|
+
context.addIssue({
|
|
334
|
+
code: 'custom',
|
|
335
|
+
message: 'Scaffold-applied reconciliation requires a reason of at least 8 characters',
|
|
336
|
+
});
|
|
337
|
+
}
|
|
329
338
|
}
|
|
330
339
|
}),
|
|
331
340
|
}, async (input) => toolResult(await service.taskReconcile(input)));
|
|
@@ -13,12 +13,14 @@ export class ApiResponseError extends Error {
|
|
|
13
13
|
code;
|
|
14
14
|
retryable;
|
|
15
15
|
envelope;
|
|
16
|
-
|
|
16
|
+
recovery;
|
|
17
|
+
constructor(message, httpStatus, code, retryable, envelope, recovery = null) {
|
|
17
18
|
super(message);
|
|
18
19
|
this.httpStatus = httpStatus;
|
|
19
20
|
this.code = code;
|
|
20
21
|
this.retryable = retryable;
|
|
21
22
|
this.envelope = envelope;
|
|
23
|
+
this.recovery = recovery;
|
|
22
24
|
this.name = 'ApiResponseError';
|
|
23
25
|
}
|
|
24
26
|
}
|
|
@@ -106,7 +108,9 @@ export class ApiClient {
|
|
|
106
108
|
this.responseSources.set(cached.value, 'stale_cache');
|
|
107
109
|
return cached.value;
|
|
108
110
|
}
|
|
109
|
-
|
|
111
|
+
const recovery = envelope.errorModel?.recovery ?? null;
|
|
112
|
+
const refusal = envelope.errorModel?.text ?? envelope.message ?? `Backend returned ${response.status}`;
|
|
113
|
+
throw new ApiResponseError(recovery ? `${refusal} Recovery: call ${recovery}.` : refusal, response.status, envelope.errorModel?.code ?? null, response.status === 408 || response.status === 429 || response.status >= 500, envelope, recovery);
|
|
110
114
|
}
|
|
111
115
|
if (request.cacheKey) {
|
|
112
116
|
await this.options.cache.set(cacheKey, response.headers.get('etag'), envelope);
|
|
@@ -53,6 +53,7 @@ export class BridgeService {
|
|
|
53
53
|
externalTaskId: input.externalTaskId,
|
|
54
54
|
objective: input.objective,
|
|
55
55
|
taskKind: input.taskKind,
|
|
56
|
+
workItemKey: input.workItemKey,
|
|
56
57
|
mode: input.mode,
|
|
57
58
|
knownRevisions: input.knownRevisions,
|
|
58
59
|
}, repository.repoRoot);
|
|
@@ -81,7 +82,7 @@ export class BridgeService {
|
|
|
81
82
|
});
|
|
82
83
|
}
|
|
83
84
|
if (!projectId) {
|
|
84
|
-
throw
|
|
85
|
+
throw refuse('This repository has a marker that names no project, so there is nothing to bootstrap against.', 'project.resolve');
|
|
85
86
|
}
|
|
86
87
|
const checkpointId = deterministicUuid('session.bootstrap', projectId, repository.repoFingerprint, input.externalTaskId);
|
|
87
88
|
const response = await this.dependencies.client.request(endpoints.sessionBootstrap, {
|
|
@@ -92,6 +93,7 @@ export class BridgeService {
|
|
|
92
93
|
externalTaskId: persistedBootstrap.externalTaskId,
|
|
93
94
|
objective: persistedBootstrap.objective,
|
|
94
95
|
taskKind: persistedBootstrap.taskKind,
|
|
96
|
+
workItemKey: persistedBootstrap.workItemKey,
|
|
95
97
|
mode: persistedBootstrap.mode ?? 'write',
|
|
96
98
|
repoFingerprint: repository.repoFingerprint,
|
|
97
99
|
...(input.mode === 'read_only'
|
|
@@ -116,7 +118,7 @@ export class BridgeService {
|
|
|
116
118
|
checkpointEvent.idempotencyKey !== checkpointId ||
|
|
117
119
|
typeof checkpointPayload?.createdAt !== 'string' ||
|
|
118
120
|
!isIsoTimestamp(checkpointPayload.createdAt)) {
|
|
119
|
-
throw
|
|
121
|
+
throw refuse('The backend opened the session without the checkpoint the task is anchored to.', 'session.bootstrap');
|
|
120
122
|
}
|
|
121
123
|
const pointer = {
|
|
122
124
|
repoFingerprint: repository.repoFingerprint,
|
|
@@ -163,9 +165,16 @@ export class BridgeService {
|
|
|
163
165
|
return asJsonValue({ authentication, repository: publicRepository(repository) });
|
|
164
166
|
}
|
|
165
167
|
const live = await this.dependencies.activeContexts.list(repository.repoFingerprint);
|
|
166
|
-
let pointer =
|
|
167
|
-
|
|
168
|
-
|
|
168
|
+
let pointer = null;
|
|
169
|
+
if (input.taskSlug) {
|
|
170
|
+
pointer = live.find((entry) => entry.taskSlug === input.taskSlug) ?? null;
|
|
171
|
+
}
|
|
172
|
+
else if (input.sessionId) {
|
|
173
|
+
pointer = live.find((entry) => entry.sessionId === input.sessionId) ?? null;
|
|
174
|
+
}
|
|
175
|
+
else if (live.length === 1) {
|
|
176
|
+
pointer = live[0];
|
|
177
|
+
}
|
|
169
178
|
if (!pointer && input.taskSlug) {
|
|
170
179
|
return asJsonValue({
|
|
171
180
|
taskChoiceRequired: true,
|
|
@@ -174,7 +183,15 @@ export class BridgeService {
|
|
|
174
183
|
repository: publicRepository(repository),
|
|
175
184
|
});
|
|
176
185
|
}
|
|
177
|
-
if (!pointer &&
|
|
186
|
+
if (!pointer && input.sessionId && live.length > 0) {
|
|
187
|
+
return asJsonValue({
|
|
188
|
+
taskChoiceRequired: true,
|
|
189
|
+
requestedSessionId: input.sessionId,
|
|
190
|
+
liveTasks: live.map(describePointer),
|
|
191
|
+
repository: publicRepository(repository),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (!pointer && live.length > 1) {
|
|
178
195
|
return asJsonValue({
|
|
179
196
|
taskChoiceRequired: true,
|
|
180
197
|
liveTasks: live.map(describePointer),
|
|
@@ -185,14 +202,14 @@ export class BridgeService {
|
|
|
185
202
|
const taskSlug = input.taskSlug ?? pointer?.taskSlug;
|
|
186
203
|
const sessionId = input.sessionId ?? pointer?.sessionId;
|
|
187
204
|
if (!projectId || !taskSlug || !sessionId) {
|
|
188
|
-
throw
|
|
205
|
+
throw refuse('This repository has no live task to resume.', 'session.bootstrap');
|
|
189
206
|
}
|
|
190
207
|
if ((pointer &&
|
|
191
208
|
(pointer.projectId !== projectId ||
|
|
192
209
|
pointer.taskSlug !== taskSlug ||
|
|
193
210
|
pointer.sessionId !== sessionId)) ||
|
|
194
211
|
(repository.projectId && repository.projectId !== projectId)) {
|
|
195
|
-
throw
|
|
212
|
+
throw refuse('The live task belongs to a different project than the one this repository is bound to.', 'project.resolve');
|
|
196
213
|
}
|
|
197
214
|
if (pointer?.closeIntent) {
|
|
198
215
|
const recoveredClose = await this.retryCloseIntent(repository, pointer);
|
|
@@ -230,7 +247,7 @@ export class BridgeService {
|
|
|
230
247
|
typeof backendTask.id !== 'string' ||
|
|
231
248
|
typeof backendSession.id !== 'string' ||
|
|
232
249
|
backendSession.id !== sessionId) {
|
|
233
|
-
throw
|
|
250
|
+
throw refuse('The backend resumed the session without the task it belongs to.', 'session.resume');
|
|
234
251
|
}
|
|
235
252
|
let localJournal = await this.dependencies.journal.load(projectId, taskSlug);
|
|
236
253
|
let pendingOutbox = await this.dependencies.outbox.list();
|
|
@@ -530,11 +547,11 @@ export class BridgeService {
|
|
|
530
547
|
.map((entry) => objectValue(entry))
|
|
531
548
|
.find((entry) => entry?.projectId === input.projectId);
|
|
532
549
|
if (!project) {
|
|
533
|
-
throw
|
|
550
|
+
throw refuse('That project has no repository address recorded, or this account is not a member of it. An organization owner records the address.', 'project.list');
|
|
534
551
|
}
|
|
535
552
|
const result = await this.dependencies.repositories.git.clone(String(project.repositoryUrl), input.targetPath);
|
|
536
553
|
if (!result.cloned) {
|
|
537
|
-
throw
|
|
554
|
+
throw refuse(`The repository could not be cloned: ${result.reason}. Engineering Memory never handles Git credentials, so this is between the developer's own Git configuration and the host.`, 'project.clone');
|
|
538
555
|
}
|
|
539
556
|
return asJsonValue({
|
|
540
557
|
cloned: true,
|
|
@@ -819,7 +836,7 @@ export class BridgeService {
|
|
|
819
836
|
return await this.execute(async () => {
|
|
820
837
|
const entry = await this.dependencies.outbox.get(input.outboxId);
|
|
821
838
|
if (!entry) {
|
|
822
|
-
throw
|
|
839
|
+
throw refuse('There is no pending delivery under that identifier.', 'session.resume');
|
|
823
840
|
}
|
|
824
841
|
const metadata = {
|
|
825
842
|
id: entry.id,
|
|
@@ -851,7 +868,7 @@ export class BridgeService {
|
|
|
851
868
|
return asJsonValue({ discarded: true, taskId, pendingDelivery: metadata });
|
|
852
869
|
}
|
|
853
870
|
if (!entry.lastError?.startsWith('api_409_')) {
|
|
854
|
-
throw
|
|
871
|
+
throw refuse('This delivery did not fail on a version conflict, so rebasing it is not the repair.', 'session.resume');
|
|
855
872
|
}
|
|
856
873
|
if (entry.journalRef) {
|
|
857
874
|
const taskSnapshot = await this.refreshTaskPointer(taskId, entry.journalRef.projectId);
|
|
@@ -868,7 +885,7 @@ export class BridgeService {
|
|
|
868
885
|
});
|
|
869
886
|
}
|
|
870
887
|
if (entry.operation !== 'memory.propose_revision' || !taskId) {
|
|
871
|
-
throw
|
|
888
|
+
throw refuse('This delivery cannot be rebased safely; discard it or retry it as it stands.', 'task.resolve_pending_delivery');
|
|
872
889
|
}
|
|
873
890
|
if (!input.proposalRebaseApproved || input.confirmedBaseRevision === undefined) {
|
|
874
891
|
return asJsonValue({
|
|
@@ -882,7 +899,7 @@ export class BridgeService {
|
|
|
882
899
|
const projectId = typeof proposal?.projectId === 'string' ? proposal.projectId : null;
|
|
883
900
|
const queuedBaseRevision = proposal?.baseRevision;
|
|
884
901
|
if (!projectId || typeof queuedBaseRevision !== 'number') {
|
|
885
|
-
throw
|
|
902
|
+
throw refuse('The queued proposal does not carry the revision it was written against.', 'memory.propose_revision');
|
|
886
903
|
}
|
|
887
904
|
if (queuedBaseRevision !== input.confirmedBaseRevision) {
|
|
888
905
|
return asJsonValue({
|
|
@@ -903,7 +920,7 @@ export class BridgeService {
|
|
|
903
920
|
const refreshData = objectValue(refreshed.data);
|
|
904
921
|
const refreshedTask = objectValue(refreshData?.task);
|
|
905
922
|
if (!refreshedTask || refreshedTask.id !== taskId) {
|
|
906
|
-
throw
|
|
923
|
+
throw refuse('The refresh did not return the task this queued proposal belongs to.', 'session.resume');
|
|
907
924
|
}
|
|
908
925
|
const expectedTaskVersion = numericTaskVersion(refreshedTask.lockVersion);
|
|
909
926
|
const rebasedBody = cleanJson({ ...proposal, expectedTaskVersion });
|
|
@@ -1174,7 +1191,7 @@ export class BridgeService {
|
|
|
1174
1191
|
initialProjectProfile: input.initialProjectProfile,
|
|
1175
1192
|
});
|
|
1176
1193
|
if ('resourceDiscovery' in input.initialProjectProfile.metadata) {
|
|
1177
|
-
throw
|
|
1194
|
+
throw refuse('Project setup metadata cannot override canonical resource discovery', 'project.setup');
|
|
1178
1195
|
}
|
|
1179
1196
|
const persistentBody = normalizeRepositoryPaths(body, repository.repoRoot);
|
|
1180
1197
|
assertSafeToPersist(persistentBody);
|
|
@@ -1197,7 +1214,7 @@ export class BridgeService {
|
|
|
1197
1214
|
!marker ||
|
|
1198
1215
|
marker.projectId !== project.id ||
|
|
1199
1216
|
readDiscoveryUnits(policy).length === 0) {
|
|
1200
|
-
throw
|
|
1217
|
+
throw refuse('Project setup response is not policy-ready', 'project.setup');
|
|
1201
1218
|
}
|
|
1202
1219
|
const markerPath = await this.dependencies.repositories.writeMarker(repository.repoRoot, project.id);
|
|
1203
1220
|
return asJsonValue({ ...data, markerPath });
|
|
@@ -1257,7 +1274,7 @@ export class BridgeService {
|
|
|
1257
1274
|
const files = await Promise.all(input.files.map(async (file) => {
|
|
1258
1275
|
const hash = await hashWorkingTreeFile(repository.repoRoot, file.path);
|
|
1259
1276
|
if (!hash) {
|
|
1260
|
-
throw
|
|
1277
|
+
throw refuse(`Applied template file is not readable: ${file.path}`, 'architecture.record_application');
|
|
1261
1278
|
}
|
|
1262
1279
|
return { templatePath: file.templatePath, path: file.path, sha256: hash };
|
|
1263
1280
|
}));
|
|
@@ -1317,7 +1334,7 @@ export class BridgeService {
|
|
|
1317
1334
|
});
|
|
1318
1335
|
const project = objectValue(response.data);
|
|
1319
1336
|
if (!project || project.id !== input.projectId) {
|
|
1320
|
-
throw
|
|
1337
|
+
throw refuse('Project bind response does not match the selected project', 'project.resolve');
|
|
1321
1338
|
}
|
|
1322
1339
|
const markerPath = await this.dependencies.repositories.writeMarker(repository.repoRoot, input.projectId);
|
|
1323
1340
|
return asJsonValue({
|
|
@@ -1574,7 +1591,7 @@ export class BridgeService {
|
|
|
1574
1591
|
(entry.operation === 'task.checkpoint' || entry.operation === 'task.record_correction'));
|
|
1575
1592
|
const blockedEntry = taskEntries.find((entry) => entry.lastError && entry.lastError !== 'backend_unavailable');
|
|
1576
1593
|
if (blockedEntry && blockedEntry.idempotencyKey !== idempotencyKey) {
|
|
1577
|
-
throw
|
|
1594
|
+
throw refuse('A blocked task delivery must be explicitly resolved before checkpointing', 'task.resolve_pending_delivery');
|
|
1578
1595
|
}
|
|
1579
1596
|
const existingExpected = pendingJournal
|
|
1580
1597
|
? expectedTaskVersionFromBody(pendingJournal.body)
|
|
@@ -1590,7 +1607,7 @@ export class BridgeService {
|
|
|
1590
1607
|
correction,
|
|
1591
1608
|
});
|
|
1592
1609
|
if (!staged.delivery) {
|
|
1593
|
-
throw
|
|
1610
|
+
throw refuse('Task journal delivery was not materialized', 'task.resolve_pending_delivery');
|
|
1594
1611
|
}
|
|
1595
1612
|
const queued = await this.dependencies.outbox.enqueue({
|
|
1596
1613
|
operation: staged.delivery.operation,
|
|
@@ -1745,10 +1762,10 @@ export class BridgeService {
|
|
|
1745
1762
|
const pointer = await this.requireActivePointer(repository.repoFingerprint, taskId);
|
|
1746
1763
|
if ((projectId && pointer.projectId !== projectId) ||
|
|
1747
1764
|
(repository.projectId && pointer.projectId !== repository.projectId)) {
|
|
1748
|
-
throw
|
|
1765
|
+
throw refuse('Active task snapshot does not match the repository project', 'session.resume');
|
|
1749
1766
|
}
|
|
1750
1767
|
if ((await this.dependencies.outbox.list()).some((entry) => taskIdFromDeliverySafe(entry) === taskId)) {
|
|
1751
|
-
throw
|
|
1768
|
+
throw refuse('Pending task deliveries must be resolved before using the task snapshot', 'task.resolve_pending_delivery');
|
|
1752
1769
|
}
|
|
1753
1770
|
const response = await this.dependencies.client.request(endpoints.sessionResume, {
|
|
1754
1771
|
method: 'POST',
|
|
@@ -1761,7 +1778,7 @@ export class BridgeService {
|
|
|
1761
1778
|
const backend = objectValue(response.data);
|
|
1762
1779
|
const task = objectValue(backend?.task);
|
|
1763
1780
|
if (!task || task.id !== taskId || backend?.requiresContextRefresh === true) {
|
|
1764
|
-
throw
|
|
1781
|
+
throw refuse('Backend task snapshot is stale or does not match the active task', 'session.resume');
|
|
1765
1782
|
}
|
|
1766
1783
|
const refreshed = {
|
|
1767
1784
|
...pointer,
|
|
@@ -1784,7 +1801,7 @@ export class BridgeService {
|
|
|
1784
1801
|
const backend = objectValue(response.data);
|
|
1785
1802
|
const task = objectValue(backend?.task);
|
|
1786
1803
|
if (!task || task.id !== taskId || backend?.requiresContextRefresh === true) {
|
|
1787
|
-
throw
|
|
1804
|
+
throw refuse('Backend task snapshot is stale or does not match the pending delivery', 'session.resume');
|
|
1788
1805
|
}
|
|
1789
1806
|
const refreshed = {
|
|
1790
1807
|
...pointer,
|
|
@@ -1803,7 +1820,7 @@ export class BridgeService {
|
|
|
1803
1820
|
pointer.taskId !== taskId ||
|
|
1804
1821
|
(blockingConflicts?.length ?? 0) > 0 ||
|
|
1805
1822
|
lifecycleIntentBlocked) {
|
|
1806
|
-
throw
|
|
1823
|
+
throw refuse('A clean active task snapshot is required for this operation', 'session.resume');
|
|
1807
1824
|
}
|
|
1808
1825
|
return pointer;
|
|
1809
1826
|
}
|
|
@@ -1832,7 +1849,7 @@ export class BridgeService {
|
|
|
1832
1849
|
backendTask.projectId !== pointer.projectId ||
|
|
1833
1850
|
backendSession?.id !== pointer.sessionId ||
|
|
1834
1851
|
body.pendingOutboxCount !== 0) {
|
|
1835
|
-
throw
|
|
1852
|
+
throw refuse('Verification recovery does not match the durable verification intent', 'session.resume');
|
|
1836
1853
|
}
|
|
1837
1854
|
if (recovery) {
|
|
1838
1855
|
const recoveryTaskVersion = numericTaskVersion(recovery.taskVersion);
|
|
@@ -1840,12 +1857,12 @@ export class BridgeService {
|
|
|
1840
1857
|
recovery.mode !== intent.mode ||
|
|
1841
1858
|
(recoveryTaskVersion !== expectedTaskVersion &&
|
|
1842
1859
|
recoveryTaskVersion !== expectedTaskVersion + 1)) {
|
|
1843
|
-
throw
|
|
1860
|
+
throw refuse('Verification recovery does not match the verified backend task', 'session.resume');
|
|
1844
1861
|
}
|
|
1845
1862
|
}
|
|
1846
1863
|
else if (numericTaskVersion(backendTask.lockVersion) !== expectedTaskVersion ||
|
|
1847
1864
|
normalizeTaskMode(backendTask.mode) !== intent.mode) {
|
|
1848
|
-
throw
|
|
1865
|
+
throw refuse('Open backend task does not match the durable verification intent', 'session.resume');
|
|
1849
1866
|
}
|
|
1850
1867
|
const currentTaskChanges = intent.mode !== 'read_only'
|
|
1851
1868
|
? pointer.changeBaseline
|
|
@@ -1853,7 +1870,7 @@ export class BridgeService {
|
|
|
1853
1870
|
: null
|
|
1854
1871
|
: [];
|
|
1855
1872
|
if (!currentTaskChanges) {
|
|
1856
|
-
throw
|
|
1873
|
+
throw refuse('Verification recovery is missing its locally pinned baseline', 'session.resume');
|
|
1857
1874
|
}
|
|
1858
1875
|
const currentChangedPaths = currentTaskChanges.map((entry) => entry.path).sort();
|
|
1859
1876
|
const currentPathChanges = intent.mode === 'read_only'
|
|
@@ -1863,7 +1880,7 @@ export class BridgeService {
|
|
|
1863
1880
|
stableStringify(body.pathChanges ?? null) !== stableStringify(currentPathChanges) ||
|
|
1864
1881
|
stableStringify(intent.taskChanges.map(changedPathIdentity)) !==
|
|
1865
1882
|
stableStringify(currentTaskChanges.map(changedPathIdentity))) {
|
|
1866
|
-
throw
|
|
1883
|
+
throw refuse('Current Git state does not match the durable verification intent', 'session.resume');
|
|
1867
1884
|
}
|
|
1868
1885
|
if (intent.mode !== 'read_only') {
|
|
1869
1886
|
const retryLease = recovery
|
|
@@ -1879,13 +1896,13 @@ export class BridgeService {
|
|
|
1879
1896
|
retryLease.baselineDiffHash !== body.baselineDiffHash ||
|
|
1880
1897
|
body.baselineDiffHash !== pointer.changeBaseline?.diffHash ||
|
|
1881
1898
|
!pathsContainAll(retryLeasePaths, currentChangedPaths)) {
|
|
1882
|
-
throw
|
|
1899
|
+
throw refuse('Backend lease does not match the durable verification intent', 'context.prepare_change');
|
|
1883
1900
|
}
|
|
1884
1901
|
}
|
|
1885
1902
|
else if (currentChangedPaths.length !== 0 ||
|
|
1886
1903
|
body.leaseId !== undefined ||
|
|
1887
1904
|
body.baselineDiffHash !== undefined) {
|
|
1888
|
-
throw
|
|
1905
|
+
throw refuse('Read-only verification recovery contains a write lease', 'task.verify');
|
|
1889
1906
|
}
|
|
1890
1907
|
let response;
|
|
1891
1908
|
try {
|
|
@@ -1903,7 +1920,7 @@ export class BridgeService {
|
|
|
1903
1920
|
const responseData = objectValue(response.data);
|
|
1904
1921
|
if (responseData?.verified !== true || responseData.diffHash !== body.diffHash) {
|
|
1905
1922
|
await this.dependencies.activeContexts.clearVerificationIntent(repository.repoFingerprint, pointer.taskId);
|
|
1906
|
-
throw
|
|
1923
|
+
throw refuse('Verification retry did not confirm the durable verification intent', 'session.resume');
|
|
1907
1924
|
}
|
|
1908
1925
|
const taskVersion = numericTaskVersion(responseData.taskVersion);
|
|
1909
1926
|
await this.dependencies.gate.record({
|
|
@@ -1922,7 +1939,7 @@ export class BridgeService {
|
|
|
1922
1939
|
async retryCloseIntent(repository, pointer) {
|
|
1923
1940
|
const intent = pointer.closeIntent;
|
|
1924
1941
|
if (!intent) {
|
|
1925
|
-
throw
|
|
1942
|
+
throw refuse('Durable close intent is unavailable', 'session.resume');
|
|
1926
1943
|
}
|
|
1927
1944
|
const body = intent.body;
|
|
1928
1945
|
const currentTaskChanges = pointer.changeBaseline
|
|
@@ -1934,7 +1951,7 @@ export class BridgeService {
|
|
|
1934
1951
|
pointer.sessionId !== body.sessionId ||
|
|
1935
1952
|
stableStringify(intent.taskChanges.map(changedPathIdentity)) !==
|
|
1936
1953
|
stableStringify(currentTaskChanges.map(changedPathIdentity))) {
|
|
1937
|
-
throw
|
|
1954
|
+
throw refuse('Current state does not match the durable close intent', 'session.resume');
|
|
1938
1955
|
}
|
|
1939
1956
|
return await this.deliverCloseIntent(repository, body, intent.taskChanges, true);
|
|
1940
1957
|
}
|
|
@@ -1956,7 +1973,7 @@ export class BridgeService {
|
|
|
1956
1973
|
const closedTask = objectValue(response.data);
|
|
1957
1974
|
if (closedTask?.closed !== true) {
|
|
1958
1975
|
await this.dependencies.activeContexts.clearCloseIntent(repository.repoFingerprint, String(body.taskId));
|
|
1959
|
-
throw
|
|
1976
|
+
throw refuse('Task close response does not confirm task closure', 'session.resume');
|
|
1960
1977
|
}
|
|
1961
1978
|
const closedTaskVersion = numericTaskVersion(closedTask.taskVersion);
|
|
1962
1979
|
await this.dependencies.gate.record({
|
|
@@ -2025,13 +2042,13 @@ export class BridgeService {
|
|
|
2025
2042
|
})()
|
|
2026
2043
|
: await this.dependencies.activeContexts.findByTaskId(input.taskId, input.projectId, false);
|
|
2027
2044
|
if (pointer.resumeConflicts.some((conflict) => !isOfflineDevelopmentWarning(conflict))) {
|
|
2028
|
-
throw
|
|
2045
|
+
throw refuse('A clean active task snapshot is required for checkpointing', 'session.resume');
|
|
2029
2046
|
}
|
|
2030
2047
|
if (pointer.verificationIntent || pointer.closeIntent) {
|
|
2031
|
-
throw
|
|
2048
|
+
throw refuse('A clean active task snapshot is required for checkpointing', 'session.resume');
|
|
2032
2049
|
}
|
|
2033
2050
|
if (pointer.taskSlug.toLowerCase() !== input.taskSlug.toLowerCase()) {
|
|
2034
|
-
throw
|
|
2051
|
+
throw refuse(`Checkpoint task slug ${input.taskSlug} does not match the active task ${pointer.taskSlug}`, 'session.resume');
|
|
2035
2052
|
}
|
|
2036
2053
|
return pointer;
|
|
2037
2054
|
}
|
|
@@ -2080,6 +2097,7 @@ export class BridgeService {
|
|
|
2080
2097
|
message: error.message,
|
|
2081
2098
|
code: error.code ?? error.httpStatus,
|
|
2082
2099
|
retryable: error.retryable,
|
|
2100
|
+
...(error.recovery ? { recovery: error.recovery } : {}),
|
|
2083
2101
|
},
|
|
2084
2102
|
};
|
|
2085
2103
|
}
|
|
@@ -2102,7 +2120,7 @@ function normalizeChangedPaths(paths) {
|
|
|
2102
2120
|
path.startsWith('/') ||
|
|
2103
2121
|
/^[A-Za-z]:\//.test(path) ||
|
|
2104
2122
|
path.split('/').includes('..')) {
|
|
2105
|
-
throw
|
|
2123
|
+
throw refuse(`Changed path must be repository-relative: ${path}`, 'context.prepare_change');
|
|
2106
2124
|
}
|
|
2107
2125
|
}
|
|
2108
2126
|
return [...new Set(normalized)].sort();
|
|
@@ -2157,7 +2175,7 @@ function semanticGitStatus(status) {
|
|
|
2157
2175
|
function semanticPathStatus(status) {
|
|
2158
2176
|
const semantic = semanticGitStatus(status);
|
|
2159
2177
|
if (!['A', 'R', 'C', 'M', 'D', 'T', 'U'].includes(semantic)) {
|
|
2160
|
-
throw
|
|
2178
|
+
throw refuse(`Unsupported semantic Git status: ${status}`, 'task.verify');
|
|
2161
2179
|
}
|
|
2162
2180
|
return semantic;
|
|
2163
2181
|
}
|
|
@@ -2327,6 +2345,7 @@ function publicError(error) {
|
|
|
2327
2345
|
httpStatus: error.httpStatus,
|
|
2328
2346
|
code: error.code,
|
|
2329
2347
|
retryable: error.retryable,
|
|
2348
|
+
...(error.recovery ? { recovery: error.recovery } : {}),
|
|
2330
2349
|
});
|
|
2331
2350
|
}
|
|
2332
2351
|
return asJsonValue({
|
|
@@ -2341,7 +2360,7 @@ function isBackendUnavailableError(error) {
|
|
|
2341
2360
|
function numericSequence(value) {
|
|
2342
2361
|
const sequence = typeof value === 'number' ? value : Number(value ?? 0);
|
|
2343
2362
|
if (!Number.isSafeInteger(sequence) || sequence < 0) {
|
|
2344
|
-
throw
|
|
2363
|
+
throw refuse('Backend task sequence is invalid', 'session.resume');
|
|
2345
2364
|
}
|
|
2346
2365
|
return sequence;
|
|
2347
2366
|
}
|
|
@@ -2352,7 +2371,7 @@ function isIsoTimestamp(value) {
|
|
|
2352
2371
|
function numericTaskVersion(value) {
|
|
2353
2372
|
const version = typeof value === 'number' ? value : Number(value);
|
|
2354
2373
|
if (!Number.isSafeInteger(version) || version < 1) {
|
|
2355
|
-
throw
|
|
2374
|
+
throw refuse('Backend task version is invalid', 'session.resume');
|
|
2356
2375
|
}
|
|
2357
2376
|
return version;
|
|
2358
2377
|
}
|
|
@@ -2370,7 +2389,7 @@ function validateOfflineLease(value, sessionId, changedPaths, baselineDiffHash)
|
|
|
2370
2389
|
stableStringify(normalizeChangedPaths(leasePaths)) !== stableStringify(changedPaths) ||
|
|
2371
2390
|
!Number.isFinite(expiresAt) ||
|
|
2372
2391
|
expiresAt <= Date.now()) {
|
|
2373
|
-
throw
|
|
2392
|
+
throw refuse('Cached change lease is missing, expired or does not match the current Git state', 'context.prepare_change');
|
|
2374
2393
|
}
|
|
2375
2394
|
}
|
|
2376
2395
|
function isOfflineLeaseUsable(lease, sessionId, changedPaths, baselineDiffHash) {
|
|
@@ -2435,7 +2454,7 @@ function isOfflineDevelopmentWarning(value) {
|
|
|
2435
2454
|
function taskIdFromDelivery(value) {
|
|
2436
2455
|
const body = objectValue(value);
|
|
2437
2456
|
if (!body || typeof body.taskId !== 'string') {
|
|
2438
|
-
throw
|
|
2457
|
+
throw refuse('Pending task delivery does not contain a task identifier', 'task.resolve_pending_delivery');
|
|
2439
2458
|
}
|
|
2440
2459
|
return body.taskId;
|
|
2441
2460
|
}
|
|
@@ -2496,7 +2515,7 @@ export function newMemoryResourceCandidate(entry, policy) {
|
|
|
2496
2515
|
if (policy) {
|
|
2497
2516
|
const matched = policy.units.filter((unit) => unit.patterns.some((pattern) => globMatches(entry.path, pattern)));
|
|
2498
2517
|
if (matched.length > 1) {
|
|
2499
|
-
throw
|
|
2518
|
+
throw refuse(`Resource discovery policy is ambiguous for path: ${entry.path}`, 'memory.propose_revision');
|
|
2500
2519
|
}
|
|
2501
2520
|
return matched[0] ? [{ path: entry.path, kind: matched[0].kind }] : [];
|
|
2502
2521
|
}
|
|
@@ -2550,7 +2569,7 @@ function readResourceDiscoveryPolicy(snapshot, activeLease) {
|
|
|
2550
2569
|
}
|
|
2551
2570
|
const units = readDiscoveryUnits(discovery);
|
|
2552
2571
|
if (units.length === 0) {
|
|
2553
|
-
throw
|
|
2572
|
+
throw refuse('Pinned resource discovery policy is incomplete', 'context.refresh');
|
|
2554
2573
|
}
|
|
2555
2574
|
return { units };
|
|
2556
2575
|
}
|
|
@@ -2587,7 +2606,7 @@ function globMatches(path, pattern) {
|
|
|
2587
2606
|
normalizedPattern.startsWith('/') ||
|
|
2588
2607
|
/^[A-Za-z]:\//.test(normalizedPattern) ||
|
|
2589
2608
|
normalizedPattern.split('/').includes('..')) {
|
|
2590
|
-
throw
|
|
2609
|
+
throw refuse(`Resource discovery pattern is unsafe: ${pattern}`, 'memory.propose_revision');
|
|
2591
2610
|
}
|
|
2592
2611
|
return minimatch(path.replace(/\\/g, '/'), normalizedPattern, { dot: true });
|
|
2593
2612
|
}
|
|
@@ -2597,10 +2616,10 @@ function assertNewResourceEvidence(candidates, evidence) {
|
|
|
2597
2616
|
path: normalizeChangedPaths([entry.path])[0],
|
|
2598
2617
|
}));
|
|
2599
2618
|
if (normalizedEvidence.some((entry) => entry.resourceKey.trim().length < 2)) {
|
|
2600
|
-
throw
|
|
2619
|
+
throw refuse('New resource evidence requires a memory resource key', 'memory.propose_revision');
|
|
2601
2620
|
}
|
|
2602
2621
|
if (new Set(normalizedEvidence.map((entry) => entry.path)).size !== normalizedEvidence.length) {
|
|
2603
|
-
throw
|
|
2622
|
+
throw refuse('New resource evidence contains duplicate paths', 'task.verify');
|
|
2604
2623
|
}
|
|
2605
2624
|
const expected = [...candidates].sort((left, right) => left.path.localeCompare(right.path));
|
|
2606
2625
|
const actual = normalizedEvidence
|
|
@@ -2619,27 +2638,27 @@ function assertNewResourceEvidence(candidates, evidence) {
|
|
|
2619
2638
|
missing.length ? `missing evidence for ${missing.join(', ')}` : '',
|
|
2620
2639
|
extra.length ? `evidence for paths the policy does not classify: ${extra.join(', ')}` : '',
|
|
2621
2640
|
].filter(Boolean);
|
|
2622
|
-
throw
|
|
2641
|
+
throw refuse(`New screen or shared component paths require exact memory reconciliation. ${details.length ? details.join('; ') : 'The kinds do not match the project discovery policy.'}`, 'task.verify');
|
|
2623
2642
|
}
|
|
2624
2643
|
}
|
|
2625
2644
|
function assertValidationEvidence(validations) {
|
|
2626
2645
|
if (validations.length === 0) {
|
|
2627
|
-
throw
|
|
2646
|
+
throw refuse('Validation evidence is required', 'task.verify');
|
|
2628
2647
|
}
|
|
2629
2648
|
const allowed = new Set(validationIds);
|
|
2630
2649
|
const seen = new Set();
|
|
2631
2650
|
for (const validation of validations) {
|
|
2632
2651
|
if (!allowed.has(validation.validationId)) {
|
|
2633
|
-
throw
|
|
2652
|
+
throw refuse(`Unknown validation evidence: ${validation.validationId}`, 'task.verify');
|
|
2634
2653
|
}
|
|
2635
2654
|
if (seen.has(validation.validationId)) {
|
|
2636
|
-
throw
|
|
2655
|
+
throw refuse(`Duplicate validation evidence: ${validation.validationId}`, 'task.verify');
|
|
2637
2656
|
}
|
|
2638
2657
|
if (!validation.command.trim()) {
|
|
2639
|
-
throw
|
|
2658
|
+
throw refuse(`Validation command is required: ${validation.validationId}`, 'task.verify');
|
|
2640
2659
|
}
|
|
2641
2660
|
if (!/^[0-9a-f]{64}$/.test(validation.outputHash)) {
|
|
2642
|
-
throw
|
|
2661
|
+
throw refuse(`Validation output hash is invalid: ${validation.validationId}`, 'task.verify');
|
|
2643
2662
|
}
|
|
2644
2663
|
seen.add(validation.validationId);
|
|
2645
2664
|
}
|