engineering-memory 1.6.1 → 1.6.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engineering-memory",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.2",
|
|
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",
|
|
@@ -390,7 +390,7 @@ export class BridgeService {
|
|
|
390
390
|
const pendingOutbox = await this.dependencies.outbox.list();
|
|
391
391
|
const pointer = await this.dependencies.activeContexts.load(repository.repoFingerprint);
|
|
392
392
|
if (pointer?.verificationIntent || pointer?.closeIntent) {
|
|
393
|
-
throw
|
|
393
|
+
throw refuse('A verification or close intent from an earlier session is still unsettled, so a new lease would be taken against the wrong state.', 'session.resume');
|
|
394
394
|
}
|
|
395
395
|
const conflicts = [
|
|
396
396
|
...(pointer?.resumeConflicts ?? []),
|
|
@@ -422,7 +422,7 @@ export class BridgeService {
|
|
|
422
422
|
let transitionTaskVersion;
|
|
423
423
|
if (input.transitionToWrite) {
|
|
424
424
|
if (!pointer || pointer.sessionId !== input.sessionId) {
|
|
425
|
-
throw
|
|
425
|
+
throw refuse('A read-only task can only become a write task from the session that owns it.', 'session.resume');
|
|
426
426
|
}
|
|
427
427
|
const refreshed = await this.activeTaskSnapshot(pointer.taskId, pointer.projectId, repository.repoRoot);
|
|
428
428
|
transitionTaskVersion = refreshed.taskVersion;
|
|
@@ -976,7 +976,7 @@ export class BridgeService {
|
|
|
976
976
|
const snapshotTask = objectValue(snapshot?.task);
|
|
977
977
|
const activeLease = objectValue(snapshot?.activeLease);
|
|
978
978
|
if (!snapshotTask || snapshotTask.id !== input.taskId) {
|
|
979
|
-
throw
|
|
979
|
+
throw refuse('The backend is holding a different task than the one being verified.', 'session.resume');
|
|
980
980
|
}
|
|
981
981
|
const recoveredVerification = await this.retryVerificationRecovery(repository, pointer, snapshot);
|
|
982
982
|
if (recoveredVerification) {
|
|
@@ -990,7 +990,7 @@ export class BridgeService {
|
|
|
990
990
|
const validations = normalizePersistentInput(input.validations, repository.repoRoot);
|
|
991
991
|
if (taskMode === 'read_only') {
|
|
992
992
|
if (input.leaseId || input.changedPaths?.length) {
|
|
993
|
-
throw
|
|
993
|
+
throw refuse('This task is read-only, so it has no lease and no changed paths to report.', 'task.verify');
|
|
994
994
|
}
|
|
995
995
|
}
|
|
996
996
|
else {
|
|
@@ -998,8 +998,11 @@ export class BridgeService {
|
|
|
998
998
|
const leaseChangedPaths = Array.isArray(activeLease?.changedPaths)
|
|
999
999
|
? activeLease.changedPaths.filter((path) => typeof path === 'string')
|
|
1000
1000
|
: [];
|
|
1001
|
-
if (!baseline
|
|
1002
|
-
throw
|
|
1001
|
+
if (!baseline) {
|
|
1002
|
+
throw refuse('This task has no local change baseline, so what it changed cannot be measured. The baseline is restored from the task record.', 'session.resume');
|
|
1003
|
+
}
|
|
1004
|
+
if (activeLease?.baselineDiffHash !== baseline.diffHash) {
|
|
1005
|
+
throw refuse(`The active lease was taken against baseline ${short(activeLease?.baselineDiffHash)} but this task started from ${short(baseline.diffHash)}, so the lease must be taken again against the task's own baseline.`, 'context.prepare_change');
|
|
1003
1006
|
}
|
|
1004
1007
|
const wholeTreeDelta = manifestDelta(baseline.changedPaths, repository.git.changedPaths);
|
|
1005
1008
|
const leased = new Set(normalizeChangedPaths([...baseline.leasePaths, ...leaseChangedPaths]));
|
|
@@ -1012,15 +1015,18 @@ export class BridgeService {
|
|
|
1012
1015
|
if (input.changedPaths &&
|
|
1013
1016
|
stableStringify(normalizeChangedPaths(input.changedPaths)) !==
|
|
1014
1017
|
stableStringify(taskChangedPaths)) {
|
|
1015
|
-
throw
|
|
1018
|
+
throw refuse(`Reported changed paths do not match the actual Git manifest. ${describePathDisagreement(normalizeChangedPaths(input.changedPaths), taskChangedPaths)} The manifest is the difference between the tree this task started from and the tree now; report exactly what it says. A manifest that looks short means the baseline has moved, and resuming repairs it.`, 'session.resume');
|
|
1019
|
+
}
|
|
1020
|
+
if (taskChangedPaths.length === 0) {
|
|
1021
|
+
throw refuse('The manifest shows this task changed nothing. Either the work is not in the working tree, or the baseline has moved to equal it.', 'session.resume');
|
|
1016
1022
|
}
|
|
1017
|
-
if (
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
!pathsContainAll(
|
|
1022
|
-
|
|
1023
|
-
throw
|
|
1023
|
+
if (!activeLease || !input.leaseId || activeLease.id !== input.leaseId) {
|
|
1024
|
+
throw refuse(`Verification needs the lease the backend currently holds. It has ${short(activeLease?.id)} and you sent ${short(input.leaseId)}.`, 'context.prepare_change');
|
|
1025
|
+
}
|
|
1026
|
+
const unleased = taskChangedPaths.filter((path) => !pathsContainAll(baseline.leasePaths, [path]) ||
|
|
1027
|
+
!pathsContainAll(leaseChangedPaths, [path]));
|
|
1028
|
+
if (unleased.length > 0) {
|
|
1029
|
+
throw refuse(`These changed paths are outside the current lease: ${unleased.slice(0, 5).join(', ')}${unleased.length > 5 ? `, and ${unleased.length - 5} more` : ''}. The lease must cover every path this task changed.`, 'context.prepare_change');
|
|
1024
1030
|
}
|
|
1025
1031
|
assertNewResourceEvidence(taskChanges.flatMap((entry) => newMemoryResourceCandidate(entry, resourceDiscoveryPolicy)), input.newResources ?? []);
|
|
1026
1032
|
}
|
|
@@ -1113,7 +1119,7 @@ export class BridgeService {
|
|
|
1113
1119
|
const snapshot = objectValue(snapshotResponse.data);
|
|
1114
1120
|
const task = objectValue(snapshot?.task);
|
|
1115
1121
|
if (!task || task.id !== input.taskId) {
|
|
1116
|
-
throw
|
|
1122
|
+
throw refuse('The backend is holding a different task than the one being closed.', 'session.resume');
|
|
1117
1123
|
}
|
|
1118
1124
|
const taskChanges = pointer.changeBaseline
|
|
1119
1125
|
? manifestDelta(pointer.changeBaseline.changedPaths, repository.git.changedPaths)
|
|
@@ -1142,7 +1148,7 @@ export class BridgeService {
|
|
|
1142
1148
|
});
|
|
1143
1149
|
const abandoned = objectValue(response.data);
|
|
1144
1150
|
if (!abandoned || abandoned.abandoned !== true) {
|
|
1145
|
-
throw
|
|
1151
|
+
throw refuse('The backend did not record the abandonment, so the task is still open.', 'session.resume');
|
|
1146
1152
|
}
|
|
1147
1153
|
const forgotten = await this.dependencies.activeContexts.forget(repository.repoFingerprint, input.taskId);
|
|
1148
1154
|
await this.dependencies.gate.invalidateTask(input.taskId);
|
|
@@ -2083,6 +2089,7 @@ export class BridgeService {
|
|
|
2083
2089
|
kind: 'bridge_error',
|
|
2084
2090
|
message: error instanceof Error ? error.message : 'Bridge operation failed',
|
|
2085
2091
|
retryable: false,
|
|
2092
|
+
...(error instanceof RefusalError ? { recovery: error.recovery } : {}),
|
|
2086
2093
|
},
|
|
2087
2094
|
};
|
|
2088
2095
|
}
|
|
@@ -2378,6 +2385,32 @@ function isOfflineLeaseUsable(lease, sessionId, changedPaths, baselineDiffHash)
|
|
|
2378
2385
|
return false;
|
|
2379
2386
|
}
|
|
2380
2387
|
}
|
|
2388
|
+
class RefusalError extends Error {
|
|
2389
|
+
recovery;
|
|
2390
|
+
constructor(message, recovery) {
|
|
2391
|
+
super(message);
|
|
2392
|
+
this.recovery = recovery;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
function refuse(message, recovery) {
|
|
2396
|
+
return new RefusalError(`${message} Recovery: call ${recovery}.`, recovery);
|
|
2397
|
+
}
|
|
2398
|
+
function short(value) {
|
|
2399
|
+
return typeof value === 'string' && value.length > 0 ? value.slice(0, 12) : 'none';
|
|
2400
|
+
}
|
|
2401
|
+
function describePathDisagreement(reported, manifest) {
|
|
2402
|
+
const inManifest = new Set(manifest);
|
|
2403
|
+
const inReport = new Set(reported);
|
|
2404
|
+
const missing = manifest.filter((path) => !inReport.has(path));
|
|
2405
|
+
const extra = reported.filter((path) => !inManifest.has(path));
|
|
2406
|
+
const sample = (paths) => paths.slice(0, 5).join(', ') + (paths.length > 5 ? `, and ${paths.length - 5} more` : '');
|
|
2407
|
+
const parts = [`You reported ${reported.length}; the manifest has ${manifest.length}.`];
|
|
2408
|
+
if (missing.length > 0)
|
|
2409
|
+
parts.push(`Missing from your report: ${sample(missing)}.`);
|
|
2410
|
+
if (extra.length > 0)
|
|
2411
|
+
parts.push(`Reported but not in the manifest: ${sample(extra)}.`);
|
|
2412
|
+
return parts.join(' ');
|
|
2413
|
+
}
|
|
2381
2414
|
function repairChangeBaseline(local, recordedDiffHash, head) {
|
|
2382
2415
|
if (!recordedDiffHash || local?.diffHash === recordedDiffHash)
|
|
2383
2416
|
return undefined;
|