engineering-memory 1.11.8 → 1.11.9

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.
@@ -9,7 +9,7 @@ Call \`session.entry\` before you answer anything in a repository, whatever the
9
9
  - Switched off: say nothing about Engineering Memory, offer nothing, hint at nothing, however useful it would be for what was asked. They turned it off; raising it again is what this prevents. Record it back on only when they ask.
10
10
  - Bound: continue into the lifecycle.
11
11
 
12
- \`session.entry\` also reports this client's version. When it says an update is available, offer once to run \`npx engineering-memory@latest install\` and tell the user the client restarts afterwards. If they decline, record it with \`session.decline_update\` and never raise that version again. When it says an update is required, there is no choice to offer: say that this client is older than the backend supports, give the command, and do not carry on as though the lifecycle works.
12
+ \`session.entry\` also reports this client's version. When it says an update is available, offer once to run \`npx engineering-memory@latest install\` and tell the user that the running bridge switches to the new release by itself on its next call, so nothing needs restarting; only a bridge older than 1.11.9 needs the client restarted once after that install. If they decline, record it with \`session.decline_update\` and never raise that version again. When it says an update is required, there is no choice to offer: say that this client is older than the backend supports, give the command, and do not carry on as though the lifecycle works.
13
13
 
14
14
  The user may say at any point that they want a different organization, a different project, Engineering Memory switched off, or switched back on. Recognise what they mean rather than matching words — the request arrives in whatever language and phrasing they happen to use — and act on it. Changing the organization always asks for the project again.
15
15
 
package/install/files.mjs CHANGED
@@ -11,7 +11,13 @@ import {
11
11
  rm,
12
12
  writeFile,
13
13
  } from 'node:fs/promises';
14
- import { basename, dirname, join } from 'node:path';
14
+ import { basename, dirname, join, resolve } from 'node:path';
15
+ import {
16
+ launcherPackageJson,
17
+ launcherSource,
18
+ normalizeRelativeEntry,
19
+ releasePointerName,
20
+ } from './launcher.mjs';
15
21
 
16
22
  export class InstallTransaction {
17
23
  constructor() {
@@ -110,6 +116,7 @@ export async function installManagedDirectory(
110
116
  ) {
111
117
  const artifact = options.artifact ?? 'skill';
112
118
  const artifactLabel = artifact === 'skill' ? 'skill' : 'bridge runtime';
119
+ const accepted = new Set([artifact, ...(options.replaces ?? [])]);
113
120
  const sourceHash = await hashDirectory(source);
114
121
  const manifestPath = join(destination, '.engineering-memory-install.json');
115
122
  if (await exists(destination)) {
@@ -117,7 +124,7 @@ export async function installManagedDirectory(
117
124
  if (
118
125
  manifest?.product !== 'engineering-memory' ||
119
126
  manifest?.schemaVersion !== 1 ||
120
- (manifest.artifact ?? 'skill') !== artifact
127
+ !accepted.has(manifest.artifact ?? 'skill')
121
128
  ) {
122
129
  throw new Error(
123
130
  `Refusing to overwrite an unmanaged ${artifactLabel} directory: ${destination}`,
@@ -151,6 +158,7 @@ export async function installManagedDirectory(
151
158
  schemaVersion: 1,
152
159
  artifact,
153
160
  sourceHash,
161
+ ...(options.manifest ?? {}),
154
162
  },
155
163
  null,
156
164
  2,
@@ -181,9 +189,22 @@ export async function installManagedDirectory(
181
189
 
182
190
  export async function installManagedBridgeRuntime(
183
191
  source,
184
- destination,
192
+ runtimeRoot,
185
193
  transaction,
194
+ options = {},
186
195
  ) {
196
+ const bridgeEntry = normalizeRelativeEntry(
197
+ options.bridgeEntry ?? 'dist/src/index.js',
198
+ 'Bridge entry',
199
+ );
200
+ const gateEntry = normalizeRelativeEntry(
201
+ options.gateEntry ?? 'dist/src/git/pre-commit.js',
202
+ 'Git gate entry',
203
+ );
204
+ const clientVersion =
205
+ typeof options.clientVersion === 'string' && options.clientVersion
206
+ ? options.clientVersion
207
+ : null;
187
208
  const packageJsonPath = join(source, 'package.json');
188
209
  const packageLockPath = join(source, 'package-lock.json');
189
210
  const nodeModulesPath = join(source, 'node_modules');
@@ -209,10 +230,8 @@ export async function installManagedBridgeRuntime(
209
230
  packageJson,
210
231
  packageLock,
211
232
  );
212
- await mkdir(dirname(destination), { recursive: true });
213
- const bundle = await mkdtemp(
214
- join(dirname(destination), '.bridge-runtime-source-'),
215
- );
233
+ await mkdir(runtimeRoot, { recursive: true });
234
+ const bundle = await mkdtemp(join(runtimeRoot, '.bridge-runtime-source-'));
216
235
  try {
217
236
  await copyDirectory(join(source, 'dist'), join(bundle, 'dist'));
218
237
  await copyFile(packageJsonPath, join(bundle, 'package.json'));
@@ -230,14 +249,101 @@ export async function installManagedBridgeRuntime(
230
249
  join(bundle, ...packageEntry.path.split('/')),
231
250
  );
232
251
  }
233
- return await installManagedDirectory(bundle, destination, transaction, {
252
+ for (const [entry, label] of [
253
+ [bridgeEntry, 'Bridge entry'],
254
+ [gateEntry, 'Git gate entry'],
255
+ ]) {
256
+ if (!(await exists(join(bundle, ...entry.split('/'))))) {
257
+ throw new Error(`${label} is missing from the built bridge: ${entry}`);
258
+ }
259
+ }
260
+ const sourceHash = await hashDirectory(bundle);
261
+ const releasePath = join(runtimeRoot, 'releases', sourceHash.slice(0, 16));
262
+ const installedAt = new Date().toISOString();
263
+ await installManagedDirectory(bundle, releasePath, transaction, {
234
264
  artifact: 'bridge-runtime',
265
+ manifest: { clientVersion, installedAt },
235
266
  });
267
+ const launcherPath = join(runtimeRoot, 'bridge');
268
+ const launcherStage = await mkdtemp(join(runtimeRoot, '.bridge-launcher-'));
269
+ try {
270
+ await writeAtomic(
271
+ join(launcherStage, 'package.json'),
272
+ launcherPackageJson,
273
+ );
274
+ await writeAtomic(
275
+ join(launcherStage, ...bridgeEntry.split('/')),
276
+ launcherSource('bridgeEntry', bridgeEntry),
277
+ );
278
+ await writeAtomic(
279
+ join(launcherStage, ...gateEntry.split('/')),
280
+ launcherSource('gateEntry', gateEntry),
281
+ );
282
+ await installManagedDirectory(launcherStage, launcherPath, transaction, {
283
+ artifact: 'bridge-launcher',
284
+ replaces: ['bridge-runtime'],
285
+ });
286
+ } finally {
287
+ await rm(launcherStage, { recursive: true, force: true });
288
+ }
289
+ await writeJson(
290
+ join(runtimeRoot, releasePointerName),
291
+ {
292
+ product: 'engineering-memory',
293
+ schemaVersion: 1,
294
+ version: clientVersion,
295
+ sourceHash,
296
+ path: releasePath,
297
+ bridgeEntry,
298
+ gateEntry,
299
+ installedAt,
300
+ },
301
+ transaction,
302
+ );
303
+ return { sourceHash, releasePath, launcherPath, bridgeEntry, gateEntry };
236
304
  } finally {
237
305
  await rm(bundle, { recursive: true, force: true });
238
306
  }
239
307
  }
240
308
 
309
+ export async function pruneReleases(runtimeRoot, options = {}) {
310
+ const keep = Math.max(1, options.keep ?? 3);
311
+ const releasesPath = join(runtimeRoot, 'releases');
312
+ if (!(await exists(releasesPath))) return [];
313
+ const pointer = await readJson(join(runtimeRoot, releasePointerName));
314
+ const currentPath =
315
+ pointer && typeof pointer.path === 'string' ? resolve(pointer.path) : null;
316
+ const candidates = [];
317
+ for (const entry of await readdir(releasesPath, { withFileTypes: true })) {
318
+ if (!entry.isDirectory()) continue;
319
+ const path = join(releasesPath, entry.name);
320
+ if (currentPath && resolve(path) === currentPath) continue;
321
+ const manifest = await readJson(
322
+ join(path, '.engineering-memory-install.json'),
323
+ );
324
+ if (
325
+ manifest?.product !== 'engineering-memory' ||
326
+ manifest.artifact !== 'bridge-runtime'
327
+ ) {
328
+ continue;
329
+ }
330
+ const installedAt = Date.parse(String(manifest.installedAt ?? ''));
331
+ candidates.push({
332
+ path,
333
+ installedAt: Number.isNaN(installedAt)
334
+ ? (await lstat(path)).mtimeMs
335
+ : installedAt,
336
+ });
337
+ }
338
+ candidates.sort((left, right) => right.installedAt - left.installedAt);
339
+ const removed = [];
340
+ for (const candidate of candidates.slice(keep - 1)) {
341
+ await rm(candidate.path, { recursive: true, force: true });
342
+ removed.push(candidate.path);
343
+ }
344
+ return removed;
345
+ }
346
+
241
347
  export async function hashDirectory(root, options = {}) {
242
348
  const hash = createHash('sha256');
243
349
  const excluded = new Set(options.exclude ?? []);
@@ -10,6 +10,7 @@ import {
10
10
  installManagedBridgeRuntime,
11
11
  installManagedDirectory,
12
12
  InstallTransaction,
13
+ pruneReleases,
13
14
  readJson,
14
15
  readText,
15
16
  replaceFile,
@@ -54,7 +55,8 @@ export async function installEngineeringMemory(options = {}) {
54
55
  options.gateEntry ??
55
56
  join(bridgeRuntimeSource, 'dist', 'src', 'git', 'pre-commit.js'),
56
57
  );
57
- const runtimePath = join(homeDir, '.engineering-memory', 'runtime', 'bridge');
58
+ const runtimeRoot = join(homeDir, '.engineering-memory', 'runtime');
59
+ const runtimePath = join(runtimeRoot, 'bridge');
58
60
  const bridgeEntry = stableRuntimeEntry(
59
61
  bridgeRuntimeSource,
60
62
  runtimePath,
@@ -153,10 +155,15 @@ export async function installEngineeringMemory(options = {}) {
153
155
  const transaction = new InstallTransaction();
154
156
  let result;
155
157
  try {
156
- const runtimeHash = await installManagedBridgeRuntime(
158
+ const runtime = await installManagedBridgeRuntime(
157
159
  bridgeRuntimeSource,
158
- runtimePath,
160
+ runtimeRoot,
159
161
  transaction,
162
+ {
163
+ clientVersion,
164
+ bridgeEntry: relative(bridgeRuntimeSource, sourceBridgeEntry),
165
+ gateEntry: relative(bridgeRuntimeSource, sourceGateEntry),
166
+ },
160
167
  );
161
168
  const mcp = {
162
169
  ...(existingState?.mcp ?? {}),
@@ -183,7 +190,8 @@ export async function installEngineeringMemory(options = {}) {
183
190
  skillHash,
184
191
  runtime: {
185
192
  path: runtimePath,
186
- sourceHash: runtimeHash,
193
+ sourceHash: runtime.sourceHash,
194
+ release: runtime.releasePath,
187
195
  bridgeEntry,
188
196
  gateEntry,
189
197
  },
@@ -206,6 +214,7 @@ export async function installEngineeringMemory(options = {}) {
206
214
  apiUrl,
207
215
  clientVersion,
208
216
  runtimePath,
217
+ release: runtime.releasePath,
209
218
  bridgeEntry,
210
219
  gateEntry,
211
220
  hook: hookResult,
@@ -222,6 +231,13 @@ export async function installEngineeringMemory(options = {}) {
222
231
  throw error;
223
232
  }
224
233
  await transaction.commit();
234
+ try {
235
+ result.prunedReleases = await pruneReleases(runtimeRoot);
236
+ } catch (error) {
237
+ result.prunedReleases = [];
238
+ result.releasePruneProblem =
239
+ error instanceof Error ? error.message : String(error);
240
+ }
225
241
  return result;
226
242
  }
227
243
 
@@ -0,0 +1,95 @@
1
+ export const launcherPackageJson = `${JSON.stringify(
2
+ {
3
+ name: 'engineering-memory-bridge-launcher',
4
+ private: true,
5
+ type: 'module',
6
+ },
7
+ null,
8
+ 2,
9
+ )}\n`;
10
+
11
+ export const releasePointerName = 'current.json';
12
+
13
+ export function normalizeRelativeEntry(entry, label) {
14
+ const normalized = String(entry).replaceAll('\\', '/').replace(/^\.\//, '');
15
+ const segments = normalized.split('/');
16
+ if (
17
+ normalized === '' ||
18
+ normalized.startsWith('/') ||
19
+ /^[A-Za-z]:/.test(normalized) ||
20
+ segments.some(
21
+ (segment) => segment === '' || segment === '..' || segment === '.',
22
+ )
23
+ ) {
24
+ throw new Error(
25
+ `${label} must be a relative path inside the release: ${entry}`,
26
+ );
27
+ }
28
+ if (segments[0] !== 'dist') {
29
+ throw new Error(
30
+ `${label} must be inside the built dist directory: ${entry}`,
31
+ );
32
+ }
33
+ return normalized;
34
+ }
35
+
36
+ export function launcherSource(kind, relativeEntry) {
37
+ if (kind !== 'bridgeEntry' && kind !== 'gateEntry') {
38
+ throw new Error(`Unknown launcher kind: ${kind}`);
39
+ }
40
+ const depth = normalizeRelativeEntry(relativeEntry, 'Launcher entry').split(
41
+ '/',
42
+ ).length;
43
+ const ascend = Array.from({ length: depth }, () => "'..'").join(', ');
44
+ const label = kind === 'bridgeEntry' ? 'bridge' : 'Git gate';
45
+ return [
46
+ '#!/usr/bin/env node',
47
+ "import { readFile } from 'node:fs/promises';",
48
+ "import { dirname, isAbsolute, join, resolve } from 'node:path';",
49
+ "import { fileURLToPath, pathToFileURL } from 'node:url';",
50
+ '',
51
+ `const runtimeRoot = resolve(dirname(fileURLToPath(import.meta.url)), ${ascend});`,
52
+ `const pointerPath = join(runtimeRoot, ${JSON.stringify(releasePointerName)});`,
53
+ `const installCommand = 'npx engineering-memory@latest install';`,
54
+ '',
55
+ 'function stop(message) {',
56
+ " process.stderr.write(message + '\\n');",
57
+ ' process.exit(1);',
58
+ '}',
59
+ '',
60
+ 'let pointer;',
61
+ 'try {',
62
+ " pointer = JSON.parse(await readFile(pointerPath, 'utf8'));",
63
+ '} catch (error) {',
64
+ ' stop(',
65
+ ` 'The Engineering Memory ${label} cannot start: ' +`,
66
+ ' pointerPath +',
67
+ " ' is unreadable (' +",
68
+ ' (error instanceof Error ? error.message : String(error)) +',
69
+ " '). Run ' +",
70
+ ' installCommand +',
71
+ " '.',",
72
+ ' );',
73
+ '}',
74
+ 'if (',
75
+ ' !pointer ||',
76
+ " pointer.product !== 'engineering-memory' ||",
77
+ " typeof pointer.path !== 'string' ||",
78
+ ` typeof pointer.${kind} !== 'string'`,
79
+ ') {',
80
+ ' stop(',
81
+ ` 'The Engineering Memory ${label} cannot start: ' +`,
82
+ ' pointerPath +',
83
+ " ' does not name a release. Run ' +",
84
+ ' installCommand +',
85
+ " '.',",
86
+ ' );',
87
+ '}',
88
+ 'const releasePath = isAbsolute(pointer.path)',
89
+ ' ? pointer.path',
90
+ ' : resolve(runtimeRoot, pointer.path);',
91
+ 'process.env.ENGINEERING_MEMORY_RUNTIME_CURRENT = pointerPath;',
92
+ `await import(pathToFileURL(join(releasePath, ...pointer.${kind}.split('/'))).href);`,
93
+ '',
94
+ ].join('\n');
95
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engineering-memory",
3
- "version": "1.11.8",
3
+ "version": "1.11.9",
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",
@@ -1,3 +1,3 @@
1
1
  {
2
- "gitHead": "eefcf3aac9c83ba9d0126d05309ca100947317d3"
2
+ "gitHead": "3b5175e578b3d631932b079b5678afe13dc4f00d"
3
3
  }
@@ -10,6 +10,9 @@ export function loadBridgeConfig(env = process.env) {
10
10
  apiBaseUrl: (env.ENGINEERING_MEMORY_API_URL ?? 'http://127.0.0.1:3000').replace(/\/$/, ''),
11
11
  stateRoot: resolve(env.ENGINEERING_MEMORY_STATE_DIR ?? join(homedir(), '.engineering-memory')),
12
12
  clientVersion: env.ENGINEERING_MEMORY_CLIENT_VERSION ?? null,
13
+ runtimeCurrentPath: env.ENGINEERING_MEMORY_RUNTIME_CURRENT
14
+ ? resolve(env.ENGINEERING_MEMORY_RUNTIME_CURRENT)
15
+ : null,
13
16
  requestTimeoutMs: positiveInteger(env.ENGINEERING_MEMORY_TIMEOUT_MS, 15_000),
14
17
  cacheMaxEntries: positiveInteger(env.ENGINEERING_MEMORY_CACHE_MAX_ENTRIES, 64),
15
18
  cacheMaxBytes: positiveInteger(env.ENGINEERING_MEMORY_CACHE_MAX_BYTES, 5 * 1024 * 1024),
@@ -1,88 +1,32 @@
1
1
  #!/usr/bin/env node
2
- import { WorktreePool } from './runtime/worktree-pool.js';
3
- import { WorktreePolicyCache } from './runtime/worktree-policy.js';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
4
  import { serveStdio } from '@modelcontextprotocol/server/stdio';
5
- import { BrowserAuthCoordinator } from './auth/browser-auth.js';
6
- import { createCredentialStore } from './auth/credential-store.js';
7
- import { EtagCache } from './cache/etag-cache.js';
8
- import { apiNamespaceKey, apiStateRoot, endpoints, loadBridgeConfig } from './config.js';
9
- import { GitInspector } from './git/git-inspector.js';
10
- import { VerificationGate } from './git/verification-gate.js';
11
- import { JournalStore } from './journal/journal-store.js';
12
- import { createEngineeringMemoryServer } from './mcp/server.js';
13
- import { RepositoryResolver } from './project/repository.js';
14
- import { ApiClient } from './runtime/api-client.js';
15
- import { BridgeService } from './runtime/bridge-service.js';
16
- import { OfflineOutbox } from './runtime/offline-outbox.js';
17
- import { ActiveContextStore } from './runtime/active-context-store.js';
18
- import { RepositoryDecisionStore } from './runtime/repository-decision-store.js';
19
- import { ShadowNoticeStore } from './runtime/shadow-notice-store.js';
20
- import { UpdateChoiceStore } from './runtime/update-choice-store.js';
21
- import { PrincipalStateGuard } from './runtime/principal-state.js';
22
- import { OnboardingStore } from './runtime/onboarding-store.js';
23
- import { QuestionnaireStore } from './runtime/questionnaire-store.js';
24
- export function createBridgeService() {
5
+ import { loadBridgeConfig } from './config.js';
6
+ import { createMcpServer } from './mcp/server.js';
7
+ import { createRuntime } from './runtime/runtime-entry.js';
8
+ import { RuntimeHost, readCurrentRelease } from './runtime/runtime-host.js';
9
+ export { createBridgeService } from './runtime/create-bridge-service.js';
10
+ const releaseRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
11
+ export async function createServer() {
25
12
  const config = loadBridgeConfig();
26
- const apiNamespace = apiNamespaceKey(config.apiBaseUrl);
27
- const stateRoot = apiStateRoot(config);
28
- const credentials = createCredentialStore({
29
- service: config.credentialService,
30
- account: `${config.credentialAccount}:${apiNamespace}`,
31
- stateRoot,
13
+ const server = createMcpServer();
14
+ let version = config.clientVersion;
15
+ const currentPath = config.runtimeCurrentPath;
16
+ if (currentPath) {
17
+ const current = await readCurrentRelease(currentPath);
18
+ if ('release' in current && current.path === releaseRoot)
19
+ version = current.release.version ?? version;
20
+ }
21
+ const host = new RuntimeHost({
22
+ server,
23
+ release: { path: releaseRoot, version, module: { createRuntime } },
24
+ ...(currentPath ? { currentPath } : {}),
25
+ log: (message) => process.stderr.write(`${message}\n`),
32
26
  });
33
- const cache = new EtagCache({
34
- stateRoot,
35
- maxEntries: config.cacheMaxEntries,
36
- maxBytes: config.cacheMaxBytes,
37
- maxAgeMs: config.cacheMaxAgeMs,
38
- });
39
- const client = new ApiClient({
40
- baseUrl: config.apiBaseUrl,
41
- timeoutMs: config.requestTimeoutMs,
42
- credentials,
43
- cache,
44
- refreshPath: endpoints.authRefresh,
45
- });
46
- const git = new GitInspector();
47
- const outbox = new OfflineOutbox(stateRoot);
48
- const activeContexts = new ActiveContextStore(stateRoot);
49
- const repositoryDecisions = new RepositoryDecisionStore(stateRoot);
50
- const updateChoices = new UpdateChoiceStore(stateRoot);
51
- const shadowNotices = new ShadowNoticeStore(stateRoot);
52
- const worktreePool = new WorktreePool(stateRoot, {
53
- pendingWork: async (entry) => {
54
- if (!entry.taskId)
55
- return false;
56
- const pointer = await activeContexts.loadForTask(entry.repoFingerprint, entry.taskId);
57
- return Boolean(pointer?.verificationIntent ||
58
- pointer?.closeIntent ||
59
- (await outbox.listForTask(entry.taskId)).length);
60
- },
61
- });
62
- const gate = new VerificationGate(stateRoot, git, outbox, undefined, worktreePool);
63
- const principalState = new PrincipalStateGuard(stateRoot, credentials, cache, outbox, activeContexts, gate);
64
- return new BridgeService({
65
- worktreePool,
66
- worktreePolicy: new WorktreePolicyCache(stateRoot),
67
- client,
68
- credentials,
69
- browserAuth: new BrowserAuthCoordinator(client, credentials),
70
- repositories: new RepositoryResolver(git, config.markerSchemaVersion, stateRoot),
71
- journal: new JournalStore(stateRoot),
72
- outbox,
73
- gate,
74
- activeContexts,
75
- repositoryDecisions,
76
- updateChoices,
77
- shadowNotices,
78
- questionnaires: new QuestionnaireStore(stateRoot),
79
- onboarding: new OnboardingStore(stateRoot),
80
- clientVersion: config.clientVersion,
81
- principalState,
82
- });
83
- }
84
- export function createServer() {
85
- return createEngineeringMemoryServer(createBridgeService());
27
+ host.start();
28
+ host.watch(15_000);
29
+ return server;
86
30
  }
87
31
  void serveStdio(createServer);
88
32
  //# sourceMappingURL=index.js.map
@@ -2,17 +2,24 @@ import { registerWorktreeTools } from './worktree-tools.js';
2
2
  import { McpServer } from '@modelcontextprotocol/server';
3
3
  import { registerOnboardingTools } from './onboarding-tools.js';
4
4
  import { registerEngineeringMemoryTools, registerQuestionnaireTools } from './tool-definitions.js';
5
- export function createEngineeringMemoryServer(service) {
6
- const server = new McpServer({
5
+ export const serverInstructions = 'Start with session.entry and follow the returned lifecycle. Do not block independent task work on memory.propose_revision drafting, submission or approval. If the host permits background agents or concurrent calls, dispatch independent proposals and continue useful work; otherwise checkpoint the draft and defer submission until needed. Checkpoint, correction, reconciliation, self-review and scaffold receipts use the durable local outbox; after deliveryStatus pending, continue independent work without polling or resending. Inventory upload, claim-bound evidence submission and test-result reporting may overlap independent analysis using host-permitted concurrent calls. Wait only when the next operation needs the result; required task deliveries must settle before verification. Preserve task/source identity and native user approval; never treat pending work as verified.';
6
+ export function createMcpServer() {
7
+ return new McpServer({
7
8
  name: 'engineering-memory',
8
9
  version: '0.1.0',
9
10
  }, {
10
- instructions: 'Start with session.entry and follow the returned lifecycle. Do not block independent task work on memory.propose_revision drafting, submission or approval. If the host permits background agents or concurrent calls, dispatch independent proposals and continue useful work; otherwise checkpoint the draft and defer submission until needed. Checkpoint, correction, reconciliation, self-review and scaffold receipts use the durable local outbox; after deliveryStatus pending, continue independent work without polling or resending. Inventory upload, claim-bound evidence submission and test-result reporting may overlap independent analysis using host-permitted concurrent calls. Wait only when the next operation needs the result; required task deliveries must settle before verification. Preserve task/source identity and native user approval; never treat pending work as verified.',
11
+ instructions: serverInstructions,
11
12
  });
12
- registerEngineeringMemoryTools(server, service);
13
- registerQuestionnaireTools(server, service);
14
- registerOnboardingTools(server, service);
15
- registerWorktreeTools(server, service);
13
+ }
14
+ export function registerAllTools(host, service) {
15
+ registerEngineeringMemoryTools(host, service);
16
+ registerQuestionnaireTools(host, service);
17
+ registerOnboardingTools(host, service);
18
+ registerWorktreeTools(host, service);
19
+ }
20
+ export function createEngineeringMemoryServer(service) {
21
+ const server = createMcpServer();
22
+ registerAllTools(server, service);
16
23
  return server;
17
24
  }
18
25
  //# sourceMappingURL=server.js.map
@@ -0,0 +1,12 @@
1
+ export function recordingHost(server) {
2
+ const tools = [];
3
+ const registerTool = (name, config, callback) => {
4
+ tools.push({ name, config, callback });
5
+ return undefined;
6
+ };
7
+ return {
8
+ host: { registerTool: registerTool, server },
9
+ tools,
10
+ };
11
+ }
12
+ //# sourceMappingURL=tool-host.js.map
@@ -459,7 +459,7 @@ export class BridgeService {
459
459
  const recoveredClose = await this.retryCloseIntent(repository, pointer);
460
460
  return asJsonValue({
461
461
  backend: { close: recoveredClose },
462
- worktree: this.poolAllocations.get(repository.repoRoot) ?? null,
462
+ worktree: (await this.ownedAllocation(projectId, repository.repoRoot)) ?? null,
463
463
  pendingQuestionnaires,
464
464
  repository: publicRepository(repository),
465
465
  localJournal: await this.dependencies.journal.load(projectId, taskSlug),
@@ -642,7 +642,7 @@ export class BridgeService {
642
642
  return asJsonValue({
643
643
  backend,
644
644
  repository: publicRepository(repository),
645
- worktree: this.poolAllocations.get(repository.repoRoot) ?? null,
645
+ worktree: (await this.ownedAllocation(projectId, repository.repoRoot)) ?? null,
646
646
  localJournal,
647
647
  pendingQuestionnaires,
648
648
  synchronization: {
@@ -741,7 +741,7 @@ export class BridgeService {
741
741
  transitionTaskVersion = refreshed.taskVersion;
742
742
  }
743
743
  if (decision && this.dependencies.worktreePool) {
744
- const allocation = this.poolAllocations.get(repository.repoRoot);
744
+ const allocation = await this.ownedAllocation(pointer.projectId, repository.repoRoot);
745
745
  if (!allocation || allocation.externalTaskId !== pointer.taskSlug)
746
746
  throw refuse('Choose this task branch and use its allocated repoRoot before transitioning to write.', 'task.branch');
747
747
  await this.dependencies.worktreePool.assertOwnership(pointer.projectId, repository.repoRoot, allocation.generation);
@@ -802,16 +802,14 @@ export class BridgeService {
802
802
  await this.dependencies.repositories.git
803
803
  .branchStore(repository.repoRoot)
804
804
  .bind(preparedPointer.projectId, preparedPointer.taskSlug, preparedPointer.taskId);
805
+ const allocation = await this.ownedAllocation(preparedPointer.projectId, repository.repoRoot);
805
806
  await this.dependencies.activeContexts.save({
806
807
  ...preparedPointer,
807
808
  worktreeId: sha256(repository.repoRoot),
808
809
  branch: typeof preparedTask?.branch === 'string' ? preparedTask.branch : decision.branch,
809
810
  mode: 'write',
810
- ...(this.poolAllocations.get(repository.repoRoot)
811
- ? { worktreeGeneration: this.poolAllocations.get(repository.repoRoot).generation }
812
- : {}),
811
+ ...(allocation ? { worktreeGeneration: allocation.generation } : {}),
813
812
  });
814
- const allocation = this.poolAllocations.get(repository.repoRoot);
815
813
  if (allocation && this.dependencies.worktreePool) {
816
814
  await this.dependencies.worktreePool.bind(preparedPointer.projectId, repository.repoRoot, allocation.generation, preparedPointer.taskId);
817
815
  allocation.taskId = preparedPointer.taskId;
@@ -2020,6 +2018,25 @@ export class BridgeService {
2020
2018
  nextAction: 'Run every file, terminal, lifecycle and delivery operation from this repoRoot. Use task.heartbeat during long work. Pause before handing the task to another client.',
2021
2019
  });
2022
2020
  }
2021
+ adoptOwned(repoRoot, entry) {
2022
+ const cached = this.poolAllocations.get(repoRoot);
2023
+ if (cached)
2024
+ return cached;
2025
+ const pool = this.dependencies.worktreePool;
2026
+ if (!entry || !pool || !pool.owns(entry))
2027
+ return undefined;
2028
+ this.poolAllocations.set(repoRoot, entry);
2029
+ return entry;
2030
+ }
2031
+ async ownedAllocation(projectId, repoRoot) {
2032
+ const cached = this.poolAllocations.get(repoRoot);
2033
+ if (cached)
2034
+ return cached;
2035
+ const pool = this.dependencies.worktreePool;
2036
+ if (!pool)
2037
+ return undefined;
2038
+ return this.adoptOwned(repoRoot, await pool.forPath(projectId, repoRoot));
2039
+ }
2023
2040
  async assertPoolPointer(repository, pointer) {
2024
2041
  const pool = this.dependencies.worktreePool;
2025
2042
  if (!pool)
@@ -2029,7 +2046,7 @@ export class BridgeService {
2029
2046
  return; // existing unmanaged tasks retain their legacy reservation
2030
2047
  if (entry && entry.externalTaskId !== pointer.taskSlug)
2031
2048
  throw refuse(`This directory is allocated to task ${entry.externalTaskId}. Resume this task from its own worktree, or reconcile the pool to release the directory.`, 'worktree.reconcile');
2032
- const owned = this.poolAllocations.get(repository.repoRoot);
2049
+ const owned = this.adoptOwned(repository.repoRoot, entry);
2033
2050
  if (!entry || !owned || owned.generation !== pointer.worktreeGeneration)
2034
2051
  throw refuse('Resume this task to acquire its worktree before writing or delivering.', 'session.resume');
2035
2052
  await pool.assertOwnership(pointer.projectId, repository.repoRoot, owned.generation);
@@ -2156,7 +2173,9 @@ export class BridgeService {
2156
2173
  if (!abandoned || abandoned.abandoned !== true) {
2157
2174
  throw refuse('The backend did not record the abandonment, so the task is still open.', 'session.resume');
2158
2175
  }
2159
- const allocation = this.poolAllocations.get(repository.repoRoot);
2176
+ const allocation = repository.projectId
2177
+ ? await this.ownedAllocation(repository.projectId, repository.repoRoot)
2178
+ : this.poolAllocations.get(repository.repoRoot);
2160
2179
  if (allocation && this.dependencies.worktreePool) {
2161
2180
  await this.dependencies.worktreePool.pause(allocation.projectId, allocation.repoRoot, allocation.generation);
2162
2181
  this.poolAllocations.delete(repository.repoRoot);
@@ -3424,7 +3443,9 @@ export class BridgeService {
3424
3443
  taskChanges,
3425
3444
  });
3426
3445
  await this.dependencies.activeContexts.clearCloseIntent(repository.repoFingerprint, String(body.taskId));
3427
- const allocation = this.poolAllocations.get(repository.repoRoot);
3446
+ const allocation = repository.projectId
3447
+ ? await this.ownedAllocation(repository.projectId, repository.repoRoot)
3448
+ : this.poolAllocations.get(repository.repoRoot);
3428
3449
  if (allocation && this.dependencies.worktreePool)
3429
3450
  await this.dependencies.worktreePool.delivery(allocation.projectId, repository.repoRoot, allocation.generation);
3430
3451
  else
@@ -3510,7 +3531,7 @@ export class BridgeService {
3510
3531
  try {
3511
3532
  const allocation = await this.dependencies.worktreePool?.forTask(taskId);
3512
3533
  if (allocation) {
3513
- const owned = this.poolAllocations.get(allocation.repoRoot);
3534
+ const owned = this.adoptOwned(allocation.repoRoot, allocation);
3514
3535
  if (!owned || owned.generation !== allocation.generation)
3515
3536
  throw refuse('Resume the task in its owned worktree before this operation.', 'session.resume');
3516
3537
  await this.dependencies.worktreePool.assertOwnership(allocation.projectId, allocation.repoRoot, owned.generation);
@@ -0,0 +1,82 @@
1
+ import { WorktreePool } from './worktree-pool.js';
2
+ import { WorktreePolicyCache } from './worktree-policy.js';
3
+ import { BrowserAuthCoordinator } from '../auth/browser-auth.js';
4
+ import { createCredentialStore } from '../auth/credential-store.js';
5
+ import { EtagCache } from '../cache/etag-cache.js';
6
+ import { apiNamespaceKey, apiStateRoot, endpoints, loadBridgeConfig } from '../config.js';
7
+ import { GitInspector } from '../git/git-inspector.js';
8
+ import { VerificationGate } from '../git/verification-gate.js';
9
+ import { JournalStore } from '../journal/journal-store.js';
10
+ import { RepositoryResolver } from '../project/repository.js';
11
+ import { ApiClient } from './api-client.js';
12
+ import { BridgeService } from './bridge-service.js';
13
+ import { OfflineOutbox } from './offline-outbox.js';
14
+ import { ActiveContextStore } from './active-context-store.js';
15
+ import { RepositoryDecisionStore } from './repository-decision-store.js';
16
+ import { ShadowNoticeStore } from './shadow-notice-store.js';
17
+ import { UpdateChoiceStore } from './update-choice-store.js';
18
+ import { PrincipalStateGuard } from './principal-state.js';
19
+ import { OnboardingStore } from './onboarding-store.js';
20
+ import { QuestionnaireStore } from './questionnaire-store.js';
21
+ export function createBridgeService(options = {}) {
22
+ const config = loadBridgeConfig();
23
+ const apiNamespace = apiNamespaceKey(config.apiBaseUrl);
24
+ const stateRoot = apiStateRoot(config);
25
+ const credentials = createCredentialStore({
26
+ service: config.credentialService,
27
+ account: `${config.credentialAccount}:${apiNamespace}`,
28
+ stateRoot,
29
+ });
30
+ const cache = new EtagCache({
31
+ stateRoot,
32
+ maxEntries: config.cacheMaxEntries,
33
+ maxBytes: config.cacheMaxBytes,
34
+ maxAgeMs: config.cacheMaxAgeMs,
35
+ });
36
+ const client = new ApiClient({
37
+ baseUrl: config.apiBaseUrl,
38
+ timeoutMs: config.requestTimeoutMs,
39
+ credentials,
40
+ cache,
41
+ refreshPath: endpoints.authRefresh,
42
+ });
43
+ const git = new GitInspector();
44
+ const outbox = new OfflineOutbox(stateRoot);
45
+ const activeContexts = new ActiveContextStore(stateRoot);
46
+ const repositoryDecisions = new RepositoryDecisionStore(stateRoot);
47
+ const updateChoices = new UpdateChoiceStore(stateRoot);
48
+ const shadowNotices = new ShadowNoticeStore(stateRoot);
49
+ const worktreePool = new WorktreePool(stateRoot, {
50
+ ...(options.ownerId ? { ownerId: options.ownerId } : {}),
51
+ pendingWork: async (entry) => {
52
+ if (!entry.taskId)
53
+ return false;
54
+ const pointer = await activeContexts.loadForTask(entry.repoFingerprint, entry.taskId);
55
+ return Boolean(pointer?.verificationIntent ||
56
+ pointer?.closeIntent ||
57
+ (await outbox.listForTask(entry.taskId)).length);
58
+ },
59
+ });
60
+ const gate = new VerificationGate(stateRoot, git, outbox, undefined, worktreePool);
61
+ const principalState = new PrincipalStateGuard(stateRoot, credentials, cache, outbox, activeContexts, gate);
62
+ return new BridgeService({
63
+ worktreePool,
64
+ worktreePolicy: new WorktreePolicyCache(stateRoot),
65
+ client,
66
+ credentials,
67
+ browserAuth: new BrowserAuthCoordinator(client, credentials),
68
+ repositories: new RepositoryResolver(git, config.markerSchemaVersion, stateRoot),
69
+ journal: new JournalStore(stateRoot),
70
+ outbox,
71
+ gate,
72
+ activeContexts,
73
+ repositoryDecisions,
74
+ updateChoices,
75
+ shadowNotices,
76
+ questionnaires: new QuestionnaireStore(stateRoot),
77
+ onboarding: new OnboardingStore(stateRoot),
78
+ clientVersion: options.clientVersion === undefined ? config.clientVersion : options.clientVersion,
79
+ principalState,
80
+ });
81
+ }
82
+ //# sourceMappingURL=create-bridge-service.js.map
@@ -0,0 +1,16 @@
1
+ import { registerAllTools } from '../mcp/server.js';
2
+ import { recordingHost } from '../mcp/tool-host.js';
3
+ import { createBridgeService } from './create-bridge-service.js';
4
+ export function collectTools(service, server) {
5
+ const recording = recordingHost(server);
6
+ registerAllTools(recording.host, service);
7
+ return recording.tools;
8
+ }
9
+ export function createRuntime(options) {
10
+ const service = createBridgeService({
11
+ ownerId: options.ownerId,
12
+ clientVersion: options.clientVersion,
13
+ });
14
+ return { tools: collectTools(service, options.server) };
15
+ }
16
+ //# sourceMappingURL=runtime-entry.js.map
@@ -0,0 +1,265 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { readFile, stat } from 'node:fs/promises';
3
+ import { isAbsolute, join, resolve } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { z } from 'zod';
6
+ export const currentReleaseSchema = z.object({
7
+ product: z.literal('engineering-memory'),
8
+ schemaVersion: z.literal(1),
9
+ path: z.string().min(1),
10
+ version: z.string().min(1).nullable().optional(),
11
+ sourceHash: z.string().optional(),
12
+ bridgeEntry: z.string().optional(),
13
+ gateEntry: z.string().optional(),
14
+ });
15
+ export const runtimeEntryRelativePath = ['dist', 'src', 'runtime', 'runtime-entry.js'];
16
+ const emptyObjectSchema = { type: 'object', properties: {} };
17
+ export async function readCurrentRelease(currentPath) {
18
+ let raw;
19
+ try {
20
+ raw = await readFile(currentPath, 'utf8');
21
+ }
22
+ catch (error) {
23
+ return { problem: `${currentPath} cannot be read: ${describe(error)}` };
24
+ }
25
+ let parsed;
26
+ try {
27
+ parsed = JSON.parse(raw);
28
+ }
29
+ catch {
30
+ return { problem: `${currentPath} is not valid JSON` };
31
+ }
32
+ const release = currentReleaseSchema.safeParse(parsed);
33
+ if (!release.success)
34
+ return { problem: `${currentPath} does not describe a release` };
35
+ const path = isAbsolute(release.data.path)
36
+ ? resolve(release.data.path)
37
+ : resolve(currentPath, '..', release.data.path);
38
+ return { release: release.data, path };
39
+ }
40
+ export async function importRelease(releasePath) {
41
+ const entry = join(releasePath, ...runtimeEntryRelativePath);
42
+ const loaded = (await import(pathToFileURL(entry).href));
43
+ if (typeof loaded.createRuntime !== 'function')
44
+ throw new Error(`${entry} does not export createRuntime`);
45
+ return { createRuntime: loaded.createRuntime };
46
+ }
47
+ export class RuntimeHost {
48
+ options;
49
+ ownerId;
50
+ registered = new Map();
51
+ specs = new Map();
52
+ loadedPath;
53
+ loadedVersion;
54
+ refreshing;
55
+ failedPath;
56
+ seen;
57
+ timer;
58
+ started = false;
59
+ constructor(options) {
60
+ this.options = options;
61
+ this.ownerId = options.ownerId ?? randomUUID();
62
+ this.loadedPath = resolve(options.release.path);
63
+ this.loadedVersion = options.release.version;
64
+ }
65
+ get version() {
66
+ return this.loadedVersion;
67
+ }
68
+ get releasePath() {
69
+ return this.loadedPath;
70
+ }
71
+ start() {
72
+ if (this.started)
73
+ return;
74
+ this.started = true;
75
+ this.apply(this.instantiate(this.options.release.module, this.options.release.version));
76
+ }
77
+ watch(intervalMs) {
78
+ if (this.timer || !this.options.currentPath)
79
+ return;
80
+ this.timer = setInterval(() => {
81
+ void this.refresh();
82
+ }, intervalMs);
83
+ this.timer.unref();
84
+ }
85
+ stop() {
86
+ if (this.timer)
87
+ clearInterval(this.timer);
88
+ this.timer = undefined;
89
+ }
90
+ async refresh() {
91
+ if (!this.options.currentPath || !this.started)
92
+ return false;
93
+ this.refreshing ??= this.reload().finally(() => {
94
+ this.refreshing = undefined;
95
+ });
96
+ return this.refreshing;
97
+ }
98
+ async reload() {
99
+ const currentPath = this.options.currentPath;
100
+ let marker;
101
+ try {
102
+ marker = await stat(currentPath);
103
+ }
104
+ catch {
105
+ return false;
106
+ }
107
+ if (this.seen && this.seen.mtimeMs === marker.mtimeMs && this.seen.size === marker.size)
108
+ return false;
109
+ this.seen = { mtimeMs: marker.mtimeMs, size: marker.size };
110
+ const current = await readCurrentRelease(currentPath);
111
+ if ('problem' in current) {
112
+ this.log(`Engineering Memory bridge keeps release ${this.describeLoaded()}: ${current.problem}`);
113
+ return false;
114
+ }
115
+ if (current.path === this.loadedPath || current.path === this.failedPath)
116
+ return false;
117
+ const version = current.release.version ?? null;
118
+ try {
119
+ const module = await (this.options.load ?? importRelease)(current.path);
120
+ this.apply(this.instantiate(module, version));
121
+ }
122
+ catch (error) {
123
+ this.failedPath = current.path;
124
+ this.log(`Engineering Memory bridge keeps release ${this.describeLoaded()} because ${current.path} could not be loaded: ${describe(error)}`);
125
+ return false;
126
+ }
127
+ this.loadedPath = current.path;
128
+ this.loadedVersion = version;
129
+ this.failedPath = undefined;
130
+ this.log(`Engineering Memory bridge switched to release ${this.describeLoaded()}`);
131
+ return true;
132
+ }
133
+ instantiate(module, version) {
134
+ const runtime = module.createRuntime({
135
+ ownerId: this.ownerId,
136
+ clientVersion: version,
137
+ server: this.options.server.server,
138
+ });
139
+ if (!runtime || !Array.isArray(runtime.tools))
140
+ throw new Error('createRuntime did not return a tool list');
141
+ const names = new Set();
142
+ for (const spec of runtime.tools) {
143
+ if (typeof spec.name !== 'string' || !spec.name)
144
+ throw new Error('createRuntime returned a tool without a name');
145
+ if (names.has(spec.name))
146
+ throw new Error(`Tool ${spec.name} is defined twice`);
147
+ if (typeof spec.callback !== 'function')
148
+ throw new Error(`Tool ${spec.name} has no callback`);
149
+ names.add(spec.name);
150
+ }
151
+ return runtime;
152
+ }
153
+ apply(runtime) {
154
+ const names = new Set();
155
+ const register = this.options.server.registerTool.bind(this.options.server);
156
+ for (const spec of runtime.tools) {
157
+ names.add(spec.name);
158
+ this.specs.set(spec.name, spec);
159
+ const signature = toolSignature(spec.config);
160
+ const existing = this.registered.get(spec.name);
161
+ if (!existing) {
162
+ const tool = register(spec.name, { ...spec.config, inputSchema: this.inputSchemaOf(spec.name) }, this.dispatcher(spec.name));
163
+ this.registered.set(spec.name, { tool, signature });
164
+ }
165
+ else if (existing.signature !== signature) {
166
+ existing.tool.update({
167
+ ...(spec.config.title !== undefined ? { title: spec.config.title } : {}),
168
+ ...(spec.config.description !== undefined
169
+ ? { description: spec.config.description }
170
+ : {}),
171
+ ...(spec.config.outputSchema !== undefined
172
+ ? { outputSchema: spec.config.outputSchema }
173
+ : {}),
174
+ ...(spec.config.annotations !== undefined
175
+ ? { annotations: spec.config.annotations }
176
+ : {}),
177
+ });
178
+ existing.signature = signature;
179
+ }
180
+ }
181
+ for (const [name, entry] of [...this.registered]) {
182
+ if (names.has(name))
183
+ continue;
184
+ entry.tool.remove();
185
+ this.registered.delete(name);
186
+ this.specs.delete(name);
187
+ }
188
+ }
189
+ inputSchemaOf(name) {
190
+ const jsonSchema = (io) => (options) => {
191
+ const schema = this.specs.get(name)?.config.inputSchema;
192
+ if (!schema)
193
+ return emptyObjectSchema;
194
+ return schema['~standard'].jsonSchema[io](options);
195
+ };
196
+ const props = {
197
+ version: 1,
198
+ vendor: 'engineering-memory',
199
+ validate: (value) => ({ value }),
200
+ jsonSchema: { input: jsonSchema('input'), output: jsonSchema('output') },
201
+ };
202
+ return { '~standard': props };
203
+ }
204
+ dispatcher(name) {
205
+ return async (args, context) => {
206
+ await this.refresh();
207
+ const spec = this.specs.get(name);
208
+ if (!spec)
209
+ throw new Error(`Tool ${name} is no longer provided by this bridge release; list the tools again and use the current lifecycle.`);
210
+ const schema = spec.config.inputSchema;
211
+ if (!schema)
212
+ return spec.callback(context);
213
+ const outcome = await schema['~standard'].validate(args ?? {});
214
+ if (outcome.issues && outcome.issues.length > 0)
215
+ return {
216
+ content: [
217
+ {
218
+ type: 'text',
219
+ text: `Input validation error: Invalid arguments for tool ${name}: ${outcome.issues
220
+ .map((issue) => formatIssue(issue))
221
+ .join(', ')}`,
222
+ },
223
+ ],
224
+ isError: true,
225
+ };
226
+ return spec.callback(outcome.value, context);
227
+ };
228
+ }
229
+ describeLoaded() {
230
+ return this.loadedVersion ?? this.loadedPath;
231
+ }
232
+ log(message) {
233
+ this.options.log?.(message);
234
+ }
235
+ }
236
+ export function toolSignature(config) {
237
+ return JSON.stringify({
238
+ title: config.title ?? null,
239
+ description: config.description ?? null,
240
+ annotations: config.annotations ?? null,
241
+ input: jsonSchemaOf(config.inputSchema),
242
+ output: jsonSchemaOf(config.outputSchema),
243
+ });
244
+ }
245
+ function jsonSchemaOf(schema) {
246
+ if (!schema)
247
+ return null;
248
+ try {
249
+ return schema['~standard'].jsonSchema.input({ target: 'draft-2020-12' });
250
+ }
251
+ catch {
252
+ return randomUUID();
253
+ }
254
+ }
255
+ function formatIssue(issue) {
256
+ if (!issue.path?.length)
257
+ return issue.message;
258
+ return `${issue.path
259
+ .map((segment) => String(typeof segment === 'object' && segment !== null ? segment.key : segment))
260
+ .join('.')}: ${issue.message}`;
261
+ }
262
+ function describe(error) {
263
+ return error instanceof Error ? error.message : String(error);
264
+ }
265
+ //# sourceMappingURL=runtime-host.js.map
@@ -239,6 +239,12 @@ export class WorktreePool {
239
239
  .flatMap((p) => p.entries)
240
240
  .find((e) => e.taskId === taskId && e.phase !== 'released') ?? null));
241
241
  }
242
+ owns(entry) {
243
+ return (entry.phase !== 'released' &&
244
+ entry.owner !== null &&
245
+ entry.owner.id === this.ownerId &&
246
+ entry.owner.pid === process.pid);
247
+ }
242
248
  async forPath(projectId, repoRoot) {
243
249
  return this.transaction(async (registry) => structuredClone(registry.projects[projectId]?.entries.find((e) => key(e.repoRoot) === key(repoRoot)) ??
244
250
  null));