engineering-memory 1.11.22 → 1.11.24

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.
@@ -0,0 +1,37 @@
1
+ import * as z from 'zod/v4';
2
+ const sharing = z.enum(['managers', 'off']);
3
+ export function registerLiveStatusTools(server, service) {
4
+ server.registerTool('live_status.list', {
5
+ description: 'Show who is working on which Engineering Memory task right now: its stage, whether it is waiting for an answer, which AI tool runs it and when it last acted. Scope it to one project or one organization; with neither it uses the project bound here. It returns identifiers only — people are user ids: project.member_list gives each one’s role and discipline (names are not available to agents), and work_item.get gives a work item’s key and title; resolve them before showing anything to the user. Nobody’s questions, answers, code, file names, branches or chat are in it.',
6
+ inputSchema: z
7
+ .strictObject({
8
+ repoRoot: z.string().min(1).optional(),
9
+ projectId: z.uuid().optional(),
10
+ organizationId: z.uuid().optional(),
11
+ })
12
+ .refine((value) => !(value.projectId && value.organizationId), 'Choose one projectId or organizationId, or neither for the bound project.'),
13
+ }, async (input) => output(await service.liveStatusList(input)));
14
+ server.registerTool('live_status.notice_presented', {
15
+ description: 'Silently record that the live task status notice session.entry returned was shown to the user, with that exact noticeVersion. This is a presentation receipt, never their agreement, and it changes nothing about what is shared. A presented:false answer means the version sent is not a notice version, so nothing was recorded and there is nothing to do: never retry it and never raise it with the user.',
16
+ inputSchema: z.strictObject({
17
+ repoRoot: z.string().min(1).optional(),
18
+ projectId: z.uuid().optional(),
19
+ noticeVersion: z.string().regex(/^[0-9a-f]{64}$/),
20
+ }),
21
+ }, async (input) => output(await service.liveStatusNoticePresented(input)));
22
+ server.registerTool('live_status.set_sharing', {
23
+ description: 'Change who sees live task status in one organization: managers lets the owners and maintainers of each project and the owners of the organization see its tasks, off leaves every task visible only to the person doing it. Call it only when the user asks for that change; pass the sharing you read as expectedSharing so a change someone else made is not overwritten. Organization owners only.',
24
+ inputSchema: z.strictObject({
25
+ organizationId: z.uuid(),
26
+ sharing,
27
+ expectedSharing: sharing,
28
+ }),
29
+ }, async (input) => output(await service.liveStatusSetSharing(input)));
30
+ }
31
+ function output(result) {
32
+ return {
33
+ content: [{ type: 'text', text: JSON.stringify(result) }],
34
+ isError: !result.ok,
35
+ };
36
+ }
37
+ //# sourceMappingURL=live-status-tools.js.map
@@ -12,6 +12,7 @@ export function createMcpServer() {
12
12
  });
13
13
  }
14
14
  export function registerAllTools(host, service) {
15
+ service.live.useHost(() => host.server.getClientVersion()?.name);
15
16
  registerEngineeringMemoryTools(host, service);
16
17
  registerQuestionnaireTools(host, service);
17
18
  registerOnboardingTools(host, service);
@@ -77,6 +77,7 @@ export const toolAnnotations = {
77
77
  'task.resolve_pending_delivery': destructive, // discards a pending delivery
78
78
  'task.verify': write,
79
79
  'task.close': write,
80
+ 'task.delivery': write,
80
81
  'task.abandon': destructive,
81
82
  'task.branch': outward, // fetches the chosen base from the Git remote
82
83
  'architecture.plan': read,
@@ -102,6 +103,8 @@ export const toolAnnotations = {
102
103
  'work_item.runs': read,
103
104
  'work_item.record_test': write,
104
105
  'work_item.test_results': read,
106
+ 'work_item.comment': write,
107
+ 'work_item.comments': read,
105
108
  'work_item.plan': read,
106
109
  'work_item.confirm_plan': write,
107
110
  'project.update': write,
@@ -122,6 +125,9 @@ export const toolAnnotations = {
122
125
  'project.link': write,
123
126
  'project.unlink': destructive,
124
127
  'project.links': read,
128
+ 'live_status.list': read,
129
+ 'live_status.notice_presented': repeatableWrite,
130
+ 'live_status.set_sharing': write,
125
131
  'auth.status': read,
126
132
  'auth.signin_browser': outward,
127
133
  'auth.logout': destructive,
@@ -1,4 +1,5 @@
1
1
  import { registerDeliveryTools } from './delivery-tools.js';
2
+ import { registerLiveStatusTools } from './live-status-tools.js';
2
3
  import { registerStatusMeaningTools } from './status-meaning-tools.js';
3
4
  import { registerStatusRemapTools } from './status-remap-tools.js';
4
5
  import { registerWorkflowTools } from './workflow-tools.js';
@@ -115,6 +116,7 @@ export const engineeringMemoryToolNames = [
115
116
  'task.resolve_pending_delivery',
116
117
  'task.verify',
117
118
  'task.close',
119
+ 'task.delivery',
118
120
  'task.abandon',
119
121
  'task.branch',
120
122
  'architecture.plan',
@@ -140,6 +142,8 @@ export const engineeringMemoryToolNames = [
140
142
  'work_item.runs',
141
143
  'work_item.record_test',
142
144
  'work_item.test_results',
145
+ 'work_item.comment',
146
+ 'work_item.comments',
143
147
  'work_item.plan',
144
148
  'work_item.confirm_plan',
145
149
  'project.update',
@@ -160,6 +164,9 @@ export const engineeringMemoryToolNames = [
160
164
  'project.link',
161
165
  'project.unlink',
162
166
  'project.links',
167
+ 'live_status.list',
168
+ 'live_status.notice_presented',
169
+ 'live_status.set_sharing',
163
170
  'auth.status',
164
171
  'auth.signin_browser',
165
172
  'auth.logout',
@@ -969,7 +976,7 @@ export function registerEngineeringMemoryTools(server, service) {
969
976
  ...workItemLocator,
970
977
  data: z.object({
971
978
  expectedVersion: z.number().int().min(1),
972
- idempotencyKey: z.string().uuid(),
979
+ idempotencyKey: z.uuidv4(),
973
980
  outcome: z.enum(['passed', 'failed', 'blocked']),
974
981
  summary: z.string().trim().min(2).max(4000),
975
982
  testedRevision: z.string().min(1).max(160).optional(),
@@ -984,6 +991,24 @@ export function registerEngineeringMemoryTools(server, service) {
984
991
  limit: z.number().int().min(1).max(100).optional(),
985
992
  }),
986
993
  }, async (input) => toolResult(await service.workItemTestResults(input)));
994
+ server.registerTool('work_item.comment', {
995
+ description: 'Write a comment on an authorized work item, such as the reason a tester sent it back to Rework. Comments are permanent: correct one with a new comment. Reuse the same idempotency key only for an identical retry.',
996
+ inputSchema: z.object({
997
+ ...workItemLocator,
998
+ data: z.object({
999
+ idempotencyKey: z.uuidv4(),
1000
+ body: z.string().trim().min(1).max(4000),
1001
+ }),
1002
+ }),
1003
+ }, async (input) => toolResult(await service.workItemComment(input)));
1004
+ server.registerTool('work_item.comments', {
1005
+ description: 'Read the comments of an authorized work item, newest first. Read them before starting a round on work that came back from testing.',
1006
+ inputSchema: z.object({
1007
+ ...workItemLocator,
1008
+ offset: z.number().int().min(0).optional(),
1009
+ limit: z.number().int().min(1).max(100).optional(),
1010
+ }),
1011
+ }, async (input) => toolResult(await service.workItemComments(input)));
987
1012
  server.registerTool('work_item.plan', {
988
1013
  description: 'Inspect authorized current and linked project targets, repository URLs, implementation capabilities and the persisted shared branch before opening work. Check contracts to identify backend/web/mobile impact; capability is not user consent.',
989
1014
  inputSchema: z.object(workItemLocator),
@@ -1207,6 +1232,7 @@ export function registerEngineeringMemoryTools(server, service) {
1207
1232
  description: 'List the projects linked to this one, and which side of the product each is.',
1208
1233
  inputSchema: z.object({ projectId: z.string().min(1) }),
1209
1234
  }, async (input) => toolResult(await service.projectLinks(input)));
1235
+ registerLiveStatusTools(server, service);
1210
1236
  server.registerTool('auth.status', {
1211
1237
  description: 'Check whether OS credential storage contains an active local Engineering Memory session.',
1212
1238
  inputSchema: z.object({}),
@@ -30,6 +30,8 @@ function deferredStart(input, questionnaireId) {
30
30
  name: input.name,
31
31
  base: input.base,
32
32
  keepCurrent: input.keepCurrent,
33
+ workItemId: input.workItemId,
34
+ workItemProjectId: input.workItemProjectId,
33
35
  },
34
36
  },
35
37
  nextAction: `Nothing was created, reserved or saved. Tell the user in one line that starting ${input.externalTaskId} is on hold. Continue only read-only work. When the user wants to start, call reconsider exactly as given; it asks the start form again.`,
@@ -285,7 +287,7 @@ export function registerWorktreeTools(server, service) {
285
287
  inputSchema: z.strictObject(policy),
286
288
  }, async (input) => output(await service.worktreePolicy(json(input))));
287
289
  server.registerTool('task.branch', {
288
- description: 'Start a write task by selecting its decision mode in one short native form, then resolve two short questions for the starting branch (unless base is given) and the folder. A unique branch name is generated automatically unless name is supplied. Folder options: a separate managed worktree, this folder as a new branch when it is clean (moving out an unfinished task that holds it, named in the option), this folder on its current branch, or not now. Nothing is created until a valid native or mode-delegated start answer is recorded; the next call with the same arguments allocates. A remote base is fetched and its exact commit pinned. Retry/resume preserves the allocation. Not now returns status deferred with the exact reconsider call; retries keep the same decisionAttempt. Use the returned repoRoot for ALL commands. A read-only task allocates only when transitioning to write.',
290
+ description: 'Start a write task by selecting its decision mode in one short native form, then resolve two short questions for the starting branch (unless base is given) and the folder. A unique branch name is generated automatically unless name is supplied. Pass workItemId (and workItemProjectId when the item lives in a linked project) for work on a work item: the branch question then also offers continuing the branch of its plan or of its earlier rounds, fetched from origin when this clone lacks it, and a planned item that has no branch yet gets its plan branch as the new name. Folder options: a separate managed worktree, this folder as a new branch when it is clean (moving out an unfinished task that holds it, named in the option), this folder on its current branch, or not now. Nothing is created until a valid native or mode-delegated start answer is recorded; the next call with the same arguments allocates. A remote base is fetched and its exact commit pinned. Retry/resume preserves the allocation. Not now returns status deferred with the exact reconsider call; retries keep the same decisionAttempt. Use the returned repoRoot for ALL commands. A read-only task allocates only when transitioning to write.',
289
291
  inputSchema: z.strictObject({
290
292
  repoRoot: repo,
291
293
  externalTaskId: z.string().min(2).max(160),
@@ -293,6 +295,8 @@ export function registerWorktreeTools(server, service) {
293
295
  name: branch.optional(),
294
296
  base: baseSchema.optional(),
295
297
  keepCurrent: z.boolean().optional(),
298
+ workItemId: z.string().uuid().optional(),
299
+ workItemProjectId: z.string().uuid().optional(),
296
300
  language: languageTag.optional(),
297
301
  presentation,
298
302
  }),
@@ -319,9 +323,14 @@ export function registerWorktreeTools(server, service) {
319
323
  recovery: 'task.branch',
320
324
  },
321
325
  });
326
+ const plan = flight.planBranch;
322
327
  const { previous, ...definition } = taskStartDefinition({
323
328
  ...input,
324
329
  ...flight,
330
+ ...(plan ? { name: plan } : {}),
331
+ ...(plan && flight.continueBranches.includes(plan)
332
+ ? { base: { kind: 'existing', branch: plan } }
333
+ : {}),
325
334
  language: told ?? 'en',
326
335
  });
327
336
  const owner = {
@@ -333,6 +342,8 @@ export function registerWorktreeTools(server, service) {
333
342
  name: input.name,
334
343
  base: input.base,
335
344
  keepCurrent: input.keepCurrent,
345
+ workItemId: input.workItemId,
346
+ workItemProjectId: input.workItemProjectId,
336
347
  }),
337
348
  };
338
349
  const form = await askQuestionnaire(server, service, { ...definition, repoRoot: input.repoRoot, presentation: input.presentation }, context, previous, owner);
@@ -359,7 +370,10 @@ export function registerWorktreeTools(server, service) {
359
370
  recovery: 'task.branch',
360
371
  },
361
372
  });
362
- if (choice.name && choice.name !== input.name && choice.name !== flight.suggestedName) {
373
+ if (choice.name &&
374
+ choice.base.kind !== 'existing' &&
375
+ choice.name !== input.name &&
376
+ choice.name !== flight.suggestedName) {
363
377
  const unusable = await service.unusableBranchName(input.repoRoot, choice.name);
364
378
  if (unusable)
365
379
  return reconsider(unusable);
@@ -8,6 +8,7 @@ export const backendRecoveryOperationNames = [
8
8
  'auth.web_csrf',
9
9
  'auth.web_signin',
10
10
  'audit.list',
11
+ 'live_status.list',
11
12
  'organization.member_list',
12
13
  'organization.member_upsert',
13
14
  'project.setup',
@@ -23,6 +24,8 @@ export const backendRecoveryOperationNames = [
23
24
  'work_item.statuses',
24
25
  'work_item.workflow',
25
26
  'work_item.get',
27
+ 'work_item.comment',
28
+ 'work_item.comments',
26
29
  'session.bootstrap',
27
30
  'session.resume',
28
31
  'context.refresh',
@@ -47,6 +50,7 @@ export const backendRecoveryOperationNames = [
47
50
  'task.abandon',
48
51
  'task.branch',
49
52
  'task.resolve_pending_delivery',
53
+ 'task.delivery',
50
54
  ];
51
55
  const backendRecoveryOperations = new Set(backendRecoveryOperationNames);
52
56
  const browserSigninRecovery = 'auth.signin_browser';
@@ -119,6 +123,9 @@ export class ApiClient {
119
123
  throw new ApiResponseError('Authentication is required. Recovery: call auth.signin_browser.', 401, null, false, null, browserSigninRecovery);
120
124
  }
121
125
  headers.Authorization = `Bearer ${accessToken}`;
126
+ const live = this.options.liveHeader?.(path);
127
+ if (live)
128
+ headers['x-em-live'] = live;
122
129
  }
123
130
  const requestSignal = request.networkTimeoutMs === undefined
124
131
  ? request.signal
@@ -1,5 +1,20 @@
1
1
  import { BridgeRecoveryError } from './recovery-error.js';
2
2
  export async function resolveTaskBase(git, repoRoot, base, preferences) {
3
+ if (base.kind === 'existing') {
4
+ const local = await git.sourceIdentity(repoRoot, 'refs/heads/' + base.branch);
5
+ const fetched = await git.fetchBranchCommit(repoRoot, 'origin', base.branch).catch(() => null);
6
+ const tip = !fetched ||
7
+ (local && (await git.isAncestor(repoRoot, fetched.sourceCommit, local.sourceCommit)))
8
+ ? local
9
+ : !local || (await git.isAncestor(repoRoot, local.sourceCommit, fetched.sourceCommit))
10
+ ? fetched
11
+ : null;
12
+ if (!tip)
13
+ throw new BridgeRecoveryError(local
14
+ ? `Branch ${base.branch} here and on origin went separate ways, so it cannot simply be continued. Bring the two together with Git, or call task.branch with a new decisionAttempt and start a new branch.`
15
+ : `Branch ${base.branch} is neither in this clone nor on origin, so it cannot be continued. Call task.branch with a new decisionAttempt and start a new branch.`, 'task.branch');
16
+ return { ...tip, base };
17
+ }
3
18
  if (base.kind === 'current' || base.kind === 'local') {
4
19
  const source = await git.sourceIdentity(repoRoot, base.kind === 'current' ? (base.commit ?? 'HEAD') : base.ref);
5
20
  if (!source && base.kind === 'current' && !base.commit)