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.
- package/dispatcher/sections.mjs +7 -3
- package/install/codex-approval.mjs +457 -0
- package/install/files.mjs +119 -9
- package/install/installer.mjs +44 -5
- package/install/launcher.mjs +95 -0
- package/package.json +1 -1
- package/runtime/build.json +1 -1
- package/runtime/dist/src/config.js +4 -0
- package/runtime/dist/src/git/git-inspector.js +82 -3
- package/runtime/dist/src/git/verification-gate.js +30 -17
- package/runtime/dist/src/index.js +25 -81
- package/runtime/dist/src/mcp/onboarding-tools.js +3 -16
- package/runtime/dist/src/mcp/questionnaire-tools.js +99 -15
- package/runtime/dist/src/mcp/server.js +14 -7
- package/runtime/dist/src/mcp/tool-annotations.js +111 -0
- package/runtime/dist/src/mcp/tool-definitions.js +148 -28
- package/runtime/dist/src/mcp/tool-host.js +12 -0
- package/runtime/dist/src/mcp/worktree-tools.js +323 -236
- package/runtime/dist/src/project/repository.js +11 -5
- package/runtime/dist/src/runtime/active-context-store.js +3 -0
- package/runtime/dist/src/runtime/branch-preferences.js +0 -19
- package/runtime/dist/src/runtime/bridge-service.js +1163 -231
- package/runtime/dist/src/runtime/create-bridge-service.js +82 -0
- package/runtime/dist/src/runtime/offline-outbox.js +33 -158
- package/runtime/dist/src/runtime/phase-timer.js +26 -0
- package/runtime/dist/src/runtime/privacy-detector.js +263 -0
- package/runtime/dist/src/runtime/questionnaire-store.js +210 -49
- package/runtime/dist/src/runtime/recovery-error.js +3 -1
- package/runtime/dist/src/runtime/runtime-entry.js +24 -0
- package/runtime/dist/src/runtime/runtime-host.js +263 -0
- package/runtime/dist/src/runtime/task-branch-store.js +17 -1
- package/runtime/dist/src/runtime/task-start.js +281 -0
- package/runtime/dist/src/runtime/worktree-pool.js +340 -24
- package/runtime/dist/src/utilities/process.js +1 -0
- package/skill/SKILL.md +3 -3
- package/skill/references/lifecycle.md +44 -19
- package/skill/references/memory-updates.md +9 -3
- package/skill/references/project-onboarding.md +19 -2
- package/skill/references/questionnaires.md +77 -15
package/install/installer.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
171
|
+
const runtime = await installManagedBridgeRuntime(
|
|
157
172
|
bridgeRuntimeSource,
|
|
158
|
-
|
|
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:
|
|
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.
|
|
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",
|
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),
|
|
@@ -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',
|
|
363
|
-
this.runner.run('git', ['diff',
|
|
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:
|
|
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 !==
|
|
177
|
+
if (receipt.diffHash !== current.diffHash) {
|
|
167
178
|
return {
|
|
168
179
|
allowed: false,
|
|
169
180
|
reason: 'git_diff_changed_after_verify',
|
|
170
|
-
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:
|
|
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:
|
|
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:
|
|
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:
|
|
201
|
-
?
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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:
|
|
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 !==
|
|
230
|
+
result.diffHash !== current.diffHash) {
|
|
218
231
|
return {
|
|
219
232
|
allowed: false,
|
|
220
233
|
reason: 'online_commit_gate_mismatch',
|
|
221
|
-
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:
|
|
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:
|
|
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 {
|
|
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
|
|
@@ -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
|
|
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 };
|