engineering-memory 1.11.15 → 1.11.17
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/package.json +1 -1
- package/runtime/build.json +1 -1
- package/runtime/dist/src/config.js +1 -0
- package/runtime/dist/src/mcp/tool-annotations.js +1 -0
- package/runtime/dist/src/mcp/tool-definitions.js +14 -2
- package/runtime/dist/src/runtime/api-client.js +1 -0
- package/runtime/dist/src/runtime/bridge-service.js +25 -23
- package/runtime/dist/src/runtime/worktree-editor.js +184 -0
- package/runtime/dist/src/runtime/worktree-gradle.js +318 -0
- package/runtime/dist/src/runtime/worktree-pool.js +181 -3
- package/runtime/dist/src/runtime/worktree-preparation.js +901 -0
- package/runtime/dist/src/runtime/worktree-readiness-types.js +18 -0
- package/skill/references/lifecycle.md +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engineering-memory",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.17",
|
|
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
|
@@ -77,6 +77,7 @@ export const endpoints = {
|
|
|
77
77
|
projectRestore: (projectId) => `/projects/${projectId}/restore`,
|
|
78
78
|
projectMemberAdd: (projectId) => `/projects/${projectId}/members`,
|
|
79
79
|
projectMemberList: (projectId) => `/projects/${projectId}/members`,
|
|
80
|
+
workItemStatuses: (projectId) => `/projects/${projectId}/work-items/statuses`,
|
|
80
81
|
workItemList: (projectId) => `/projects/${projectId}/work-items`,
|
|
81
82
|
workItemGet: (projectId, workItemId) => `/projects/${projectId}/work-items/${workItemId}`,
|
|
82
83
|
projectLink: '/projects/link',
|
|
@@ -130,6 +130,7 @@ export const engineeringMemoryToolNames = [
|
|
|
130
130
|
'work_item.plan',
|
|
131
131
|
'work_item.confirm_plan',
|
|
132
132
|
'project.update',
|
|
133
|
+
'work_item.statuses',
|
|
133
134
|
'work_item.list',
|
|
134
135
|
'work_item.get',
|
|
135
136
|
'project.clone',
|
|
@@ -932,7 +933,7 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
932
933
|
}),
|
|
933
934
|
}, async (input) => toolResult(await service.workItemCreate(input)));
|
|
934
935
|
server.registerTool('work_item.update', {
|
|
935
|
-
description:
|
|
936
|
+
description: 'Edit or assign a work item at its current version. Assignees must already be active project members; this does not grant project access. Read work_item.statuses before changing status. Send the actual project catalogue slug and follow its user-defined meaning, entryRule and flags; never infer workflow from a status name.',
|
|
936
937
|
inputSchema: z.object({
|
|
937
938
|
...workItemLocator,
|
|
938
939
|
data: z.object({
|
|
@@ -1015,11 +1016,22 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
1015
1016
|
}),
|
|
1016
1017
|
}),
|
|
1017
1018
|
}, async (input) => toolResult(await service.projectUpdate(input)));
|
|
1019
|
+
server.registerTool('work_item.statuses', {
|
|
1020
|
+
description: 'Read the project status catalogue before choosing a work item status. Follow the user-defined meaning, entryRule and flags; never infer workflow from status names. Page with offset and limit (defaults 0 and 50, maximum 100). Archived statuses are excluded unless includeArchived is true.',
|
|
1021
|
+
inputSchema: z.object({
|
|
1022
|
+
projectId: z.string().uuid(),
|
|
1023
|
+
offset: z.number().int().min(0).optional(),
|
|
1024
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
1025
|
+
includeArchived: z.boolean().optional(),
|
|
1026
|
+
}),
|
|
1027
|
+
}, async (input) => toolResult(await service.workItemStatuses(input)));
|
|
1018
1028
|
server.registerTool('work_item.list', {
|
|
1019
|
-
description:
|
|
1029
|
+
description: 'List selectable work items for a project before opening an engineering run in this chat. Read work_item.statuses for the actual project catalogue slugs and user-defined meanings; never infer workflow from status names. The status filter matches an actual slug, including a retained archived status; an unknown slug returns an empty page.',
|
|
1020
1030
|
inputSchema: z.object({
|
|
1021
1031
|
projectId: z.string().uuid(),
|
|
1022
1032
|
status: z.string().trim().min(1).max(64).optional(),
|
|
1033
|
+
offset: z.number().int().min(0).optional(),
|
|
1034
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
1023
1035
|
priority: z.enum(['lowest', 'low', 'medium', 'high', 'highest']).optional(),
|
|
1024
1036
|
assigneeUserId: z.string().uuid().optional(),
|
|
1025
1037
|
includeArchived: z.boolean().optional(),
|
|
@@ -544,7 +544,7 @@ export class BridgeService {
|
|
|
544
544
|
if ((!sourceCommit && !(previous && previous.baseCommit === null && !previous.managed)) ||
|
|
545
545
|
branch === undefined)
|
|
546
546
|
throw refuse('The task branch and base must be restored before resuming its worktree.', 'worktree.reconcile');
|
|
547
|
-
const allocation = await pool.allocate({
|
|
547
|
+
const allocation = await pool.prepare(await pool.allocate({
|
|
548
548
|
projectId,
|
|
549
549
|
projectName: projectId,
|
|
550
550
|
repoFingerprint: repository.repoFingerprint,
|
|
@@ -554,7 +554,7 @@ export class BridgeService {
|
|
|
554
554
|
baseCommit: sourceCommit ?? null,
|
|
555
555
|
keepCurrent: previous ? !previous.managed : false,
|
|
556
556
|
resume: true,
|
|
557
|
-
}, await this.poolPolicy());
|
|
557
|
+
}, await this.poolPolicy()));
|
|
558
558
|
await pool.bind(projectId, allocation.repoRoot, allocation.generation, pointer.taskId);
|
|
559
559
|
allocation.taskId = pointer.taskId;
|
|
560
560
|
this.poolAllocations.set(allocation.repoRoot, allocation);
|
|
@@ -2615,7 +2615,7 @@ export class BridgeService {
|
|
|
2615
2615
|
const allocationTimer = startPhaseTimer('task.branch.allocate');
|
|
2616
2616
|
const allocationPolicy = await this.poolPolicy(true);
|
|
2617
2617
|
allocationTimer.mark('policy');
|
|
2618
|
-
const allocation = await pool.allocate({
|
|
2618
|
+
const allocation = await pool.prepare(await pool.allocate({
|
|
2619
2619
|
projectId,
|
|
2620
2620
|
projectName: preferences.projectName ?? projectId,
|
|
2621
2621
|
repoFingerprint: repository.repoFingerprint,
|
|
@@ -2626,7 +2626,7 @@ export class BridgeService {
|
|
|
2626
2626
|
keepCurrent: input.keepCurrent,
|
|
2627
2627
|
inPlace: !existing && input.inPlace,
|
|
2628
2628
|
resume: Boolean(existing),
|
|
2629
|
-
}, allocationPolicy);
|
|
2629
|
+
}, allocationPolicy));
|
|
2630
2630
|
allocationTimer.mark('pool_allocate');
|
|
2631
2631
|
allocationTimer.finish();
|
|
2632
2632
|
this.poolAllocations.set(allocation.repoRoot, allocation);
|
|
@@ -2648,7 +2648,7 @@ export class BridgeService {
|
|
|
2648
2648
|
return asJsonValue({
|
|
2649
2649
|
...allocation,
|
|
2650
2650
|
kept: !allocation.managed,
|
|
2651
|
-
nextAction: 'Run every file, terminal, lifecycle and delivery operation from this repoRoot.
|
|
2651
|
+
nextAction: 'Run every file, terminal, lifecycle and delivery operation from this repoRoot. Inspect readiness separately: a file problem does not cancel the task, and an editor request does not prove its window is visible. Resolve reported local file issues without overwriting existing files, then retry task.branch or session.resume. Use task.heartbeat during long work and pause before handoff.',
|
|
2652
2652
|
});
|
|
2653
2653
|
}
|
|
2654
2654
|
adoptOwned(repoRoot, entry) {
|
|
@@ -3077,6 +3077,19 @@ export class BridgeService {
|
|
|
3077
3077
|
});
|
|
3078
3078
|
});
|
|
3079
3079
|
}
|
|
3080
|
+
async workItemStatuses(input) {
|
|
3081
|
+
return this.execute(async () => {
|
|
3082
|
+
const query = new URLSearchParams({
|
|
3083
|
+
offset: String(input.offset ?? 0),
|
|
3084
|
+
limit: String(input.limit ?? 50),
|
|
3085
|
+
});
|
|
3086
|
+
if (input.includeArchived !== undefined) {
|
|
3087
|
+
query.set('includeArchived', String(input.includeArchived));
|
|
3088
|
+
}
|
|
3089
|
+
const response = await this.dependencies.client.request(`${endpoints.workItemStatuses(input.projectId)}?${query.toString()}`);
|
|
3090
|
+
return asJsonValue(response.data);
|
|
3091
|
+
});
|
|
3092
|
+
}
|
|
3080
3093
|
async workItemList(input) {
|
|
3081
3094
|
return await this.execute(async () => {
|
|
3082
3095
|
const query = new URLSearchParams();
|
|
@@ -3089,6 +3102,10 @@ export class BridgeService {
|
|
|
3089
3102
|
}
|
|
3090
3103
|
if (input.includeArchived)
|
|
3091
3104
|
query.set('includeArchived', 'true');
|
|
3105
|
+
if (input.offset !== undefined)
|
|
3106
|
+
query.set('offset', String(input.offset));
|
|
3107
|
+
if (input.limit !== undefined)
|
|
3108
|
+
query.set('limit', String(input.limit));
|
|
3092
3109
|
const suffix = query.size > 0 ? `?${query.toString()}` : '';
|
|
3093
3110
|
const response = await this.dependencies.client.request(`${endpoints.workItemList(input.projectId)}${suffix}`);
|
|
3094
3111
|
return asJsonValue({
|
|
@@ -3551,30 +3568,15 @@ export class BridgeService {
|
|
|
3551
3568
|
}
|
|
3552
3569
|
}
|
|
3553
3570
|
async actionableWorkItems(projectId) {
|
|
3554
|
-
const path = `${endpoints.workItemList(projectId)}?limit=100`;
|
|
3555
3571
|
try {
|
|
3556
|
-
const
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
throw error;
|
|
3560
|
-
});
|
|
3561
|
-
if (chosen && chosen.length > 0)
|
|
3562
|
-
return chosen;
|
|
3563
|
-
const every = await this.workItemPage(path);
|
|
3564
|
-
if (chosen && every.some((entry) => objectValue(objectValue(entry)?.projectStatus) !== null))
|
|
3565
|
-
return [];
|
|
3566
|
-
const actionable = new Set(['backlog', 'ready', 'in_progress', 'in_review']);
|
|
3567
|
-
return every.filter((entry) => actionable.has(String(objectValue(entry)?.status)));
|
|
3572
|
+
const response = await this.dependencies.client.request(`${endpoints.workItemList(projectId)}?limit=100&actionable=true`);
|
|
3573
|
+
const items = objectValue(response.data)?.items;
|
|
3574
|
+
return Array.isArray(items) ? items : [];
|
|
3568
3575
|
}
|
|
3569
3576
|
catch {
|
|
3570
3577
|
return [];
|
|
3571
3578
|
}
|
|
3572
3579
|
}
|
|
3573
|
-
async workItemPage(path) {
|
|
3574
|
-
const response = await this.dependencies.client.request(path);
|
|
3575
|
-
const items = objectValue(response.data)?.items;
|
|
3576
|
-
return Array.isArray(items) ? items : [];
|
|
3577
|
-
}
|
|
3578
3580
|
async clientUpdate(authenticated) {
|
|
3579
3581
|
const installed = this.dependencies.clientVersion;
|
|
3580
3582
|
if (!authenticated) {
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { access, open, realpath, stat } from 'node:fs/promises';
|
|
4
|
+
import { delimiter, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
export var WorktreeEditorStatus;
|
|
6
|
+
(function (WorktreeEditorStatus) {
|
|
7
|
+
WorktreeEditorStatus["Requested"] = "requested";
|
|
8
|
+
WorktreeEditorStatus["Unavailable"] = "unavailable";
|
|
9
|
+
WorktreeEditorStatus["Failed"] = "failed";
|
|
10
|
+
WorktreeEditorStatus["Skipped"] = "skipped";
|
|
11
|
+
})(WorktreeEditorStatus || (WorktreeEditorStatus = {}));
|
|
12
|
+
export async function openWorktreeInEditor(repoRoot) {
|
|
13
|
+
if (!['win32', 'darwin', 'linux'].includes(process.platform) ||
|
|
14
|
+
(process.env.CI && !/^(false|0)$/i.test(process.env.CI)) ||
|
|
15
|
+
process.env.SSH_CONNECTION ||
|
|
16
|
+
process.env.SSH_CLIENT ||
|
|
17
|
+
process.env.SSH_TTY ||
|
|
18
|
+
process.env.VSCODE_AGENT_FOLDER ||
|
|
19
|
+
process.env.VSCODE_REMOTE_NAME ||
|
|
20
|
+
process.env.WSL_DISTRO_NAME ||
|
|
21
|
+
process.env.CODESPACES === 'true' ||
|
|
22
|
+
process.env.SESSIONNAME?.toLowerCase() === 'services' ||
|
|
23
|
+
(process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY)) {
|
|
24
|
+
return {
|
|
25
|
+
status: WorktreeEditorStatus.Skipped,
|
|
26
|
+
detail: 'A local desktop session is not available.',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
let folder;
|
|
30
|
+
try {
|
|
31
|
+
if (!repoRoot.trim())
|
|
32
|
+
throw new Error();
|
|
33
|
+
folder = await realpath(resolve(repoRoot));
|
|
34
|
+
if (!(await stat(folder)).isDirectory())
|
|
35
|
+
throw new Error();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return {
|
|
39
|
+
status: WorktreeEditorStatus.Failed,
|
|
40
|
+
detail: 'The worktree folder is not accessible.',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const command = await findEditor().catch(() => null);
|
|
44
|
+
if (!command) {
|
|
45
|
+
return {
|
|
46
|
+
status: WorktreeEditorStatus.Unavailable,
|
|
47
|
+
detail: 'An installed local VS Code CLI was not found.',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (/[\\/](?:\.vscode-server(?:-insiders)?|remote-cli)[\\/]/i.test(command.executable)) {
|
|
51
|
+
return {
|
|
52
|
+
status: WorktreeEditorStatus.Skipped,
|
|
53
|
+
detail: 'The available VS Code CLI belongs to a remote session.',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const env = { ...process.env };
|
|
57
|
+
delete env.VSCODE_DEV;
|
|
58
|
+
if (process.platform === 'win32')
|
|
59
|
+
env.ELECTRON_RUN_AS_NODE = '1';
|
|
60
|
+
try {
|
|
61
|
+
return await new Promise((resolveResult) => {
|
|
62
|
+
const child = spawn(command.executable, [...command.args, '--new-window', folder], {
|
|
63
|
+
cwd: folder,
|
|
64
|
+
env,
|
|
65
|
+
shell: false,
|
|
66
|
+
windowsHide: true,
|
|
67
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
68
|
+
});
|
|
69
|
+
let settled = false;
|
|
70
|
+
let outputBytes = 0;
|
|
71
|
+
const timer = setTimeout(() => stop('The VS Code request timed out; window state is unknown.'), 10_000);
|
|
72
|
+
const finish = (status, detail) => {
|
|
73
|
+
if (settled)
|
|
74
|
+
return;
|
|
75
|
+
settled = true;
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
child.stdout.destroy();
|
|
78
|
+
child.stderr.destroy();
|
|
79
|
+
child.unref();
|
|
80
|
+
resolveResult({ status, detail });
|
|
81
|
+
};
|
|
82
|
+
const stop = (detail) => {
|
|
83
|
+
if (settled)
|
|
84
|
+
return;
|
|
85
|
+
try {
|
|
86
|
+
child.kill('SIGKILL');
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
finish(WorktreeEditorStatus.Failed, detail);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const discardOutput = (chunk) => {
|
|
93
|
+
outputBytes += chunk.length;
|
|
94
|
+
if (outputBytes > 65_536) {
|
|
95
|
+
stop('The VS Code CLI exceeded the output limit; window state is unknown.');
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
child.stdout.on('data', discardOutput);
|
|
99
|
+
child.stderr.on('data', discardOutput);
|
|
100
|
+
child.once('error', () => finish(WorktreeEditorStatus.Failed, 'The VS Code CLI could not start.'));
|
|
101
|
+
child.once('close', (code) => finish(code === 0 ? WorktreeEditorStatus.Requested : WorktreeEditorStatus.Failed, code === 0
|
|
102
|
+
? 'VS Code launch requested; window state was not verified.'
|
|
103
|
+
: 'The VS Code CLI failed; window state is unknown.'));
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return { status: WorktreeEditorStatus.Failed, detail: 'The VS Code CLI could not start.' };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function findEditor() {
|
|
111
|
+
const directories = (process.env.PATH ?? '')
|
|
112
|
+
.split(delimiter)
|
|
113
|
+
.map((directory) => directory.replace(/^"(.+)"$/, '$1'))
|
|
114
|
+
.filter(isAbsolute)
|
|
115
|
+
.slice(0, 128);
|
|
116
|
+
if (process.platform === 'win32') {
|
|
117
|
+
for (const root of [
|
|
118
|
+
process.env.LOCALAPPDATA && join(process.env.LOCALAPPDATA, 'Programs'),
|
|
119
|
+
process.env.ProgramFiles,
|
|
120
|
+
process.env['ProgramFiles(x86)'],
|
|
121
|
+
]) {
|
|
122
|
+
if (root && isAbsolute(root))
|
|
123
|
+
directories.push(join(root, 'Microsoft VS Code', 'bin'));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const directory of new Set(directories)) {
|
|
127
|
+
if (process.platform === 'win32') {
|
|
128
|
+
const command = await windowsEditor(directory);
|
|
129
|
+
if (command)
|
|
130
|
+
return command;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const executable = join(directory, 'code');
|
|
134
|
+
if (await executableFile(executable)) {
|
|
135
|
+
return { executable: await realpath(executable), args: [] };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
async function windowsEditor(bin) {
|
|
142
|
+
let handle;
|
|
143
|
+
try {
|
|
144
|
+
const launcher = await realpath(join(bin, 'code.cmd'));
|
|
145
|
+
handle = await open(launcher, 'r');
|
|
146
|
+
const info = await handle.stat();
|
|
147
|
+
if (!info.isFile() || info.size > 16_384)
|
|
148
|
+
return null;
|
|
149
|
+
const bytes = Buffer.alloc(16_385);
|
|
150
|
+
const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0);
|
|
151
|
+
if (bytesRead > 16_384)
|
|
152
|
+
return null;
|
|
153
|
+
const match = bytes
|
|
154
|
+
.subarray(0, bytesRead)
|
|
155
|
+
.toString('utf8')
|
|
156
|
+
.match(/^[ \t]*"%~dp0\.\.\\Code\.exe"[ \t]+"%~dp0\.\.\\((?:[a-f0-9]{7,40}\\)?resources\\app\\out\\cli\.js)"[ \t]+%\*[ \t]*\r?$/im);
|
|
157
|
+
if (!match)
|
|
158
|
+
return null;
|
|
159
|
+
const installation = resolve(dirname(launcher), '..');
|
|
160
|
+
const executable = join(installation, 'Code.exe');
|
|
161
|
+
const cli = join(installation, match[1]);
|
|
162
|
+
if (!(await executableFile(executable)) || !(await stat(cli)).isFile())
|
|
163
|
+
return null;
|
|
164
|
+
return { executable, args: [cli] };
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
await handle?.close();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function executableFile(path) {
|
|
174
|
+
try {
|
|
175
|
+
if (!(await stat(path)).isFile())
|
|
176
|
+
return false;
|
|
177
|
+
await access(path, process.platform === 'win32' ? constants.F_OK : constants.X_OK);
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=worktree-editor.js.map
|