engineering-memory 1.7.0 → 1.9.0
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 +2 -0
- package/install/commands.mjs +39 -10
- package/package.json +1 -1
- package/runtime/dist/src/config.js +9 -1
- package/runtime/dist/src/git/git-inspector.js +140 -17
- package/runtime/dist/src/git/pre-commit.js +16 -0
- package/runtime/dist/src/git/verification-gate.js +14 -1
- package/runtime/dist/src/mcp/tool-definitions.js +101 -5
- package/runtime/dist/src/project/repository.js +1 -1
- package/runtime/dist/src/runtime/active-context-store.js +13 -7
- package/runtime/dist/src/runtime/api-client.js +62 -12
- package/runtime/dist/src/runtime/bridge-service.js +357 -21
- package/runtime/dist/src/runtime/principal-state.js +3 -2
- package/runtime/dist/src/runtime/recovery-error.js +9 -0
- package/runtime/dist/src/runtime/task-branch-store.js +111 -0
- package/skill/SKILL.md +2 -0
- package/skill/references/lifecycle.md +15 -9
- package/skill/references/memory-updates.md +3 -1
- package/skill/references/questionnaires.md +6 -4
- package/skill/references/scaffolding.md +9 -8
- package/bin/entry-point.test.mjs +0 -29
package/dispatcher/sections.mjs
CHANGED
|
@@ -21,6 +21,8 @@ After compaction, a new chat, interruption, or handoff, call \`session.resume\`
|
|
|
21
21
|
|
|
22
22
|
Do not edit until the skill lifecycle has completed discovery, its checkpoint, and \`context.prepare_change\`. Do not claim completion until \`task.verify\` succeeds.
|
|
23
23
|
|
|
24
|
+
Use the host's native questionnaire for every question to the user, including implementation choices, names, clarification, branch/worktree decisions and delivery. In Codex use request_user_input when available; in Claude use AskUserQuestion. Never replace the questionnaire with a chat instruction such as 'type this', 'reply yes', or 'write X if you want Y'. Do not open a survey web page. If the required native control is unavailable or prohibited for that kind of question, follow the host's tool restrictions, explain the limitation, and continue only work already authorized; do not fabricate a survey or silently choose an answer. Existing answers remain valid through retries and handoffs.
|
|
25
|
+
|
|
24
26
|
When the repository is unbound, use the native questionnaire required by the skill. Do not silently create or bind a project. Do not open a survey web page.`;
|
|
25
27
|
|
|
26
28
|
export const dispatcherSections = Object.freeze({
|
package/install/commands.mjs
CHANGED
|
@@ -6,16 +6,19 @@ import { delimiter, join } from 'node:path';
|
|
|
6
6
|
const metaCharacters = /([()\][%!^"`<>&|;, *?])/g;
|
|
7
7
|
const defaultPathExtensions = '.COM;.EXE;.BAT;.CMD';
|
|
8
8
|
const shimExtension = /\.(?:cmd|bat)$/i;
|
|
9
|
+
const npmShimPath = /node_modules[\\/]\.bin[\\/][^\\/]+\.cmd$/i;
|
|
9
10
|
const pathSeparators = /[/\\]/;
|
|
10
11
|
|
|
11
12
|
export async function runCommand(command, args, options = {}) {
|
|
12
13
|
const environment = options.env ?? process.env;
|
|
13
14
|
const invocation = await planInvocation(command, args, environment, options);
|
|
15
|
+
const captureOutput = options.captureOutput !== false;
|
|
14
16
|
return await new Promise((resolve) => {
|
|
15
17
|
const child = spawn(invocation.command, invocation.args, {
|
|
16
18
|
cwd: options.cwd,
|
|
17
19
|
env: environment,
|
|
18
20
|
shell: false,
|
|
21
|
+
...(captureOutput ? {} : { stdio: 'ignore' }),
|
|
19
22
|
windowsHide: true,
|
|
20
23
|
windowsVerbatimArguments: invocation.verbatim,
|
|
21
24
|
});
|
|
@@ -38,6 +41,24 @@ export async function runCommand(command, args, options = {}) {
|
|
|
38
41
|
});
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
export function prependEnvironmentPath(environment, directory) {
|
|
45
|
+
const pathKeys = Object.keys(environment).filter(
|
|
46
|
+
(key) => key.toLowerCase() === 'path',
|
|
47
|
+
);
|
|
48
|
+
const pathKey =
|
|
49
|
+
pathKeys.find((key) => Boolean(environment[key])) ?? pathKeys[0] ?? 'PATH';
|
|
50
|
+
const current = pathKeys
|
|
51
|
+
.map((key) => environment[key])
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.join(delimiter);
|
|
54
|
+
environment[pathKey] = current
|
|
55
|
+
? `${directory}${delimiter}${current}`
|
|
56
|
+
: directory;
|
|
57
|
+
for (const duplicate of pathKeys) {
|
|
58
|
+
if (duplicate !== pathKey) delete environment[duplicate];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
41
62
|
export async function planInvocation(
|
|
42
63
|
command,
|
|
43
64
|
args,
|
|
@@ -58,9 +79,13 @@ export async function planInvocation(
|
|
|
58
79
|
return { command: executable, args, verbatim: false };
|
|
59
80
|
}
|
|
60
81
|
const comSpec = environment.ComSpec ?? environment.COMSPEC ?? 'cmd.exe';
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
82
|
+
const doubleEscapeMetaCharacters = npmShimPath.test(executable);
|
|
83
|
+
const line = [
|
|
84
|
+
escapeCommand(executable),
|
|
85
|
+
...args.map((argument) =>
|
|
86
|
+
escapeArgument(argument, doubleEscapeMetaCharacters),
|
|
87
|
+
),
|
|
88
|
+
].join(' ');
|
|
64
89
|
return {
|
|
65
90
|
command: comSpec,
|
|
66
91
|
args: ['/d', '/s', '/c', `"${line}"`],
|
|
@@ -80,7 +105,10 @@ export async function resolveWindowsExecutable(command, environment) {
|
|
|
80
105
|
if (pathSeparators.test(command)) {
|
|
81
106
|
return await firstExistingFile(command, extensions, carriesExtension);
|
|
82
107
|
}
|
|
83
|
-
const searchPath =
|
|
108
|
+
const searchPath = Object.entries(environment)
|
|
109
|
+
.filter(([key, value]) => key.toLowerCase() === 'path' && Boolean(value))
|
|
110
|
+
.map(([, value]) => value)
|
|
111
|
+
.join(delimiter);
|
|
84
112
|
for (const directory of searchPath.split(delimiter).filter(Boolean)) {
|
|
85
113
|
const candidate = await firstExistingFile(
|
|
86
114
|
join(unquote(directory), command),
|
|
@@ -122,13 +150,14 @@ function escapeCommand(value) {
|
|
|
122
150
|
return String(value).replace(metaCharacters, '^$1');
|
|
123
151
|
}
|
|
124
152
|
|
|
125
|
-
function escapeArgument(value) {
|
|
153
|
+
function escapeArgument(value, doubleEscapeMetaCharacters) {
|
|
126
154
|
const escaped = String(value)
|
|
127
|
-
.replace(/(
|
|
128
|
-
.replace(/(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
.replace(metaCharacters, '^$1')
|
|
155
|
+
.replace(/(?=(\\+?)?)\1"/g, '$1$1\\"')
|
|
156
|
+
.replace(/(?=(\\+?)?)\1$/, '$1$1');
|
|
157
|
+
const quoted = `"${escaped}"`.replace(metaCharacters, '^$1');
|
|
158
|
+
return doubleEscapeMetaCharacters
|
|
159
|
+
? quoted.replace(metaCharacters, '^$1')
|
|
160
|
+
: quoted;
|
|
132
161
|
}
|
|
133
162
|
|
|
134
163
|
export function commandFailure(command, result) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engineering-memory",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
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",
|
|
@@ -14,7 +14,7 @@ export function loadBridgeConfig(env = process.env) {
|
|
|
14
14
|
cacheMaxEntries: positiveInteger(env.ENGINEERING_MEMORY_CACHE_MAX_ENTRIES, 64),
|
|
15
15
|
cacheMaxBytes: positiveInteger(env.ENGINEERING_MEMORY_CACHE_MAX_BYTES, 5 * 1024 * 1024),
|
|
16
16
|
cacheMaxAgeMs: positiveInteger(env.ENGINEERING_MEMORY_CACHE_MAX_AGE_MS, 24 * 60 * 60 * 1000),
|
|
17
|
-
markerSchemaVersion: positiveInteger(env.ENGINEERING_MEMORY_MARKER_SCHEMA_VERSION,
|
|
17
|
+
markerSchemaVersion: positiveInteger(env.ENGINEERING_MEMORY_MARKER_SCHEMA_VERSION, 2),
|
|
18
18
|
credentialService: env.ENGINEERING_MEMORY_CREDENTIAL_SERVICE ?? 'engineering-memory',
|
|
19
19
|
credentialAccount: env.ENGINEERING_MEMORY_CREDENTIAL_ACCOUNT ?? 'default',
|
|
20
20
|
};
|
|
@@ -26,6 +26,7 @@ export function apiStateRoot(config) {
|
|
|
26
26
|
return join(config.stateRoot, 'origins', apiNamespaceKey(config.apiBaseUrl));
|
|
27
27
|
}
|
|
28
28
|
export const endpoints = {
|
|
29
|
+
auditList: '/audit',
|
|
29
30
|
authRefresh: '/auth/refresh',
|
|
30
31
|
authLogout: '/auth/logout',
|
|
31
32
|
authBrowserStart: '/auth/browser/start',
|
|
@@ -49,6 +50,8 @@ export const endpoints = {
|
|
|
49
50
|
taskScaffoldApplication: '/tasks/scaffold-application',
|
|
50
51
|
organizationList: '/organizations',
|
|
51
52
|
organizationCreate: '/organizations',
|
|
53
|
+
organizationMemberList: (organizationId) => `/organizations/${organizationId}/members`,
|
|
54
|
+
organizationMemberUpsert: (organizationId) => `/organizations/${organizationId}/members`,
|
|
52
55
|
taskVerify: '/runtime/task/verify',
|
|
53
56
|
taskClose: '/runtime/task/close',
|
|
54
57
|
taskAbandon: '/runtime/task/abandon',
|
|
@@ -57,7 +60,12 @@ export const endpoints = {
|
|
|
57
60
|
projectList: '/projects',
|
|
58
61
|
projectResolve: '/projects/resolve',
|
|
59
62
|
projectBind: (projectId) => `/projects/${projectId}/bind`,
|
|
63
|
+
projectArchive: (projectId) => `/projects/${projectId}`,
|
|
64
|
+
projectRestore: (projectId) => `/projects/${projectId}/restore`,
|
|
60
65
|
projectMemberAdd: (projectId) => `/projects/${projectId}/members`,
|
|
66
|
+
projectMemberList: (projectId) => `/projects/${projectId}/members`,
|
|
67
|
+
workItemList: (projectId) => `/projects/${projectId}/work-items`,
|
|
68
|
+
workItemGet: (projectId, workItemId) => `/projects/${projectId}/work-items/${workItemId}`,
|
|
61
69
|
projectLink: '/projects/link',
|
|
62
70
|
projectUnlink: '/projects/unlink',
|
|
63
71
|
projectLinks: (projectId) => `/projects/${projectId}/links`,
|
|
@@ -3,6 +3,7 @@ import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
|
3
3
|
import { assertManagedPath, canonicalPath } from '../utilities/files.js';
|
|
4
4
|
import { sha256, stableStringify } from '../utilities/hash.js';
|
|
5
5
|
import { NativeCommandRunner } from '../utilities/process.js';
|
|
6
|
+
import { TaskBranchStore } from '../runtime/task-branch-store.js';
|
|
6
7
|
export const temporaryScaffoldingMarker = 'ENGINEERING-MEMORY-TEMPORARY';
|
|
7
8
|
export const temporaryScaffoldingPattern = `${temporaryScaffoldingMarker}:`;
|
|
8
9
|
export class GitInspector {
|
|
@@ -19,16 +20,110 @@ export class GitInspector {
|
|
|
19
20
|
}
|
|
20
21
|
return await canonicalPath(result.stdout.trim());
|
|
21
22
|
}
|
|
22
|
-
async fingerprint(repoRoot) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
]);
|
|
23
|
+
async fingerprint(repoRoot, identityVersion = 2) {
|
|
24
|
+
if (!Number.isSafeInteger(identityVersion) || identityVersion < 1) {
|
|
25
|
+
throw new Error('Repository identity version must be a positive integer');
|
|
26
|
+
}
|
|
27
|
+
const remote = await this.gitValue(repoRoot, ['config', '--get', 'remote.origin.url']);
|
|
27
28
|
const identity = remote
|
|
28
|
-
?
|
|
29
|
-
:
|
|
29
|
+
? canonicalRemoteIdentity(remote)
|
|
30
|
+
: (await this.mainWorktree(repoRoot)).toLowerCase();
|
|
31
|
+
if (identityVersion === 1) {
|
|
32
|
+
const firstCommit = await this.gitValue(repoRoot, ['rev-list', '--max-parents=0', 'HEAD']);
|
|
33
|
+
return sha256(`${identity}\n${firstCommit ?? ''}`);
|
|
34
|
+
}
|
|
30
35
|
return sha256(identity);
|
|
31
36
|
}
|
|
37
|
+
branchStore(repoRoot) {
|
|
38
|
+
return new TaskBranchStore(repoRoot, this.runner);
|
|
39
|
+
}
|
|
40
|
+
async validateBranch(repoRoot, name) {
|
|
41
|
+
const result = await this.runner.run('git', ['check-ref-format', '--branch', name], {
|
|
42
|
+
cwd: repoRoot,
|
|
43
|
+
});
|
|
44
|
+
if (name.length > 255 || result.exitCode !== 0 || result.stdout.trim() !== name)
|
|
45
|
+
throw new Error('Choose a valid literal Git branch name of at most 255 characters');
|
|
46
|
+
}
|
|
47
|
+
async createWorktree(repoRoot, name, target) {
|
|
48
|
+
await this.validateBranch(repoRoot, name);
|
|
49
|
+
const path = resolve(repoRoot, target);
|
|
50
|
+
const present = await lstat(path).then(() => true, (error) => {
|
|
51
|
+
if (error.code === 'ENOENT')
|
|
52
|
+
return false;
|
|
53
|
+
throw error;
|
|
54
|
+
});
|
|
55
|
+
if (present) {
|
|
56
|
+
const root = await this.findRoot(path);
|
|
57
|
+
const common = await this.gitValue(root, [
|
|
58
|
+
'rev-parse',
|
|
59
|
+
'--path-format=absolute',
|
|
60
|
+
'--git-common-dir',
|
|
61
|
+
]);
|
|
62
|
+
const sourceCommon = await this.gitValue(repoRoot, [
|
|
63
|
+
'rev-parse',
|
|
64
|
+
'--path-format=absolute',
|
|
65
|
+
'--git-common-dir',
|
|
66
|
+
]);
|
|
67
|
+
if (root === (await canonicalPath(path)) &&
|
|
68
|
+
common === sourceCommon &&
|
|
69
|
+
(await this.currentBranch(root)) === name)
|
|
70
|
+
return root;
|
|
71
|
+
throw new Error('The worktree directory already belongs to another repository or branch. Choose a different directory');
|
|
72
|
+
}
|
|
73
|
+
const exists = await this.branchExists(repoRoot, name);
|
|
74
|
+
const args = exists
|
|
75
|
+
? ['worktree', 'add', '--', path, name]
|
|
76
|
+
: ['worktree', 'add', '-b', name, '--', path];
|
|
77
|
+
const result = await this.runner.run('git', args, { cwd: repoRoot });
|
|
78
|
+
if (result.exitCode !== 0)
|
|
79
|
+
throw new Error(`Git could not create the worktree: ${result.stderr.trim()}`);
|
|
80
|
+
return await this.findRoot(path);
|
|
81
|
+
}
|
|
82
|
+
async currentBranch(repoRoot) {
|
|
83
|
+
return await this.gitValue(repoRoot, ['symbolic-ref', '--short', '-q', 'HEAD']);
|
|
84
|
+
}
|
|
85
|
+
async defaultBranch(repoRoot) {
|
|
86
|
+
const ref = await this.gitValue(repoRoot, [
|
|
87
|
+
'symbolic-ref',
|
|
88
|
+
'--short',
|
|
89
|
+
'-q',
|
|
90
|
+
'refs/remotes/origin/HEAD',
|
|
91
|
+
]);
|
|
92
|
+
return ref ? ref.replace(/^origin\//, '') : null;
|
|
93
|
+
}
|
|
94
|
+
async protectedBranchNames(repoRoot) {
|
|
95
|
+
const names = new Set(['main', 'master', 'develop', 'prod']);
|
|
96
|
+
const fallback = await this.defaultBranch(repoRoot);
|
|
97
|
+
if (fallback)
|
|
98
|
+
names.add(fallback);
|
|
99
|
+
return names;
|
|
100
|
+
}
|
|
101
|
+
async createBranch(repoRoot, name) {
|
|
102
|
+
await this.validateBranch(repoRoot, name);
|
|
103
|
+
const result = await this.runner.run('git', ['switch', '-c', name], { cwd: repoRoot });
|
|
104
|
+
if (result.exitCode !== 0) {
|
|
105
|
+
throw new Error(`Git could not create the branch ${name}: ${result.stderr.trim() || 'unknown error'}`);
|
|
106
|
+
}
|
|
107
|
+
const branch = await this.currentBranch(repoRoot);
|
|
108
|
+
if (branch !== name) {
|
|
109
|
+
throw new Error(`Git created ${name} but HEAD is on ${branch ?? 'no branch'}`);
|
|
110
|
+
}
|
|
111
|
+
return branch;
|
|
112
|
+
}
|
|
113
|
+
async branchExists(repoRoot, name) {
|
|
114
|
+
const result = await this.runner.run('git', ['rev-parse', '--verify', '--quiet', `refs/heads/${name}`], {
|
|
115
|
+
cwd: repoRoot,
|
|
116
|
+
});
|
|
117
|
+
return result.exitCode === 0;
|
|
118
|
+
}
|
|
119
|
+
async switchBranch(repoRoot, name) {
|
|
120
|
+
await this.validateBranch(repoRoot, name);
|
|
121
|
+
const result = await this.runner.run('git', ['switch', '--', name], { cwd: repoRoot });
|
|
122
|
+
if (result.exitCode !== 0) {
|
|
123
|
+
throw new Error(`Git could not switch to the branch ${name}: ${result.stderr.trim() || 'unknown error'}`);
|
|
124
|
+
}
|
|
125
|
+
return name;
|
|
126
|
+
}
|
|
32
127
|
async mainWorktree(repoRoot) {
|
|
33
128
|
const commonDirectory = await this.gitValue(repoRoot, [
|
|
34
129
|
'rev-parse',
|
|
@@ -46,20 +141,13 @@ export class GitInspector {
|
|
|
46
141
|
const changedPaths = head
|
|
47
142
|
? await this.changedPathsAgainstHead(root)
|
|
48
143
|
: await this.unbornChangedPaths(root);
|
|
49
|
-
const
|
|
50
|
-
path,
|
|
51
|
-
originalPath: originalPath ?? null,
|
|
52
|
-
status: semanticStatus(status),
|
|
53
|
-
contentHash,
|
|
54
|
-
size,
|
|
55
|
-
mode: mode ?? null,
|
|
56
|
-
}));
|
|
144
|
+
const hashManifest = canonicalHashManifest(changedPaths);
|
|
57
145
|
return {
|
|
58
146
|
repoRoot: root,
|
|
59
147
|
head,
|
|
60
148
|
changedPaths,
|
|
61
|
-
worktreeHash: sha256(`${stableStringify(
|
|
62
|
-
diffHash: sha256(`${head ?? '<unborn>'}\n${stableStringify(
|
|
149
|
+
worktreeHash: sha256(`${stableStringify(hashManifest)}\n`),
|
|
150
|
+
diffHash: sha256(`${head ?? '<unborn>'}\n${stableStringify(hashManifest)}\n`),
|
|
63
151
|
};
|
|
64
152
|
}
|
|
65
153
|
async stagedManifest(repoRoot) {
|
|
@@ -364,6 +452,41 @@ function semanticStatus(status) {
|
|
|
364
452
|
}
|
|
365
453
|
return status.trim() || status;
|
|
366
454
|
}
|
|
455
|
+
function canonicalHashManifest(entries) {
|
|
456
|
+
const byPath = new Map();
|
|
457
|
+
const renamedSources = new Set();
|
|
458
|
+
for (const entry of entries) {
|
|
459
|
+
const status = semanticStatus(entry.status);
|
|
460
|
+
const deleted = status === 'D';
|
|
461
|
+
if (status === 'R') {
|
|
462
|
+
if (!entry.originalPath) {
|
|
463
|
+
throw new Error(`Git rename is missing its original path: ${entry.path}`);
|
|
464
|
+
}
|
|
465
|
+
renamedSources.add(entry.originalPath);
|
|
466
|
+
}
|
|
467
|
+
byPath.set(entry.path, {
|
|
468
|
+
path: entry.path,
|
|
469
|
+
originalPath: null,
|
|
470
|
+
status: status === 'C' || status === 'R' ? 'A' : status,
|
|
471
|
+
contentHash: deleted ? null : entry.contentHash,
|
|
472
|
+
size: deleted ? null : entry.size,
|
|
473
|
+
mode: deleted ? null : (entry.mode ?? null),
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
for (const path of renamedSources) {
|
|
477
|
+
if (!byPath.has(path)) {
|
|
478
|
+
byPath.set(path, {
|
|
479
|
+
path,
|
|
480
|
+
originalPath: null,
|
|
481
|
+
status: 'D',
|
|
482
|
+
contentHash: null,
|
|
483
|
+
size: null,
|
|
484
|
+
mode: null,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return [...byPath.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
489
|
+
}
|
|
367
490
|
function deduplicateChangedPaths(entries) {
|
|
368
491
|
const byPath = new Map();
|
|
369
492
|
for (const entry of entries) {
|
|
@@ -35,8 +35,24 @@ async function run() {
|
|
|
35
35
|
})).data);
|
|
36
36
|
const result = await gate.verify(repoRoot);
|
|
37
37
|
process.stderr.write(`${JSON.stringify(result)}\n`);
|
|
38
|
+
const advice = describe(result);
|
|
39
|
+
if (advice)
|
|
40
|
+
process.stderr.write(`${advice}\n`);
|
|
38
41
|
process.exitCode = result.allowed ? 0 : 1;
|
|
39
42
|
}
|
|
43
|
+
function describe(result) {
|
|
44
|
+
const [expected, actual] = result.details ?? [];
|
|
45
|
+
switch (result.reason) {
|
|
46
|
+
case 'branch_mismatch':
|
|
47
|
+
return `The task selected ${expected} but HEAD is on ${actual}. Run: git switch ${expected}`;
|
|
48
|
+
case 'branch_detached':
|
|
49
|
+
return `The task selected ${expected} but HEAD is detached. Run: git switch ${expected}`;
|
|
50
|
+
case 'online_commit_gate_rejected':
|
|
51
|
+
return `The backend refused the commit: ${expected ?? 'no reason given'}`;
|
|
52
|
+
default:
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
40
56
|
void run().catch(() => {
|
|
41
57
|
process.stderr.write('Engineering Memory Git gate failed\n');
|
|
42
58
|
process.exitCode = 1;
|
|
@@ -32,6 +32,7 @@ export class VerificationGate {
|
|
|
32
32
|
taskClosed: input.taskClosed ?? false,
|
|
33
33
|
taskVersion: input.taskVersion ?? null,
|
|
34
34
|
taskChanges,
|
|
35
|
+
branch: input.branch ?? null,
|
|
35
36
|
}, this.root);
|
|
36
37
|
}
|
|
37
38
|
async verify(repoRoot) {
|
|
@@ -79,6 +80,16 @@ export class VerificationGate {
|
|
|
79
80
|
taskId: receipt.taskId,
|
|
80
81
|
};
|
|
81
82
|
}
|
|
83
|
+
const branch = await this.git.currentBranch(repoRoot);
|
|
84
|
+
if (receipt.branch && branch !== receipt.branch) {
|
|
85
|
+
return {
|
|
86
|
+
allowed: false,
|
|
87
|
+
reason: branch ? 'branch_mismatch' : 'branch_detached',
|
|
88
|
+
diffHash: manifest.diffHash,
|
|
89
|
+
taskId: receipt.taskId,
|
|
90
|
+
details: [receipt.branch, branch ?? 'HEAD'],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
82
93
|
if (receipt.diffHash !== manifest.diffHash) {
|
|
83
94
|
return {
|
|
84
95
|
allowed: false,
|
|
@@ -118,6 +129,7 @@ export class VerificationGate {
|
|
|
118
129
|
repoFingerprint: receipt.repoFingerprint,
|
|
119
130
|
diffHash: manifest.diffHash,
|
|
120
131
|
expectedTaskVersion: receipt.taskVersion,
|
|
132
|
+
branch: branch ?? undefined,
|
|
121
133
|
});
|
|
122
134
|
const result = asObject(attestation);
|
|
123
135
|
if (result?.allowed !== true ||
|
|
@@ -132,12 +144,13 @@ export class VerificationGate {
|
|
|
132
144
|
};
|
|
133
145
|
}
|
|
134
146
|
}
|
|
135
|
-
catch {
|
|
147
|
+
catch (error) {
|
|
136
148
|
return {
|
|
137
149
|
allowed: false,
|
|
138
150
|
reason: 'online_commit_gate_rejected',
|
|
139
151
|
diffHash: manifest.diffHash,
|
|
140
152
|
taskId: receipt.taskId,
|
|
153
|
+
details: [error instanceof Error ? error.message : String(error)],
|
|
141
154
|
};
|
|
142
155
|
}
|
|
143
156
|
return {
|
|
@@ -44,6 +44,7 @@ const checkpointBase = {
|
|
|
44
44
|
idempotencyKey: z.string().uuid().optional(),
|
|
45
45
|
};
|
|
46
46
|
export const engineeringMemoryToolNames = [
|
|
47
|
+
'audit.list',
|
|
47
48
|
'session.entry',
|
|
48
49
|
'session.set_decision',
|
|
49
50
|
'session.decline_update',
|
|
@@ -65,15 +66,23 @@ export const engineeringMemoryToolNames = [
|
|
|
65
66
|
'task.verify',
|
|
66
67
|
'task.close',
|
|
67
68
|
'task.abandon',
|
|
69
|
+
'task.branch',
|
|
68
70
|
'architecture.plan',
|
|
69
71
|
'architecture.module',
|
|
70
72
|
'architecture.record_application',
|
|
71
73
|
'organization.list',
|
|
72
74
|
'organization.create',
|
|
75
|
+
'organization.member_list',
|
|
76
|
+
'organization.member_upsert',
|
|
73
77
|
'project.setup',
|
|
74
78
|
'project.list',
|
|
75
79
|
'project.resolve',
|
|
80
|
+
'project.archive',
|
|
81
|
+
'project.restore',
|
|
76
82
|
'project.member_add',
|
|
83
|
+
'project.member_list',
|
|
84
|
+
'work_item.list',
|
|
85
|
+
'work_item.get',
|
|
77
86
|
'project.clone',
|
|
78
87
|
'project.link',
|
|
79
88
|
'project.unlink',
|
|
@@ -97,6 +106,31 @@ const reconciliationEntry = z.object({
|
|
|
97
106
|
reason: z.string().optional(),
|
|
98
107
|
});
|
|
99
108
|
export function registerEngineeringMemoryTools(server, service) {
|
|
109
|
+
server.registerTool('audit.list', {
|
|
110
|
+
description: 'List one authorized organization or project audit trail with an exact cursor. Choose exactly one scope and send both cursor fields together.',
|
|
111
|
+
inputSchema: z
|
|
112
|
+
.object({
|
|
113
|
+
organizationId: z.string().uuid().optional(),
|
|
114
|
+
projectId: z.string().uuid().optional(),
|
|
115
|
+
beforeCreatedAt: z.string().datetime({ offset: true }).optional(),
|
|
116
|
+
beforeId: z.string().uuid().optional(),
|
|
117
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
118
|
+
})
|
|
119
|
+
.superRefine((value, context) => {
|
|
120
|
+
if (Boolean(value.organizationId) === Boolean(value.projectId)) {
|
|
121
|
+
context.addIssue({
|
|
122
|
+
code: 'custom',
|
|
123
|
+
message: 'Choose exactly one organizationId or projectId.',
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (Boolean(value.beforeCreatedAt) !== Boolean(value.beforeId)) {
|
|
127
|
+
context.addIssue({
|
|
128
|
+
code: 'custom',
|
|
129
|
+
message: 'Send beforeCreatedAt and beforeId together.',
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}),
|
|
133
|
+
}, async (input) => toolResult(await service.auditList(input)));
|
|
100
134
|
server.registerTool('session.entry', {
|
|
101
135
|
description: 'The first call of every session in a repository, before answering anything about the project. Reports whether the user is signed in, whether this repository is bound, what this user decided about it last time, and the one thing to do now. A repository the user switched Engineering Memory off in reports that, and is left alone.',
|
|
102
136
|
inputSchema: z.object({ repoRoot: optionalRepoRoot }),
|
|
@@ -131,6 +165,7 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
131
165
|
objective: z.string().min(2),
|
|
132
166
|
taskKind: z.string().min(2),
|
|
133
167
|
workItemKey: z.string().min(1).optional(),
|
|
168
|
+
workItemId: z.string().uuid().optional(),
|
|
134
169
|
mode: z.enum(['write', 'read_only', 'scaffold']).optional(),
|
|
135
170
|
knownRevisions: z.record(z.string(), z.number().int().min(0)).optional(),
|
|
136
171
|
}),
|
|
@@ -394,6 +429,16 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
394
429
|
reason: z.string().min(1).max(2000),
|
|
395
430
|
}),
|
|
396
431
|
}, async (input) => toolResult(await service.taskAbandon(input)));
|
|
432
|
+
server.registerTool('task.branch', {
|
|
433
|
+
description: 'Record the native questionnaire answer for the exact externalTaskId before any new write/scaffold task or read-only-to-write transition. Choose name or keepCurrent. For another task in a busy checkout supply worktreePath and name; use the returned repoRoot for all subsequent commands. Existing branches may be selected without resetting them. Decisions persist across retries and restarts.',
|
|
434
|
+
inputSchema: z.object({
|
|
435
|
+
repoRoot: optionalRepoRoot,
|
|
436
|
+
externalTaskId: z.string().min(2).max(160),
|
|
437
|
+
worktreePath: z.string().min(1).optional(),
|
|
438
|
+
name: z.string().min(1).max(255).optional(),
|
|
439
|
+
keepCurrent: z.boolean().optional(),
|
|
440
|
+
}),
|
|
441
|
+
}, async (input) => toolResult(await service.taskBranch(input)));
|
|
397
442
|
server.registerTool('organization.create', {
|
|
398
443
|
description: 'Create an organization the user named, with the short identifier they typed. Offer this underneath the organizations they already belong to; never invent the identifier from the name.',
|
|
399
444
|
inputSchema: z.object({
|
|
@@ -401,6 +446,19 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
401
446
|
slug: z.string().min(2).max(120),
|
|
402
447
|
}),
|
|
403
448
|
}, async (input) => toolResult(await service.organizationCreate(input)));
|
|
449
|
+
server.registerTool('organization.member_list', {
|
|
450
|
+
description: "List an organization's members and roles so a member who cannot perform an owner-only recovery can coordinate with an owner.",
|
|
451
|
+
inputSchema: z.object({ organizationId: z.string().uuid() }),
|
|
452
|
+
}, async (input) => toolResult(await service.organizationMemberList(input)));
|
|
453
|
+
server.registerTool('organization.member_upsert', {
|
|
454
|
+
description: 'Add or update an existing Engineering Memory account in an organization. Only an organization owner may call this; use it before adding that account to a child project.',
|
|
455
|
+
inputSchema: z.object({
|
|
456
|
+
organizationId: z.string().uuid(),
|
|
457
|
+
email: z.string().email(),
|
|
458
|
+
role: z.enum(['owner', 'member']),
|
|
459
|
+
discipline: z.enum(['backend', 'frontend', 'fullstack']),
|
|
460
|
+
}),
|
|
461
|
+
}, async (input) => toolResult(await service.organizationMemberUpsert(input)));
|
|
404
462
|
server.registerTool('project.setup', {
|
|
405
463
|
description: 'Create a policy-ready backend project from the native questionnaire and local repository audit, then write its secret-free marker.',
|
|
406
464
|
inputSchema: z.object({
|
|
@@ -429,7 +487,7 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
429
487
|
}),
|
|
430
488
|
}, async (input) => toolResult(await service.projectSetup(input)));
|
|
431
489
|
server.registerTool('architecture.plan', {
|
|
432
|
-
description: 'List
|
|
490
|
+
description: 'List only architecture modules compatible with the approved project runtime contract, in apply order with their manifests, rename map, asset contract and tenant-specific points. Returns no file bodies; an empty plan means continue without scaffolding.',
|
|
433
491
|
inputSchema: z.object({
|
|
434
492
|
projectId: z.string().uuid(),
|
|
435
493
|
sessionId: z.string().uuid(),
|
|
@@ -465,9 +523,9 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
465
523
|
inputSchema: z.object({}),
|
|
466
524
|
}, async () => toolResult(await service.organizationList()));
|
|
467
525
|
server.registerTool('project.list', {
|
|
468
|
-
description: 'List projects available to the authenticated user
|
|
469
|
-
inputSchema: z.object({}),
|
|
470
|
-
}, async () => toolResult(await service.projectList()));
|
|
526
|
+
description: 'List projects available to the authenticated user, optionally including archived projects that need restoration.',
|
|
527
|
+
inputSchema: z.object({ includeArchived: z.boolean().optional() }),
|
|
528
|
+
}, async (input) => toolResult(await service.projectList(input)));
|
|
471
529
|
server.registerTool('project.resolve', {
|
|
472
530
|
description: 'Resolve the current repository fingerprint or bind an explicitly selected project, then write the secret-free marker only after backend authorization succeeds.',
|
|
473
531
|
inputSchema: z.object({
|
|
@@ -476,14 +534,52 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
476
534
|
projectId: z.string().uuid().optional(),
|
|
477
535
|
}),
|
|
478
536
|
}, async (input) => toolResult(await service.projectResolve(input)));
|
|
537
|
+
server.registerTool('project.archive', {
|
|
538
|
+
description: 'Archive a project at an explicit version while preserving its repository identity and history.',
|
|
539
|
+
inputSchema: z.object({
|
|
540
|
+
projectId: z.string().uuid(),
|
|
541
|
+
expectedVersion: z.number().int().min(1),
|
|
542
|
+
}),
|
|
543
|
+
}, async (input) => toolResult(await service.projectArchive(input)));
|
|
544
|
+
server.registerTool('project.restore', {
|
|
545
|
+
description: 'Restore an archived project at the version returned by project.list with includeArchived enabled.',
|
|
546
|
+
inputSchema: z.object({
|
|
547
|
+
projectId: z.string().uuid(),
|
|
548
|
+
expectedVersion: z.number().int().min(1),
|
|
549
|
+
}),
|
|
550
|
+
}, async (input) => toolResult(await service.projectRestore(input)));
|
|
479
551
|
server.registerTool('project.member_add', {
|
|
480
|
-
description: 'Add an existing Engineering Memory account
|
|
552
|
+
description: 'Add or update an existing Engineering Memory account with an explicit project role and optional discipline override.',
|
|
481
553
|
inputSchema: z.object({
|
|
482
554
|
projectId: z.string().min(1),
|
|
483
555
|
email: z.string().email(),
|
|
484
556
|
role: z.enum(['owner', 'maintainer', 'member', 'reader']),
|
|
557
|
+
discipline: z.enum(['backend', 'frontend', 'fullstack']).nullable().optional(),
|
|
485
558
|
}),
|
|
486
559
|
}, async (input) => toolResult(await service.projectMemberAdd(input)));
|
|
560
|
+
server.registerTool('project.member_list', {
|
|
561
|
+
description: 'List the members and roles of a project, including an archived project, so an action that requires an owner can be coordinated with the right person.',
|
|
562
|
+
inputSchema: z.object({ projectId: z.string().uuid() }),
|
|
563
|
+
}, async (input) => toolResult(await service.projectMemberList(input)));
|
|
564
|
+
server.registerTool('work_item.list', {
|
|
565
|
+
description: 'List selectable work items for a project before opening an engineering run in this chat.',
|
|
566
|
+
inputSchema: z.object({
|
|
567
|
+
projectId: z.string().uuid(),
|
|
568
|
+
status: z
|
|
569
|
+
.enum(['backlog', 'ready', 'in_progress', 'in_review', 'done', 'cancelled'])
|
|
570
|
+
.optional(),
|
|
571
|
+
priority: z.enum(['lowest', 'low', 'medium', 'high', 'highest']).optional(),
|
|
572
|
+
assigneeUserId: z.string().uuid().optional(),
|
|
573
|
+
includeArchived: z.boolean().optional(),
|
|
574
|
+
}),
|
|
575
|
+
}, async (input) => toolResult(await service.workItemList(input)));
|
|
576
|
+
server.registerTool('work_item.get', {
|
|
577
|
+
description: 'Load one project work item, including acceptance criteria and dependencies, before starting or resuming its engineering run.',
|
|
578
|
+
inputSchema: z.object({
|
|
579
|
+
projectId: z.string().uuid(),
|
|
580
|
+
workItemId: z.string().uuid(),
|
|
581
|
+
}),
|
|
582
|
+
}, async (input) => toolResult(await service.workItemGet(input)));
|
|
487
583
|
server.registerTool('auth.logout', {
|
|
488
584
|
description: 'Revoke the refresh session and delete local credentials; transient revoke failures preserve the session unless the user explicitly confirms local-only logout.',
|
|
489
585
|
inputSchema: z.object({
|
|
@@ -18,7 +18,7 @@ export class RepositoryResolver {
|
|
|
18
18
|
throw new Error('Explicit project does not match the repository marker');
|
|
19
19
|
}
|
|
20
20
|
const [repoFingerprint, git] = await Promise.all([
|
|
21
|
-
this.git.fingerprint(repoRoot),
|
|
21
|
+
this.git.fingerprint(repoRoot, marker?.schemaVersion ?? this.markerSchemaVersion),
|
|
22
22
|
this.git.manifest(repoRoot),
|
|
23
23
|
]);
|
|
24
24
|
return {
|
|
@@ -43,6 +43,16 @@ export class ActiveContextStore {
|
|
|
43
43
|
const pointers = await this.list(repoFingerprint);
|
|
44
44
|
return pointers.find((pointer) => pointer.taskSlug === taskSlug) ?? null;
|
|
45
45
|
}
|
|
46
|
+
async loadForSession(repoFingerprint, sessionId) {
|
|
47
|
+
if (!isUuid(sessionId)) {
|
|
48
|
+
throw new Error('Context session identifier is invalid');
|
|
49
|
+
}
|
|
50
|
+
const matches = (await this.list(repoFingerprint)).filter((pointer) => pointer.sessionId === sessionId);
|
|
51
|
+
if (matches.length > 1) {
|
|
52
|
+
throw new Error('Context session has more than one active task pointer');
|
|
53
|
+
}
|
|
54
|
+
return matches[0] ?? null;
|
|
55
|
+
}
|
|
46
56
|
async list(repoFingerprint) {
|
|
47
57
|
assertFingerprint(repoFingerprint);
|
|
48
58
|
await this.adoptLegacyPointer(repoFingerprint);
|
|
@@ -112,7 +122,7 @@ export class ActiveContextStore {
|
|
|
112
122
|
async setChangeBaseline(repoFingerprint, sessionId, manifest, leasePaths, taskSnapshot) {
|
|
113
123
|
const pointer = taskSnapshot
|
|
114
124
|
? await this.loadForTask(repoFingerprint, taskSnapshot.taskId)
|
|
115
|
-
: await this.
|
|
125
|
+
: await this.loadForSession(repoFingerprint, sessionId);
|
|
116
126
|
if (!pointer || pointer.sessionId !== sessionId) {
|
|
117
127
|
throw new Error('Change baseline does not match the active repository session');
|
|
118
128
|
}
|
|
@@ -163,9 +173,7 @@ export class ActiveContextStore {
|
|
|
163
173
|
}
|
|
164
174
|
async clearVerificationIntent(repoFingerprint, taskId) {
|
|
165
175
|
await this.exclusive(repoFingerprint, async () => {
|
|
166
|
-
const pointer = taskId
|
|
167
|
-
? await this.loadForTask(repoFingerprint, taskId)
|
|
168
|
-
: await this.load(repoFingerprint);
|
|
176
|
+
const pointer = await this.loadForTask(repoFingerprint, taskId);
|
|
169
177
|
if (!pointer?.verificationIntent) {
|
|
170
178
|
return;
|
|
171
179
|
}
|
|
@@ -199,9 +207,7 @@ export class ActiveContextStore {
|
|
|
199
207
|
}
|
|
200
208
|
async clearCloseIntent(repoFingerprint, taskId) {
|
|
201
209
|
await this.exclusive(repoFingerprint, async () => {
|
|
202
|
-
const pointer = taskId
|
|
203
|
-
? await this.loadForTask(repoFingerprint, taskId)
|
|
204
|
-
: await this.load(repoFingerprint);
|
|
210
|
+
const pointer = await this.loadForTask(repoFingerprint, taskId);
|
|
205
211
|
if (!pointer?.closeIntent) {
|
|
206
212
|
return;
|
|
207
213
|
}
|