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.
- package/dispatcher/sections.mjs +2 -0
- package/package.json +1 -1
- package/runtime/build.json +1 -1
- package/runtime/dist/src/config.js +11 -0
- package/runtime/dist/src/git/verification-gate.js +2 -0
- package/runtime/dist/src/localization/catalogue.generated.js +26 -0
- package/runtime/dist/src/mcp/delivery-tools.js +313 -100
- package/runtime/dist/src/mcp/live-status-tools.js +37 -0
- package/runtime/dist/src/mcp/server.js +1 -0
- package/runtime/dist/src/mcp/tool-annotations.js +6 -0
- package/runtime/dist/src/mcp/tool-definitions.js +27 -1
- package/runtime/dist/src/mcp/worktree-tools.js +16 -2
- package/runtime/dist/src/runtime/api-client.js +7 -0
- package/runtime/dist/src/runtime/branch-preferences.js +15 -0
- package/runtime/dist/src/runtime/bridge-service.js +291 -30
- package/runtime/dist/src/runtime/create-bridge-service.js +8 -2
- package/runtime/dist/src/runtime/live-signals.js +178 -0
- package/runtime/dist/src/runtime/questionnaire-store.js +7 -0
- package/runtime/dist/src/runtime/task-start.js +45 -15
- package/runtime/dist/src/runtime/worktree-pool.js +97 -20
- package/skill/SKILL.md +2 -0
- package/skill/references/lifecycle.md +32 -3
|
@@ -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.
|
|
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 &&
|
|
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)
|