engineering-memory 1.11.8 → 1.11.10

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.
Files changed (39) hide show
  1. package/dispatcher/sections.mjs +7 -3
  2. package/install/codex-approval.mjs +457 -0
  3. package/install/files.mjs +119 -9
  4. package/install/installer.mjs +44 -5
  5. package/install/launcher.mjs +95 -0
  6. package/package.json +1 -1
  7. package/runtime/build.json +1 -1
  8. package/runtime/dist/src/config.js +4 -0
  9. package/runtime/dist/src/git/git-inspector.js +82 -3
  10. package/runtime/dist/src/git/verification-gate.js +30 -17
  11. package/runtime/dist/src/index.js +25 -81
  12. package/runtime/dist/src/mcp/onboarding-tools.js +3 -16
  13. package/runtime/dist/src/mcp/questionnaire-tools.js +99 -15
  14. package/runtime/dist/src/mcp/server.js +14 -7
  15. package/runtime/dist/src/mcp/tool-annotations.js +111 -0
  16. package/runtime/dist/src/mcp/tool-definitions.js +148 -28
  17. package/runtime/dist/src/mcp/tool-host.js +12 -0
  18. package/runtime/dist/src/mcp/worktree-tools.js +323 -236
  19. package/runtime/dist/src/project/repository.js +11 -5
  20. package/runtime/dist/src/runtime/active-context-store.js +3 -0
  21. package/runtime/dist/src/runtime/branch-preferences.js +0 -19
  22. package/runtime/dist/src/runtime/bridge-service.js +1163 -231
  23. package/runtime/dist/src/runtime/create-bridge-service.js +82 -0
  24. package/runtime/dist/src/runtime/offline-outbox.js +33 -158
  25. package/runtime/dist/src/runtime/phase-timer.js +26 -0
  26. package/runtime/dist/src/runtime/privacy-detector.js +263 -0
  27. package/runtime/dist/src/runtime/questionnaire-store.js +210 -49
  28. package/runtime/dist/src/runtime/recovery-error.js +3 -1
  29. package/runtime/dist/src/runtime/runtime-entry.js +24 -0
  30. package/runtime/dist/src/runtime/runtime-host.js +263 -0
  31. package/runtime/dist/src/runtime/task-branch-store.js +17 -1
  32. package/runtime/dist/src/runtime/task-start.js +281 -0
  33. package/runtime/dist/src/runtime/worktree-pool.js +340 -24
  34. package/runtime/dist/src/utilities/process.js +1 -0
  35. package/skill/SKILL.md +3 -3
  36. package/skill/references/lifecycle.md +44 -19
  37. package/skill/references/memory-updates.md +9 -3
  38. package/skill/references/project-onboarding.md +19 -2
  39. package/skill/references/questionnaires.md +77 -15
@@ -4,14 +4,21 @@ import { fileURLToPath } from 'node:url';
4
4
  import { dispatcherSections } from '../dispatcher/sections.mjs';
5
5
  import { upsertManagedSection } from '../dispatcher/managed-section.mjs';
6
6
  import { normalizeApiUrl } from './api-url.mjs';
7
+ import {
8
+ applyCodexApproval,
9
+ codexHome,
10
+ planCodexApproval,
11
+ } from './codex-approval.mjs';
7
12
  import { runCommand } from './commands.mjs';
8
13
  import {
9
14
  exists,
10
15
  installManagedBridgeRuntime,
11
16
  installManagedDirectory,
12
17
  InstallTransaction,
18
+ pruneReleases,
13
19
  readJson,
14
20
  readText,
21
+ rememberFile,
15
22
  replaceFile,
16
23
  writeJson,
17
24
  } from './files.mjs';
@@ -54,7 +61,8 @@ export async function installEngineeringMemory(options = {}) {
54
61
  options.gateEntry ??
55
62
  join(bridgeRuntimeSource, 'dist', 'src', 'git', 'pre-commit.js'),
56
63
  );
57
- const runtimePath = join(homeDir, '.engineering-memory', 'runtime', 'bridge');
64
+ const runtimeRoot = join(homeDir, '.engineering-memory', 'runtime');
65
+ const runtimePath = join(runtimeRoot, 'bridge');
58
66
  const bridgeEntry = stableRuntimeEntry(
59
67
  bridgeRuntimeSource,
60
68
  runtimePath,
@@ -93,7 +101,8 @@ export async function installEngineeringMemory(options = {}) {
93
101
  `Refusing to overwrite unmanaged installer state: ${statePath}`,
94
102
  );
95
103
  }
96
- const codexDispatcherPath = join(homeDir, '.codex', 'AGENTS.md');
104
+ const codexConfigHome = codexHome(homeDir, options.env);
105
+ const codexDispatcherPath = join(codexConfigHome, 'AGENTS.md');
97
106
  const claudeDispatcherPath = join(homeDir, '.claude', 'CLAUDE.md');
98
107
  const clientTargets = {
99
108
  codex: {
@@ -141,6 +150,12 @@ export async function installEngineeringMemory(options = {}) {
141
150
  };
142
151
  }),
143
152
  );
153
+ const codexApprovalPlan = installedClients.includes('codex')
154
+ ? await planCodexApproval({
155
+ codexHome: codexConfigHome,
156
+ state: existingState?.mcp?.codex?.approval,
157
+ })
158
+ : null;
144
159
  const hookPlan = await planGitHook({
145
160
  repoRoot: options.repoRoot ? resolve(options.repoRoot) : null,
146
161
  mode: options.hookMode ?? 'cancel',
@@ -153,15 +168,29 @@ export async function installEngineeringMemory(options = {}) {
153
168
  const transaction = new InstallTransaction();
154
169
  let result;
155
170
  try {
156
- const runtimeHash = await installManagedBridgeRuntime(
171
+ const runtime = await installManagedBridgeRuntime(
157
172
  bridgeRuntimeSource,
158
- runtimePath,
173
+ runtimeRoot,
159
174
  transaction,
175
+ {
176
+ clientVersion,
177
+ bridgeEntry: relative(bridgeRuntimeSource, sourceBridgeEntry),
178
+ gateEntry: relative(bridgeRuntimeSource, sourceGateEntry),
179
+ },
160
180
  );
181
+ if (codexApprovalPlan) {
182
+ await rememberFile(codexApprovalPlan.path, transaction);
183
+ }
161
184
  const mcp = {
162
185
  ...(existingState?.mcp ?? {}),
163
186
  ...(await applyMcpRegistrations(mcpPlans, commandRunner, transaction)),
164
187
  };
188
+ const codexApproval = codexApprovalPlan
189
+ ? await applyCodexApproval(codexApprovalPlan, transaction)
190
+ : null;
191
+ if (codexApproval && !codexApproval.skipped) {
192
+ mcp.codex = { ...mcp.codex, approval: codexApprovalPlan.intended };
193
+ }
165
194
  let skillHash;
166
195
  for (const plan of clientPlans) {
167
196
  const installedHash = await installManagedDirectory(
@@ -183,7 +212,8 @@ export async function installEngineeringMemory(options = {}) {
183
212
  skillHash,
184
213
  runtime: {
185
214
  path: runtimePath,
186
- sourceHash: runtimeHash,
215
+ sourceHash: runtime.sourceHash,
216
+ release: runtime.releasePath,
187
217
  bridgeEntry,
188
218
  gateEntry,
189
219
  },
@@ -206,9 +236,11 @@ export async function installEngineeringMemory(options = {}) {
206
236
  apiUrl,
207
237
  clientVersion,
208
238
  runtimePath,
239
+ release: runtime.releasePath,
209
240
  bridgeEntry,
210
241
  gateEntry,
211
242
  hook: hookResult,
243
+ ...(codexApproval ? { codexApproval } : {}),
212
244
  };
213
245
  } catch (error) {
214
246
  try {
@@ -222,6 +254,13 @@ export async function installEngineeringMemory(options = {}) {
222
254
  throw error;
223
255
  }
224
256
  await transaction.commit();
257
+ try {
258
+ result.prunedReleases = await pruneReleases(runtimeRoot);
259
+ } catch (error) {
260
+ result.prunedReleases = [];
261
+ result.releasePruneProblem =
262
+ error instanceof Error ? error.message : String(error);
263
+ }
225
264
  return result;
226
265
  }
227
266
 
@@ -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.10",
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": "875304dd11735e1bf2817ae59e75b54186e5071a"
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),
@@ -58,6 +61,7 @@ export const endpoints = {
58
61
  taskVerify: '/runtime/task/verify',
59
62
  taskClose: '/runtime/task/close',
60
63
  taskAbandon: '/runtime/task/abandon',
64
+ taskStatus: (taskId) => `/runtime/task/${taskId}/status`,
61
65
  taskCommitGate: '/runtime/task/commit-gate',
62
66
  projectSetup: '/projects/setup',
63
67
  projectList: '/projects',
@@ -242,6 +242,39 @@ export class GitInspector {
242
242
  ]);
243
243
  return ref ? ref.replace(/^origin\//, '') : null;
244
244
  }
245
+ async pushDestination(repoRoot) {
246
+ const branch = await this.currentBranch(repoRoot);
247
+ const upstream = branch
248
+ ? await this.gitValue(repoRoot, ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'])
249
+ : null;
250
+ const listed = await this.runner.run('git', ['remote'], { cwd: repoRoot });
251
+ const remotes = listed.exitCode === 0 ? listed.stdout.split(/\r?\n/).filter(Boolean) : [];
252
+ const configured = (branch &&
253
+ (await this.gitValue(repoRoot, ['config', '--get', `branch.${branch}.pushRemote`]))) ||
254
+ (await this.gitValue(repoRoot, ['config', '--get', 'remote.pushDefault'])) ||
255
+ (branch && (await this.gitValue(repoRoot, ['config', '--get', `branch.${branch}.remote`]))) ||
256
+ 'origin';
257
+ const remote = remotes.includes(configured) ? configured : null;
258
+ const pushUrl = remote
259
+ ? await this.gitValue(repoRoot, ['remote', 'get-url', '--push', remote])
260
+ : null;
261
+ const remoteHead = remote
262
+ ? await this.gitValue(repoRoot, [
263
+ 'symbolic-ref',
264
+ '--short',
265
+ '-q',
266
+ `refs/remotes/${remote}/HEAD`,
267
+ ])
268
+ : null;
269
+ return {
270
+ branch,
271
+ upstream,
272
+ remote,
273
+ pushUrl: pushUrl ? withoutCredentials(pushUrl) : null,
274
+ remoteDefaultBranch: remoteHead ? remoteHead.slice(`${remote}/`.length) : null,
275
+ headCommit: await this.gitValue(repoRoot, ['rev-parse', '--verify', '-q', 'HEAD']),
276
+ };
277
+ }
245
278
  async protectedBranchNames(repoRoot) {
246
279
  const names = new Set(['main', 'master', 'develop', 'prod']);
247
280
  const fallback = await this.defaultBranch(repoRoot);
@@ -286,6 +319,14 @@ export class GitInspector {
286
319
  }
287
320
  return await canonicalPath(resolve(commonDirectory, '..'));
288
321
  }
322
+ async identity(repoRoot) {
323
+ const root = await this.findRoot(repoRoot);
324
+ const [head, branch] = await Promise.all([
325
+ this.gitValue(root, ['rev-parse', 'HEAD']),
326
+ this.currentBranch(root),
327
+ ]);
328
+ return { repoRoot: root, head, branch };
329
+ }
289
330
  async manifest(repoRoot) {
290
331
  const root = await this.findRoot(repoRoot);
291
332
  const head = await this.gitValue(root, ['rev-parse', 'HEAD']);
@@ -301,6 +342,30 @@ export class GitInspector {
301
342
  diffHash: sha256(`${head ?? '<unborn>'}\n${stableStringify(hashManifest)}\n`),
302
343
  };
303
344
  }
345
+ async manifestAgainst(repoRoot, baseCommit) {
346
+ const root = await this.findRoot(repoRoot);
347
+ const head = await this.gitValue(root, ['rev-parse', 'HEAD']);
348
+ const changedPaths = await this.changedPathsAgainstHead(root, baseCommit);
349
+ const hashManifest = canonicalHashManifest(changedPaths);
350
+ return {
351
+ repoRoot: root,
352
+ head,
353
+ baseCommit,
354
+ changedPaths,
355
+ worktreeHash: sha256(`${stableStringify(hashManifest)}\n`),
356
+ diffHash: sha256(`${baseCommit}\n${head ?? '<unborn>'}\n${stableStringify(hashManifest)}\n`),
357
+ };
358
+ }
359
+ async isAncestor(repoRoot, ancestor, descendant = 'HEAD') {
360
+ const result = await this.runner.run('git', ['merge-base', '--is-ancestor', ancestor, descendant], { cwd: repoRoot });
361
+ return result.exitCode === 0;
362
+ }
363
+ async committedPaths(repoRoot, baseCommit) {
364
+ const result = await this.runner.run('git', ['diff', '--name-only', '-z', '--find-renames', baseCommit, 'HEAD', '--'], { cwd: repoRoot });
365
+ if (result.exitCode !== 0)
366
+ throw new Error(`Git committed diff failed: ${result.stderr.trim()}`);
367
+ return result.stdout.split('\0').filter(Boolean).map(normalizeGitPath).sort();
368
+ }
304
369
  async stagedManifest(repoRoot) {
305
370
  const root = await this.findRoot(repoRoot);
306
371
  let result = await this.runner.run('git', ['diff', '--cached', '--name-status', '-z', '--find-renames', '--find-copies', 'HEAD', '--'], { cwd: root });
@@ -357,10 +422,10 @@ export class GitInspector {
357
422
  }
358
423
  return result.sort((left, right) => left.path.localeCompare(right.path));
359
424
  }
360
- async changedPathsAgainstHead(repoRoot) {
425
+ async changedPathsAgainstHead(repoRoot, ref = 'HEAD') {
361
426
  const [tracked, raw, untracked] = await Promise.all([
362
- this.runner.run('git', ['diff', 'HEAD', '--name-status', '-z', '--find-renames', '--find-copies', '--'], { cwd: repoRoot }),
363
- this.runner.run('git', ['diff', 'HEAD', '--raw', '-z', '--no-abbrev', '--'], {
427
+ this.runner.run('git', ['diff', ref, '--name-status', '-z', '--find-renames', '--find-copies', '--'], { cwd: repoRoot }),
428
+ this.runner.run('git', ['diff', ref, '--raw', '-z', '--no-abbrev', '--'], {
364
429
  cwd: repoRoot,
365
430
  }),
366
431
  this.runner.run('git', ['ls-files', '--others', '--exclude-standard', '-z'], {
@@ -699,6 +764,20 @@ function canonicalRemoteIdentity(remote) {
699
764
  return `local/${normalizeRemotePath(trimmed.replace(/[?#].*$/, ''))}`;
700
765
  }
701
766
  }
767
+ function withoutCredentials(remote) {
768
+ if (!/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(remote))
769
+ return remote;
770
+ try {
771
+ const url = new URL(remote);
772
+ if (url.protocol === 'http:' || url.protocol === 'https:')
773
+ url.username = ''; // a pasted token sits here
774
+ url.password = '';
775
+ return url.toString();
776
+ }
777
+ catch {
778
+ return remote.replace(/\/\/[^/@]*@/, '//');
779
+ }
780
+ }
702
781
  function normalizeRemotePath(path) {
703
782
  const normalized = path
704
783
  .replace(/\\/g, '/')
@@ -151,23 +151,34 @@ export class VerificationGate {
151
151
  };
152
152
  }
153
153
  const actualSource = receipt.source ? await this.git.sourceIdentity(repoRoot) : null;
154
+ const ahead = receipt.branch &&
155
+ receipt.source &&
156
+ actualSource &&
157
+ actualSource.sourceCommit !== receipt.source.sourceCommit &&
158
+ (await this.git.isAncestor(repository.repoRoot, receipt.source.sourceCommit)) &&
159
+ (await this.git.committedPaths(repository.repoRoot, receipt.source.sourceCommit)).every((path) => receipt.taskChanges.some((change) => change.path === path))
160
+ ? await this.git.manifestAgainst(repository.repoRoot, receipt.source.sourceCommit)
161
+ : null;
162
+ const committedAhead = ahead?.diffHash === receipt.diffHash;
163
+ const current = ahead && committedAhead ? ahead : manifest;
154
164
  if (receipt.source &&
165
+ !committedAhead &&
155
166
  (!actualSource ||
156
167
  actualSource.sourceCommit !== receipt.source.sourceCommit ||
157
168
  actualSource.treeHash !== receipt.source.treeHash)) {
158
169
  return {
159
170
  allowed: false,
160
171
  reason: 'source_commit_mismatch',
161
- diffHash: manifest.diffHash,
172
+ diffHash: current.diffHash,
162
173
  taskId: receipt.taskId,
163
174
  details: ['Return to the task source commit or start a task for the current source.'],
164
175
  };
165
176
  }
166
- if (receipt.diffHash !== manifest.diffHash) {
177
+ if (receipt.diffHash !== current.diffHash) {
167
178
  return {
168
179
  allowed: false,
169
180
  reason: 'git_diff_changed_after_verify',
170
- diffHash: manifest.diffHash,
181
+ diffHash: current.diffHash,
171
182
  taskId: receipt.taskId,
172
183
  };
173
184
  }
@@ -175,7 +186,7 @@ export class VerificationGate {
175
186
  return {
176
187
  allowed: false,
177
188
  reason: 'no_task_changes_staged',
178
- diffHash: manifest.diffHash,
189
+ diffHash: current.diffHash,
179
190
  taskId: receipt.taskId,
180
191
  };
181
192
  }
@@ -183,7 +194,7 @@ export class VerificationGate {
183
194
  return {
184
195
  allowed: false,
185
196
  reason: 'staged_change_outside_task',
186
- diffHash: manifest.diffHash,
197
+ diffHash: current.diffHash,
187
198
  taskId: receipt.taskId,
188
199
  };
189
200
  }
@@ -191,22 +202,24 @@ export class VerificationGate {
191
202
  return {
192
203
  allowed: false,
193
204
  reason: 'online_commit_gate_unavailable',
194
- diffHash: manifest.diffHash,
205
+ diffHash: current.diffHash,
195
206
  taskId: receipt.taskId,
196
207
  };
197
208
  }
198
209
  try {
199
210
  const attestation = await this.commitGateAttestor({
200
- source: actualSource
201
- ? {
202
- ...actualSource,
203
- ...(receipt.source?.linked ? { linked: receipt.source.linked } : {}),
204
- }
205
- : undefined,
211
+ source: committedAhead
212
+ ? receipt.source
213
+ : actualSource
214
+ ? {
215
+ ...actualSource,
216
+ ...(receipt.source?.linked ? { linked: receipt.source.linked } : {}),
217
+ }
218
+ : undefined,
206
219
  taskId: receipt.taskId,
207
220
  projectId: receipt.projectId,
208
221
  repoFingerprint: receipt.repoFingerprint,
209
- diffHash: manifest.diffHash,
222
+ diffHash: current.diffHash,
210
223
  expectedTaskVersion: receipt.taskVersion,
211
224
  branch: branch ?? undefined,
212
225
  });
@@ -214,11 +227,11 @@ export class VerificationGate {
214
227
  if (result?.allowed !== true ||
215
228
  result.taskId !== receipt.taskId ||
216
229
  result.taskVersion !== receipt.taskVersion ||
217
- result.diffHash !== manifest.diffHash) {
230
+ result.diffHash !== current.diffHash) {
218
231
  return {
219
232
  allowed: false,
220
233
  reason: 'online_commit_gate_mismatch',
221
- diffHash: manifest.diffHash,
234
+ diffHash: current.diffHash,
222
235
  taskId: receipt.taskId,
223
236
  };
224
237
  }
@@ -227,7 +240,7 @@ export class VerificationGate {
227
240
  return {
228
241
  allowed: false,
229
242
  reason: 'online_commit_gate_rejected',
230
- diffHash: manifest.diffHash,
243
+ diffHash: current.diffHash,
231
244
  taskId: receipt.taskId,
232
245
  details: [error instanceof Error ? error.message : String(error)],
233
246
  };
@@ -235,7 +248,7 @@ export class VerificationGate {
235
248
  return {
236
249
  allowed: true,
237
250
  reason: 'verified',
238
- diffHash: manifest.diffHash,
251
+ diffHash: current.diffHash,
239
252
  taskId: receipt.taskId,
240
253
  };
241
254
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  import * as z from 'zod/v4';
2
2
  import { sha256, stableStringify } from '../utilities/hash.js';
3
- import { askQuestionnaire } from './questionnaire-tools.js';
3
+ import { answerChoice, answerData, askQuestionnaire } from './questionnaire-tools.js';
4
4
  const uuid = z.string().uuid();
5
5
  const hash = z.string().regex(/^[a-f0-9]{64}$/);
6
6
  const project = { projectId: uuid };
@@ -468,7 +468,7 @@ export function registerOnboardingTools(server, service) {
468
468
  inputSchema: z.object(run),
469
469
  }, async (input) => output(await service.onboardingRequest('verify', body(input))));
470
470
  server.registerTool('memory.catalog', {
471
- description: 'Page accessible memory resource headers by id. Read all pages using the last id when a page is full. includeArchived exposes retired identities for refresh and rename preservation.',
471
+ description: 'Page accessible memory resource headers by id: each entry names its current revision and its size in characters instead of carrying its content. Pass the response nextAfterId as afterId to read the next page; nextAfterId is null once nothing more remains. includeArchived exposes retired identities for refresh and rename preservation.',
472
472
  inputSchema: z.object({
473
473
  ...project,
474
474
  ...page,
@@ -479,7 +479,7 @@ export function registerOnboardingTools(server, service) {
479
479
  }),
480
480
  }, async (input) => output(await service.onboardingRequest('catalog', body(input))));
481
481
  server.registerTool('memory.read_revisions', {
482
- description: 'Read selected immutable revisions in an authorized context session and record receipts. Bounds keep large project learning independent of normal task context limits.',
482
+ description: 'Read selected immutable revisions in an authorized context session and record receipts. One response carries what fits its size budget, in the order requested, as items; the revisions it did not carry are listed in remainingRevisionIds, to pass to the next call. Bounds keep large project learning independent of normal task context limits.',
483
483
  inputSchema: z.object({
484
484
  ...project,
485
485
  sessionId: uuid,
@@ -533,19 +533,6 @@ export function registerOnboardingTools(server, service) {
533
533
  })));
534
534
  });
535
535
  }
536
- function answerChoice(result) {
537
- return answerData(result)?.choice;
538
- }
539
- function answerData(result) {
540
- const response = result;
541
- const text = response.content?.find((item) => item.type === 'text')?.text;
542
- if (!text)
543
- return undefined;
544
- const value = JSON.parse(text);
545
- return value.ok && value.data?.status === 'answered' && value.data.answerAvailable
546
- ? value.data.answer
547
- : undefined;
548
- }
549
536
  async function approveOnboarding(server, service, input, context, initialize) {
550
537
  const preparation = !input.projectId;
551
538
  const scope = { repoRoot: input.repoRoot, preparation };