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.
- package/dispatcher/sections.mjs +1 -1
- package/install/files.mjs +114 -8
- package/install/installer.mjs +20 -4
- package/install/launcher.mjs +95 -0
- package/package.json +1 -1
- package/runtime/build.json +1 -1
- package/runtime/dist/src/config.js +3 -0
- package/runtime/dist/src/index.js +25 -81
- package/runtime/dist/src/mcp/server.js +14 -7
- package/runtime/dist/src/mcp/tool-host.js +12 -0
- package/runtime/dist/src/runtime/bridge-service.js +32 -11
- package/runtime/dist/src/runtime/create-bridge-service.js +82 -0
- package/runtime/dist/src/runtime/runtime-entry.js +16 -0
- package/runtime/dist/src/runtime/runtime-host.js +265 -0
- package/runtime/dist/src/runtime/worktree-pool.js +6 -0
package/dispatcher/sections.mjs
CHANGED
|
@@ -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
|
|
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')
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 ?? []);
|
package/install/installer.mjs
CHANGED
|
@@ -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
|
|
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
|
|
158
|
+
const runtime = await installManagedBridgeRuntime(
|
|
157
159
|
bridgeRuntimeSource,
|
|
158
|
-
|
|
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:
|
|
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.
|
|
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",
|
package/runtime/build.json
CHANGED
|
@@ -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 {
|
|
3
|
-
import {
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
6
|
-
|
|
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:
|
|
11
|
+
instructions: serverInstructions,
|
|
11
12
|
});
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
...(
|
|
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.
|
|
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 =
|
|
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 =
|
|
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.
|
|
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));
|