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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engineering-memory",
3
- "version": "1.11.15",
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",
@@ -1,3 +1,3 @@
1
1
  {
2
- "gitHead": "52f278262fad1afde8d11aa6e2343b3de5e8fe78"
2
+ "gitHead": "42ef1e9f90ef7b9d729457c45be97b755c737d1c"
3
3
  }
@@ -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',
@@ -100,6 +100,7 @@ export const toolAnnotations = {
100
100
  'work_item.confirm_plan': write,
101
101
  'project.update': write,
102
102
  'work_item.list': read,
103
+ 'work_item.statuses': read,
103
104
  'work_item.get': read,
104
105
  'project.clone': outward,
105
106
  'project.link': write,
@@ -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: "Edit or assign a work item at its current version. Assignees must already be active project members; this does not grant project access. Status is a slug from the project's own status catalogue; a backend that still keeps the six legacy values (backlog, ready, in_progress, in_review, done, cancelled) refuses anything else and names what it accepts.",
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: "List selectable work items for a project before opening an engineering run in this chat. The status filter is a slug from the project's own status catalogue; a backend that still keeps the six legacy values (backlog, ready, in_progress, in_review, done, cancelled) refuses anything else and names what it accepts.",
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(),
@@ -20,6 +20,7 @@ export const backendRecoveryOperationNames = [
20
20
  'organization.list',
21
21
  'project.member_add',
22
22
  'work_item.list',
23
+ 'work_item.statuses',
23
24
  'work_item.get',
24
25
  'session.bootstrap',
25
26
  'session.resume',
@@ -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. Use task.heartbeat during long work. Pause before handing the task to another client.',
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 chosen = await this.workItemPage(`${path}&actionable=true`).catch((error) => {
3557
- if (error instanceof ApiResponseError && error.httpStatus === 400)
3558
- return null;
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