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.1",
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 = input.taskSlug
167
- ? (live.find((entry) => entry.taskSlug === input.taskSlug) ?? null)
168
- : await this.dependencies.activeContexts.load(repository.repoFingerprint);
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 && !input.sessionId && live.length > 1) {
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 new Error('Pending lifecycle intent must be recovered before preparing changes');
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 new Error('Read-only transition requires the active repository session');
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 new Error('Current backend task snapshot does not match verification');
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 new Error('Read-only task verification does not accept a lease or changed paths');
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 || activeLease?.baselineDiffHash !== baseline.diffHash) {
1002
- throw new Error('Write task verification requires its locally pinned change baseline');
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 new Error('Reported changed paths do not match the actual Git manifest');
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 (taskChangedPaths.length === 0 ||
1018
- !activeLease ||
1019
- !input.leaseId ||
1020
- activeLease.id !== input.leaseId ||
1021
- !pathsContainAll(baseline.leasePaths, taskChangedPaths) ||
1022
- !pathsContainAll(leaseChangedPaths, taskChangedPaths)) {
1023
- throw new Error('Write task verification requires the current backend change lease');
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 new Error('Current backend task snapshot does not match task close');
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 new Error('The backend did not abandon the task');
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;