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.1",
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 new Error('Pending lifecycle intent must be recovered before preparing changes');
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 new Error('Read-only transition requires the active repository session');
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 new Error('Current backend task snapshot does not match verification');
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 new Error('Read-only task verification does not accept a lease or changed paths');
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 || activeLease?.baselineDiffHash !== baseline.diffHash) {
1002
- throw new Error('Write task verification requires its locally pinned change baseline');
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 new Error('Reported changed paths do not match the actual Git manifest');
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 (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');
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 new Error('Current backend task snapshot does not match task close');
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 new Error('The backend did not abandon the task');
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;