engineering-memory 1.6.1 → 1.6.3
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.3",
|
|
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",
|
|
@@ -163,9 +163,16 @@ export class BridgeService {
|
|
|
163
163
|
return asJsonValue({ authentication, repository: publicRepository(repository) });
|
|
164
164
|
}
|
|
165
165
|
const live = await this.dependencies.activeContexts.list(repository.repoFingerprint);
|
|
166
|
-
let pointer =
|
|
167
|
-
|
|
168
|
-
|
|
166
|
+
let pointer = null;
|
|
167
|
+
if (input.taskSlug) {
|
|
168
|
+
pointer = live.find((entry) => entry.taskSlug === input.taskSlug) ?? null;
|
|
169
|
+
}
|
|
170
|
+
else if (input.sessionId) {
|
|
171
|
+
pointer = live.find((entry) => entry.sessionId === input.sessionId) ?? null;
|
|
172
|
+
}
|
|
173
|
+
else if (live.length === 1) {
|
|
174
|
+
pointer = live[0];
|
|
175
|
+
}
|
|
169
176
|
if (!pointer && input.taskSlug) {
|
|
170
177
|
return asJsonValue({
|
|
171
178
|
taskChoiceRequired: true,
|
|
@@ -174,7 +181,15 @@ export class BridgeService {
|
|
|
174
181
|
repository: publicRepository(repository),
|
|
175
182
|
});
|
|
176
183
|
}
|
|
177
|
-
if (!pointer &&
|
|
184
|
+
if (!pointer && input.sessionId && live.length > 0) {
|
|
185
|
+
return asJsonValue({
|
|
186
|
+
taskChoiceRequired: true,
|
|
187
|
+
requestedSessionId: input.sessionId,
|
|
188
|
+
liveTasks: live.map(describePointer),
|
|
189
|
+
repository: publicRepository(repository),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
if (!pointer && live.length > 1) {
|
|
178
193
|
return asJsonValue({
|
|
179
194
|
taskChoiceRequired: true,
|
|
180
195
|
liveTasks: live.map(describePointer),
|
|
@@ -390,7 +405,7 @@ export class BridgeService {
|
|
|
390
405
|
const pendingOutbox = await this.dependencies.outbox.list();
|
|
391
406
|
const pointer = await this.dependencies.activeContexts.load(repository.repoFingerprint);
|
|
392
407
|
if (pointer?.verificationIntent || pointer?.closeIntent) {
|
|
393
|
-
throw
|
|
408
|
+
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
409
|
}
|
|
395
410
|
const conflicts = [
|
|
396
411
|
...(pointer?.resumeConflicts ?? []),
|
|
@@ -422,7 +437,7 @@ export class BridgeService {
|
|
|
422
437
|
let transitionTaskVersion;
|
|
423
438
|
if (input.transitionToWrite) {
|
|
424
439
|
if (!pointer || pointer.sessionId !== input.sessionId) {
|
|
425
|
-
throw
|
|
440
|
+
throw refuse('A read-only task can only become a write task from the session that owns it.', 'session.resume');
|
|
426
441
|
}
|
|
427
442
|
const refreshed = await this.activeTaskSnapshot(pointer.taskId, pointer.projectId, repository.repoRoot);
|
|
428
443
|
transitionTaskVersion = refreshed.taskVersion;
|
|
@@ -976,7 +991,7 @@ export class BridgeService {
|
|
|
976
991
|
const snapshotTask = objectValue(snapshot?.task);
|
|
977
992
|
const activeLease = objectValue(snapshot?.activeLease);
|
|
978
993
|
if (!snapshotTask || snapshotTask.id !== input.taskId) {
|
|
979
|
-
throw
|
|
994
|
+
throw refuse('The backend is holding a different task than the one being verified.', 'session.resume');
|
|
980
995
|
}
|
|
981
996
|
const recoveredVerification = await this.retryVerificationRecovery(repository, pointer, snapshot);
|
|
982
997
|
if (recoveredVerification) {
|
|
@@ -990,7 +1005,7 @@ export class BridgeService {
|
|
|
990
1005
|
const validations = normalizePersistentInput(input.validations, repository.repoRoot);
|
|
991
1006
|
if (taskMode === 'read_only') {
|
|
992
1007
|
if (input.leaseId || input.changedPaths?.length) {
|
|
993
|
-
throw
|
|
1008
|
+
throw refuse('This task is read-only, so it has no lease and no changed paths to report.', 'task.verify');
|
|
994
1009
|
}
|
|
995
1010
|
}
|
|
996
1011
|
else {
|
|
@@ -998,8 +1013,11 @@ export class BridgeService {
|
|
|
998
1013
|
const leaseChangedPaths = Array.isArray(activeLease?.changedPaths)
|
|
999
1014
|
? activeLease.changedPaths.filter((path) => typeof path === 'string')
|
|
1000
1015
|
: [];
|
|
1001
|
-
if (!baseline
|
|
1002
|
-
throw
|
|
1016
|
+
if (!baseline) {
|
|
1017
|
+
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');
|
|
1018
|
+
}
|
|
1019
|
+
if (activeLease?.baselineDiffHash !== baseline.diffHash) {
|
|
1020
|
+
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
1021
|
}
|
|
1004
1022
|
const wholeTreeDelta = manifestDelta(baseline.changedPaths, repository.git.changedPaths);
|
|
1005
1023
|
const leased = new Set(normalizeChangedPaths([...baseline.leasePaths, ...leaseChangedPaths]));
|
|
@@ -1012,15 +1030,18 @@ export class BridgeService {
|
|
|
1012
1030
|
if (input.changedPaths &&
|
|
1013
1031
|
stableStringify(normalizeChangedPaths(input.changedPaths)) !==
|
|
1014
1032
|
stableStringify(taskChangedPaths)) {
|
|
1015
|
-
throw
|
|
1033
|
+
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');
|
|
1034
|
+
}
|
|
1035
|
+
if (taskChangedPaths.length === 0) {
|
|
1036
|
+
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
1037
|
}
|
|
1017
|
-
if (
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
!pathsContainAll(
|
|
1022
|
-
|
|
1023
|
-
throw
|
|
1038
|
+
if (!activeLease || !input.leaseId || activeLease.id !== input.leaseId) {
|
|
1039
|
+
throw refuse(`Verification needs the lease the backend currently holds. It has ${short(activeLease?.id)} and you sent ${short(input.leaseId)}.`, 'context.prepare_change');
|
|
1040
|
+
}
|
|
1041
|
+
const unleased = taskChangedPaths.filter((path) => !pathsContainAll(baseline.leasePaths, [path]) ||
|
|
1042
|
+
!pathsContainAll(leaseChangedPaths, [path]));
|
|
1043
|
+
if (unleased.length > 0) {
|
|
1044
|
+
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
1045
|
}
|
|
1025
1046
|
assertNewResourceEvidence(taskChanges.flatMap((entry) => newMemoryResourceCandidate(entry, resourceDiscoveryPolicy)), input.newResources ?? []);
|
|
1026
1047
|
}
|
|
@@ -1113,7 +1134,7 @@ export class BridgeService {
|
|
|
1113
1134
|
const snapshot = objectValue(snapshotResponse.data);
|
|
1114
1135
|
const task = objectValue(snapshot?.task);
|
|
1115
1136
|
if (!task || task.id !== input.taskId) {
|
|
1116
|
-
throw
|
|
1137
|
+
throw refuse('The backend is holding a different task than the one being closed.', 'session.resume');
|
|
1117
1138
|
}
|
|
1118
1139
|
const taskChanges = pointer.changeBaseline
|
|
1119
1140
|
? manifestDelta(pointer.changeBaseline.changedPaths, repository.git.changedPaths)
|
|
@@ -1142,7 +1163,7 @@ export class BridgeService {
|
|
|
1142
1163
|
});
|
|
1143
1164
|
const abandoned = objectValue(response.data);
|
|
1144
1165
|
if (!abandoned || abandoned.abandoned !== true) {
|
|
1145
|
-
throw
|
|
1166
|
+
throw refuse('The backend did not record the abandonment, so the task is still open.', 'session.resume');
|
|
1146
1167
|
}
|
|
1147
1168
|
const forgotten = await this.dependencies.activeContexts.forget(repository.repoFingerprint, input.taskId);
|
|
1148
1169
|
await this.dependencies.gate.invalidateTask(input.taskId);
|
|
@@ -2083,6 +2104,7 @@ export class BridgeService {
|
|
|
2083
2104
|
kind: 'bridge_error',
|
|
2084
2105
|
message: error instanceof Error ? error.message : 'Bridge operation failed',
|
|
2085
2106
|
retryable: false,
|
|
2107
|
+
...(error instanceof RefusalError ? { recovery: error.recovery } : {}),
|
|
2086
2108
|
},
|
|
2087
2109
|
};
|
|
2088
2110
|
}
|
|
@@ -2378,6 +2400,32 @@ function isOfflineLeaseUsable(lease, sessionId, changedPaths, baselineDiffHash)
|
|
|
2378
2400
|
return false;
|
|
2379
2401
|
}
|
|
2380
2402
|
}
|
|
2403
|
+
class RefusalError extends Error {
|
|
2404
|
+
recovery;
|
|
2405
|
+
constructor(message, recovery) {
|
|
2406
|
+
super(message);
|
|
2407
|
+
this.recovery = recovery;
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
function refuse(message, recovery) {
|
|
2411
|
+
return new RefusalError(`${message} Recovery: call ${recovery}.`, recovery);
|
|
2412
|
+
}
|
|
2413
|
+
function short(value) {
|
|
2414
|
+
return typeof value === 'string' && value.length > 0 ? value.slice(0, 12) : 'none';
|
|
2415
|
+
}
|
|
2416
|
+
function describePathDisagreement(reported, manifest) {
|
|
2417
|
+
const inManifest = new Set(manifest);
|
|
2418
|
+
const inReport = new Set(reported);
|
|
2419
|
+
const missing = manifest.filter((path) => !inReport.has(path));
|
|
2420
|
+
const extra = reported.filter((path) => !inManifest.has(path));
|
|
2421
|
+
const sample = (paths) => paths.slice(0, 5).join(', ') + (paths.length > 5 ? `, and ${paths.length - 5} more` : '');
|
|
2422
|
+
const parts = [`You reported ${reported.length}; the manifest has ${manifest.length}.`];
|
|
2423
|
+
if (missing.length > 0)
|
|
2424
|
+
parts.push(`Missing from your report: ${sample(missing)}.`);
|
|
2425
|
+
if (extra.length > 0)
|
|
2426
|
+
parts.push(`Reported but not in the manifest: ${sample(extra)}.`);
|
|
2427
|
+
return parts.join(' ');
|
|
2428
|
+
}
|
|
2381
2429
|
function repairChangeBaseline(local, recordedDiffHash, head) {
|
|
2382
2430
|
if (!recordedDiffHash || local?.diffHash === recordedDiffHash)
|
|
2383
2431
|
return undefined;
|