engineering-memory 1.11.27 → 1.11.29
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 +9 -0
- package/runtime/dist/src/git/git-inspector.js +40 -8
- package/runtime/dist/src/localization/catalogue.generated.js +124 -0
- package/runtime/dist/src/mcp/delivery-tools.js +241 -50
- package/runtime/dist/src/mcp/review-tools.js +369 -0
- package/runtime/dist/src/mcp/tool-annotations.js +11 -0
- package/runtime/dist/src/mcp/tool-definitions.js +18 -0
- package/runtime/dist/src/mcp/worktree-tools.js +12 -1
- package/runtime/dist/src/providers/gitlab-token-page.js +226 -0
- package/runtime/dist/src/providers/gitlab.js +421 -0
- package/runtime/dist/src/runtime/api-client.js +7 -0
- package/runtime/dist/src/runtime/bridge-service.js +366 -3
- package/runtime/dist/src/runtime/create-bridge-service.js +16 -2
- package/runtime/dist/src/runtime/merge-request-sync.js +322 -0
- package/runtime/dist/src/runtime/principal-state.js +4 -1
- package/runtime/dist/src/runtime/review-masking.js +31 -0
- package/runtime/dist/src/runtime/task-start.js +12 -6
- package/skill/references/lifecycle.md +12 -2
|
@@ -7,6 +7,7 @@ const option = z.strictObject({
|
|
|
7
7
|
label: z.string().trim().min(1).max(100),
|
|
8
8
|
description: z.string().trim().min(1).max(240),
|
|
9
9
|
});
|
|
10
|
+
const reviewLine = z.string().trim().min(1).max(300);
|
|
10
11
|
const copySchema = z.strictObject({
|
|
11
12
|
message: z.string().trim().min(1).max(240),
|
|
12
13
|
context: z.string().trim().min(1).max(500),
|
|
@@ -17,12 +18,26 @@ const copySchema = z.strictObject({
|
|
|
17
18
|
commit_push_draft_pr: option,
|
|
18
19
|
commit_push_pr: option,
|
|
19
20
|
defer: option,
|
|
21
|
+
commit_push_review_pr: option.optional(),
|
|
22
|
+
commit_push_pr_unreviewed: option.optional(),
|
|
23
|
+
reviewRequired: reviewLine.optional(),
|
|
24
|
+
reviewOptional: reviewLine.optional(),
|
|
25
|
+
reviewFix: reviewLine.optional(),
|
|
20
26
|
baseMessage: z.string().trim().min(1).max(240),
|
|
21
27
|
baseContext: z.string().trim().min(1).max(500),
|
|
22
28
|
baseTitle: z.string().trim().min(1).max(100),
|
|
23
29
|
baseWrite: option,
|
|
24
30
|
recorded: z.string().trim().min(1).max(100).optional(),
|
|
25
31
|
});
|
|
32
|
+
const deliveryChoices = [
|
|
33
|
+
'commit',
|
|
34
|
+
'commit_push',
|
|
35
|
+
'commit_push_review_pr',
|
|
36
|
+
'commit_push_draft_pr',
|
|
37
|
+
'commit_push_pr',
|
|
38
|
+
'defer',
|
|
39
|
+
];
|
|
40
|
+
const reviewPolicy = z.enum(['none', 'optional', 'required']);
|
|
26
41
|
const closedSchema = z.object({
|
|
27
42
|
closed: z.literal(true),
|
|
28
43
|
taskVersion: z.number().int().positive(),
|
|
@@ -47,8 +62,11 @@ const closedSchema = z.object({
|
|
|
47
62
|
taskId: z.string(),
|
|
48
63
|
lockVersion: z.number().int(),
|
|
49
64
|
choice: z.enum(['commit', 'commit_push', 'commit_push_draft_pr', 'commit_push_pr']).nullish(),
|
|
65
|
+
reviewState: z.string().nullish(),
|
|
50
66
|
})
|
|
51
67
|
.nullish(),
|
|
68
|
+
reviewPolicy: reviewPolicy.optional(),
|
|
69
|
+
reviewFixOf: z.object({ taskId: z.string(), externalTaskId: z.string() }).nullish(),
|
|
52
70
|
});
|
|
53
71
|
const recordWording = z.strictObject({
|
|
54
72
|
message: z.string().trim().min(1).max(240),
|
|
@@ -63,10 +81,53 @@ const recordSchema = z.object({
|
|
|
63
81
|
branch: z.string().nullable(),
|
|
64
82
|
state: z.enum(['unanswered', 'answered', 'delivered', 'cancelled']),
|
|
65
83
|
lockVersion: z.number().int(),
|
|
84
|
+
reviewPolicy: reviewPolicy.optional(),
|
|
85
|
+
reviewFixOfDeliveryId: z.string().nullish(),
|
|
66
86
|
});
|
|
87
|
+
function offered(policy, fix) {
|
|
88
|
+
if (fix)
|
|
89
|
+
return ['commit', 'commit_push'];
|
|
90
|
+
if (policy === 'required')
|
|
91
|
+
return ['commit', 'commit_push', 'commit_push_review_pr'];
|
|
92
|
+
if (policy === 'optional')
|
|
93
|
+
return ['commit', 'commit_push', 'commit_push_review_pr', 'commit_push_pr'];
|
|
94
|
+
return ['commit', 'commit_push', 'commit_push_draft_pr', 'commit_push_pr'];
|
|
95
|
+
}
|
|
96
|
+
function wording(copy, policy, choice) {
|
|
97
|
+
return choice === 'commit_push_pr' && policy === 'optional'
|
|
98
|
+
? copy.commit_push_pr_unreviewed
|
|
99
|
+
: copy[choice];
|
|
100
|
+
}
|
|
101
|
+
function pullRequest(choice) {
|
|
102
|
+
return (choice === 'commit_push_review_pr' ||
|
|
103
|
+
choice === 'commit_push_draft_pr' ||
|
|
104
|
+
choice === 'commit_push_pr');
|
|
105
|
+
}
|
|
106
|
+
const incompleteCopy = {
|
|
107
|
+
ok: false,
|
|
108
|
+
error: {
|
|
109
|
+
kind: 'delivery',
|
|
110
|
+
message: "Supply complete delivery copy in the conversation language before closing, including the code review wording this project's question needs (commit_push_review_pr, commit_push_pr_unreviewed, reviewRequired, reviewOptional, reviewFix).",
|
|
111
|
+
recovery: 'task.close',
|
|
112
|
+
retryable: false,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
function settingChanged(tool, now) {
|
|
116
|
+
return output({
|
|
117
|
+
ok: false,
|
|
118
|
+
error: {
|
|
119
|
+
kind: 'delivery',
|
|
120
|
+
message: `The project's code review setting changed to ${now} while the delivery question was open, so the answer was not recorded and no Git action was performed. Tell the user, then call ${tool} again: its question follows the new setting.`,
|
|
121
|
+
recovery: tool,
|
|
122
|
+
retryable: true,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
67
126
|
export function registerDeliveryTools(server, service) {
|
|
68
127
|
registerTaskDelivery(server, service);
|
|
69
128
|
registerTaskReviewRequest(server, service);
|
|
129
|
+
registerTaskOpenReviewRequest(server, service);
|
|
130
|
+
registerGitLabToken(server, service);
|
|
70
131
|
server.registerTool('task.close', {
|
|
71
132
|
description: 'Close the verified task, then open its durable mode-governed Git delivery selector in the same call. Memory publication is separate. Cancellation/feedback leave delivery pending; retry identical input to resume. PR/MR is one review-request concept, not an instruction to merge. No Git action is performed by this tool. Read-only tasks need no delivery. Supply complete copy when the product text catalogue lacks the conversation language. If the user already explicitly chose Git delivery in their own message, deliveryInstruction preserves that choice with its exact relevant excerpt; it is an agent-reported instruction, never a native answer or a substitute for missing user consent. Do not use it for agent decisions or memory-publication approval.',
|
|
72
133
|
inputSchema: z.object({
|
|
@@ -77,13 +138,7 @@ export function registerDeliveryTools(server, service) {
|
|
|
77
138
|
decisionAttempt: z.number().int().min(1).max(10000).optional(),
|
|
78
139
|
deliveryInstruction: z
|
|
79
140
|
.strictObject({
|
|
80
|
-
choice: z.enum(
|
|
81
|
-
'commit',
|
|
82
|
-
'commit_push',
|
|
83
|
-
'commit_push_draft_pr',
|
|
84
|
-
'commit_push_pr',
|
|
85
|
-
'defer',
|
|
86
|
-
]),
|
|
141
|
+
choice: z.enum(deliveryChoices),
|
|
87
142
|
userRequestExcerpt: z.string().trim().min(1).max(1200),
|
|
88
143
|
baseBranch: z.string().trim().min(1).max(240).optional(),
|
|
89
144
|
})
|
|
@@ -121,6 +176,8 @@ export function registerDeliveryTools(server, service) {
|
|
|
121
176
|
const data = parsed.data;
|
|
122
177
|
if (data.deliveryContext.mode === 'read_only')
|
|
123
178
|
return output(closed);
|
|
179
|
+
const policy = data.reviewPolicy ?? 'none';
|
|
180
|
+
const fix = data.reviewFixOf ?? null;
|
|
124
181
|
const delivered = data.delivery.alreadyDelivered;
|
|
125
182
|
if (delivered)
|
|
126
183
|
return output({
|
|
@@ -128,7 +185,12 @@ export function registerDeliveryTools(server, service) {
|
|
|
128
185
|
data: {
|
|
129
186
|
...closed.data,
|
|
130
187
|
deliveryStatus: 'delivered',
|
|
131
|
-
nextAction: `Everything this task changed was already committed and pushed by hand: ${delivered.ref.replace(/^refs\/remotes\//, '')} contains ${delivered.commit.slice(0, 12)}. There is nothing left to deliver, so no delivery question is asked, and the delivery is recorded for the project. Call worktree.release in this folder with deliveryOutcome delivered to settle the folder
|
|
188
|
+
nextAction: `Everything this task changed was already committed and pushed by hand: ${delivered.ref.replace(/^refs\/remotes\//, '')} contains ${delivered.commit.slice(0, 12)}. There is nothing left to deliver, so no delivery question is asked, and the delivery is recorded for the project. Call worktree.release in this folder with deliveryOutcome delivered to settle the folder.` +
|
|
189
|
+
(fix
|
|
190
|
+
? ` This task fixes the review findings of ${fix.externalTaskId}: once the push is recorded, its open findings are marked fixed at this commit. Read review.get for ${fix.externalTaskId} and ask the user whether to review it again (review.start) or end its review as passed (review.conclude).`
|
|
191
|
+
: policy === 'required'
|
|
192
|
+
? ' Code review is required in this project: if a PR/MR is opened for this branch, open it as a draft and record it (task.open_review_request on GitLab, task.review_request elsewhere). Recording it starts the review, which review.start runs.'
|
|
193
|
+
: ''),
|
|
132
194
|
},
|
|
133
195
|
});
|
|
134
196
|
const destination = data.delivery.destination;
|
|
@@ -150,6 +212,8 @@ export function registerDeliveryTools(server, service) {
|
|
|
150
212
|
language,
|
|
151
213
|
copy,
|
|
152
214
|
decisionAttempt: input.decisionAttempt ?? 0,
|
|
215
|
+
reviewPolicy: policy,
|
|
216
|
+
reviewFixOf: fix?.taskId ?? null,
|
|
153
217
|
};
|
|
154
218
|
const binding = { deliveryDigest: sha256(stableStringify(identity)) };
|
|
155
219
|
const owner = {
|
|
@@ -161,20 +225,47 @@ export function registerDeliveryTools(server, service) {
|
|
|
161
225
|
const questionnaireId = 'task-delivery-' + binding.deliveryDigest;
|
|
162
226
|
const canPush = Boolean(destination.remote && destination.pushUrl);
|
|
163
227
|
const ids = canPush
|
|
164
|
-
? [
|
|
228
|
+
? [...offered(policy, Boolean(fix)), 'defer']
|
|
165
229
|
: ['commit', 'defer'];
|
|
166
230
|
const instruction = input.deliveryInstruction;
|
|
167
|
-
const
|
|
168
|
-
|
|
231
|
+
const instructed = !instruction
|
|
232
|
+
? null
|
|
233
|
+
: fix && pullRequest(instruction.choice)
|
|
234
|
+
? 'commit_push'
|
|
235
|
+
: policy === 'required' && pullRequest(instruction.choice)
|
|
236
|
+
? 'commit_push_review_pr'
|
|
237
|
+
: instruction.choice;
|
|
238
|
+
if (instructed && !ids.includes(instructed))
|
|
169
239
|
return output({
|
|
170
240
|
ok: false,
|
|
171
241
|
error: {
|
|
172
242
|
kind: 'delivery',
|
|
173
|
-
message:
|
|
243
|
+
message: !canPush
|
|
244
|
+
? 'This delivery needs an available remote and push URL. No Git action was performed.'
|
|
245
|
+
: policy === 'none'
|
|
246
|
+
? 'Code review is off in this project, so a reviewed PR/MR is not offered. No Git action was performed. Call task.close without deliveryInstruction to ask the user how to deliver.'
|
|
247
|
+
: 'Code review is optional in this project, so a draft PR/MR is either reviewed first or opened without review, and the instruction does not say which. No Git action was performed. Call task.close without deliveryInstruction to ask the user.',
|
|
174
248
|
recovery: 'task.close',
|
|
175
249
|
retryable: false,
|
|
176
250
|
},
|
|
177
251
|
});
|
|
252
|
+
const line = !canPush
|
|
253
|
+
? ''
|
|
254
|
+
: fix
|
|
255
|
+
? copy.reviewFix && format(copy.reviewFix, language ?? 'en', { task: fix.externalTaskId })
|
|
256
|
+
: policy === 'required'
|
|
257
|
+
? copy.reviewRequired
|
|
258
|
+
: policy === 'optional'
|
|
259
|
+
? copy.reviewOptional
|
|
260
|
+
: '';
|
|
261
|
+
if (!instruction && (line === undefined || ids.some((id) => !wording(copy, policy, id))))
|
|
262
|
+
return output(incompleteCopy);
|
|
263
|
+
const recorded = data.deliveryRecord?.choice;
|
|
264
|
+
const recordedLabel = !recorded
|
|
265
|
+
? undefined
|
|
266
|
+
: data.deliveryRecord?.reviewState === 'pending'
|
|
267
|
+
? (copy.commit_push_review_pr ?? copy[recorded]).label
|
|
268
|
+
: (wording(copy, policy, recorded) ?? copy[recorded]).label;
|
|
178
269
|
const form = instruction
|
|
179
270
|
? null
|
|
180
271
|
: await askQuestionnaire(server, service, {
|
|
@@ -190,18 +281,15 @@ export function registerDeliveryTools(server, service) {
|
|
|
190
281
|
': ' +
|
|
191
282
|
destination.branch +
|
|
192
283
|
(canPush ? ' → ' + destination.remote + ' (' + destination.pushUrl + ')' : '') +
|
|
193
|
-
(
|
|
194
|
-
? '\n' + copy.recorded + ': ' +
|
|
195
|
-
: '')
|
|
284
|
+
(recordedLabel && copy.recorded
|
|
285
|
+
? '\n' + copy.recorded + ': ' + recordedLabel
|
|
286
|
+
: '') +
|
|
287
|
+
(line ? '\n' + line : ''),
|
|
196
288
|
example: copy.example,
|
|
197
|
-
options: ids.map((id) => ({
|
|
198
|
-
id,
|
|
199
|
-
label: copy[id].label,
|
|
200
|
-
description: copy[id].description,
|
|
201
|
-
})),
|
|
289
|
+
options: ids.map((id) => ({ id, ...wording(copy, policy, id) })),
|
|
202
290
|
binding,
|
|
203
291
|
}, context, [], owner);
|
|
204
|
-
const choice =
|
|
292
|
+
const choice = instructed ?? (form ? answerChoice(form) : null);
|
|
205
293
|
if (!choice)
|
|
206
294
|
return form;
|
|
207
295
|
const { deliveryInstruction: _instruction, ...formInput } = input;
|
|
@@ -225,11 +313,9 @@ export function registerDeliveryTools(server, service) {
|
|
|
225
313
|
return deferred();
|
|
226
314
|
service.live.deliveryStarted();
|
|
227
315
|
try {
|
|
228
|
-
let baseBranch = choice
|
|
229
|
-
? instruction?.baseBranch
|
|
230
|
-
: undefined;
|
|
316
|
+
let baseBranch = pullRequest(choice) ? instruction?.baseBranch : undefined;
|
|
231
317
|
let baseQuestionnaireId;
|
|
232
|
-
if ((choice
|
|
318
|
+
if (pullRequest(choice) && !baseBranch) {
|
|
233
319
|
baseQuestionnaireId = 'task-delivery-base-' + sha256(questionnaireId + choice);
|
|
234
320
|
const base = await askPullRequestBase(server, service, context, { ...input, questionnaireId: baseQuestionnaireId, binding: { ...binding, choice } }, language, copy, owner);
|
|
235
321
|
if (!answerChoice(base))
|
|
@@ -240,7 +326,7 @@ export function registerDeliveryTools(server, service) {
|
|
|
240
326
|
if (!baseBranch)
|
|
241
327
|
return base;
|
|
242
328
|
}
|
|
243
|
-
if (choice
|
|
329
|
+
if (pullRequest(choice)) {
|
|
244
330
|
const valid = await service.taskDeliveryBase({
|
|
245
331
|
repoRoot: input.repoRoot,
|
|
246
332
|
branch: baseBranch,
|
|
@@ -297,6 +383,9 @@ export function registerDeliveryTools(server, service) {
|
|
|
297
383
|
retryable: false,
|
|
298
384
|
},
|
|
299
385
|
});
|
|
386
|
+
const now = current.data.reviewPolicy ?? 'none';
|
|
387
|
+
if (pullRequest(choice) && now !== policy)
|
|
388
|
+
return settingChanged('task.close', now);
|
|
300
389
|
const question = instruction
|
|
301
390
|
? null
|
|
302
391
|
: await service.questionnaireResume({ repoRoot: input.repoRoot, questionnaireId });
|
|
@@ -311,14 +400,21 @@ export function registerDeliveryTools(server, service) {
|
|
|
311
400
|
projectId: record.projectId,
|
|
312
401
|
taskId: record.taskId,
|
|
313
402
|
expectedVersion: record.lockVersion,
|
|
314
|
-
choice,
|
|
403
|
+
choice: choice === 'commit_push_review_pr' ? 'commit_push_draft_pr' : choice,
|
|
315
404
|
...(baseBranch ? { baseBranch } : {}),
|
|
405
|
+
...(pullRequest(choice) && data.reviewPolicy
|
|
406
|
+
? { expectedReviewPolicy: data.reviewPolicy }
|
|
407
|
+
: {}),
|
|
316
408
|
answerSource: question ? answerSourceName(question.answerSource) : 'user_instruction',
|
|
317
409
|
})
|
|
318
410
|
: null;
|
|
319
411
|
const answered = answer?.ok ? answer.data : null;
|
|
320
412
|
const refused = answer && !answer.ok ? answer.error : null;
|
|
321
|
-
const
|
|
413
|
+
const details = refused?.details;
|
|
414
|
+
if (details?.reviewPolicy)
|
|
415
|
+
return settingChanged('task.close', details.reviewPolicy);
|
|
416
|
+
const other = details?.delivery;
|
|
417
|
+
const told = instruction && instructed !== instruction.choice ? instruction.choice : null;
|
|
322
418
|
return output({
|
|
323
419
|
ok: true,
|
|
324
420
|
data: {
|
|
@@ -350,7 +446,24 @@ export function registerDeliveryTools(server, service) {
|
|
|
350
446
|
...(baseQuestionnaireId ? { baseQuestionnaireId } : {}),
|
|
351
447
|
destination,
|
|
352
448
|
},
|
|
353
|
-
nextAction:
|
|
449
|
+
nextAction: [
|
|
450
|
+
'Perform only the selected Git delivery under host permissions. No commit, push, PR/MR or merge was executed by task.close. Preserve this exact branch and destination.',
|
|
451
|
+
told && fix
|
|
452
|
+
? `The user asked for a PR/MR, but this task fixes the review findings of ${fix.externalTaskId}, whose PR/MR is already open: its changes are committed and pushed onto that branch and no new PR/MR is opened. Tell the user.`
|
|
453
|
+
: told
|
|
454
|
+
? 'The user asked for a PR/MR without saying it is reviewed, and code review is required in this project, so it opens as a draft and becomes ready once the review ends. Tell the user.'
|
|
455
|
+
: '',
|
|
456
|
+
choice === 'commit_push_review_pr'
|
|
457
|
+
? `After the push and worktree.release, open the PR/MR into ${baseBranch} as a draft: on GitLab task.open_review_request opens it as a draft; elsewhere open it as a draft with your own tools and record it with task.review_request. Then start the code review with review.start for this task. The PR/MR becomes ready when the review ends.`
|
|
458
|
+
: '',
|
|
459
|
+
fix && choice === 'commit_push'
|
|
460
|
+
? `The push goes onto ${destination.branch}, the open PR/MR branch of ${fix.externalTaskId}; open no new PR/MR. Once the push is recorded by worktree.release, the open findings of ${fix.externalTaskId} are marked fixed at that commit: read review.get for ${fix.externalTaskId} and ask the user whether to review it again (review.start) or end its review as passed (review.conclude).`
|
|
461
|
+
: fix
|
|
462
|
+
? `The findings of ${fix.externalTaskId} stay open until this commit is pushed onto its PR/MR branch.`
|
|
463
|
+
: '',
|
|
464
|
+
]
|
|
465
|
+
.filter(Boolean)
|
|
466
|
+
.join(' '),
|
|
354
467
|
},
|
|
355
468
|
});
|
|
356
469
|
}
|
|
@@ -373,10 +486,10 @@ function registerTaskDelivery(server, service) {
|
|
|
373
486
|
}, async (input, context) => {
|
|
374
487
|
const language = await service.language(input.language);
|
|
375
488
|
const copy = language ? copySchema.safeParse(texts('delivery', language)).data : undefined;
|
|
376
|
-
const
|
|
489
|
+
const recordCopy = language
|
|
377
490
|
? recordWording.safeParse(texts('taskDelivery', language)).data
|
|
378
491
|
: undefined;
|
|
379
|
-
if (!language || !copy || !
|
|
492
|
+
if (!language || !copy || !recordCopy)
|
|
380
493
|
return output({
|
|
381
494
|
ok: false,
|
|
382
495
|
error: {
|
|
@@ -408,7 +521,14 @@ function registerTaskDelivery(server, service) {
|
|
|
408
521
|
nextAction: 'This delivery is already concluded. Tell the user what was recorded.',
|
|
409
522
|
},
|
|
410
523
|
});
|
|
411
|
-
const
|
|
524
|
+
const policy = record.reviewPolicy ?? 'none';
|
|
525
|
+
const fix = Boolean(record.reviewFixOfDeliveryId);
|
|
526
|
+
const binding = {
|
|
527
|
+
taskId: record.taskId,
|
|
528
|
+
lockVersion: record.lockVersion,
|
|
529
|
+
reviewPolicy: policy,
|
|
530
|
+
reviewFix: fix,
|
|
531
|
+
};
|
|
412
532
|
const questionnaireId = 'task-delivery-record-' +
|
|
413
533
|
sha256(stableStringify({ ...binding, decisionAttempt: input.decisionAttempt ?? 0 }));
|
|
414
534
|
const owner = {
|
|
@@ -421,28 +541,38 @@ function registerTaskDelivery(server, service) {
|
|
|
421
541
|
language: input.language,
|
|
422
542
|
},
|
|
423
543
|
};
|
|
424
|
-
const ids =
|
|
425
|
-
|
|
426
|
-
'
|
|
427
|
-
'
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
544
|
+
const ids = offered(policy, fix);
|
|
545
|
+
const line = fix || policy === 'none'
|
|
546
|
+
? ''
|
|
547
|
+
: policy === 'required'
|
|
548
|
+
? copy.reviewRequired
|
|
549
|
+
: copy.reviewOptional;
|
|
550
|
+
if (line === undefined || ids.some((id) => !wording(copy, policy, id)))
|
|
551
|
+
return output({
|
|
552
|
+
ok: false,
|
|
553
|
+
error: {
|
|
554
|
+
kind: 'delivery',
|
|
555
|
+
message: "The delivery question has no code review text in the conversation language, and this project's question needs it. Ask again in a language Engineering Memory offers.",
|
|
556
|
+
recovery: 'task.delivery',
|
|
557
|
+
retryable: false,
|
|
558
|
+
},
|
|
559
|
+
});
|
|
432
560
|
const form = await askQuestionnaire(server, service, {
|
|
433
561
|
repoRoot: input.repoRoot,
|
|
434
562
|
presentation: input.presentation,
|
|
435
563
|
questionnaireId,
|
|
436
564
|
language,
|
|
437
565
|
impact: 'critical',
|
|
438
|
-
message: format(
|
|
439
|
-
context:
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
566
|
+
message: format(recordCopy.message, language, { task: record.externalTaskId }),
|
|
567
|
+
context: recordCopy.context +
|
|
568
|
+
(record.branch ? '\n' + copy.destination + ': ' + record.branch : '') +
|
|
569
|
+
(line ? '\n' + line : ''),
|
|
570
|
+
example: recordCopy.example,
|
|
571
|
+
options: [
|
|
572
|
+
...ids.map((id) => ({ id, ...wording(copy, policy, id) })),
|
|
573
|
+
{ id: 'abandon', ...recordCopy.abandon },
|
|
574
|
+
{ id: 'defer', ...copy.defer },
|
|
575
|
+
],
|
|
446
576
|
binding,
|
|
447
577
|
}, context, [], owner);
|
|
448
578
|
const choice = answerChoice(form);
|
|
@@ -464,7 +594,7 @@ function registerTaskDelivery(server, service) {
|
|
|
464
594
|
reason: 'abandoned',
|
|
465
595
|
}));
|
|
466
596
|
let baseBranch;
|
|
467
|
-
if (choice
|
|
597
|
+
if (pullRequest(choice)) {
|
|
468
598
|
const base = await askPullRequestBase(server, service, context, {
|
|
469
599
|
...input,
|
|
470
600
|
questionnaireId: 'task-delivery-base-' + sha256(questionnaireId + choice),
|
|
@@ -493,10 +623,17 @@ function registerTaskDelivery(server, service) {
|
|
|
493
623
|
projectId: input.projectId,
|
|
494
624
|
taskId: input.taskId,
|
|
495
625
|
expectedVersion: record.lockVersion,
|
|
496
|
-
choice,
|
|
626
|
+
choice: choice === 'commit_push_review_pr' ? 'commit_push_draft_pr' : choice,
|
|
497
627
|
...(baseBranch ? { baseBranch } : {}),
|
|
628
|
+
...(pullRequest(choice) && record.reviewPolicy
|
|
629
|
+
? { expectedReviewPolicy: record.reviewPolicy }
|
|
630
|
+
: {}),
|
|
498
631
|
answerSource: answerSourceName(question.answerSource),
|
|
499
632
|
});
|
|
633
|
+
const changed = answer.error?.details
|
|
634
|
+
?.reviewPolicy;
|
|
635
|
+
if (changed)
|
|
636
|
+
return settingChanged('task.delivery', changed);
|
|
500
637
|
if (!answer.ok)
|
|
501
638
|
return output(answer);
|
|
502
639
|
return output({
|
|
@@ -536,6 +673,60 @@ function registerTaskReviewRequest(server, service) {
|
|
|
536
673
|
});
|
|
537
674
|
});
|
|
538
675
|
}
|
|
676
|
+
function registerTaskOpenReviewRequest(server, service) {
|
|
677
|
+
server.registerTool('task.open_review_request', {
|
|
678
|
+
description: "Open the merge request of a delivered task on GitLab from this computer and record it, in one call. It works for a GitLab only this computer's network reaches, with the user's own token saved on this computer through gitlab.token. The target branch and draft come from the delivery answer unless given here. The branch must already be on GitLab with the delivered commit. An open merge request of the branch is reused, never opened twice. When it refuses, it says why and what works instead; opening the merge request in GitLab by hand and recording it with task.review_request always works.",
|
|
679
|
+
inputSchema: z.strictObject({
|
|
680
|
+
projectId: z.string().uuid(),
|
|
681
|
+
taskId: z.string().uuid(),
|
|
682
|
+
repoRoot: z.string().min(1).optional(),
|
|
683
|
+
targetBranch: z.string().trim().min(1).max(240).optional(),
|
|
684
|
+
draft: z.boolean().optional(),
|
|
685
|
+
title: z
|
|
686
|
+
.string()
|
|
687
|
+
.trim()
|
|
688
|
+
.min(1)
|
|
689
|
+
.max(200)
|
|
690
|
+
.regex(/^[^\r\n]+$/)
|
|
691
|
+
.optional(),
|
|
692
|
+
description: z.string().trim().min(1).max(4000).optional(),
|
|
693
|
+
}),
|
|
694
|
+
}, async (input) => {
|
|
695
|
+
const result = await service.taskOpenReviewRequest(input);
|
|
696
|
+
if (!result.ok)
|
|
697
|
+
return output(result);
|
|
698
|
+
const data = result.data;
|
|
699
|
+
const opened = data.mergeRequest;
|
|
700
|
+
const said = opened
|
|
701
|
+
? `${opened.reused ? 'The branch already had an open merge request, and it is used' : 'The merge request is opened'}: ${opened.url} (${opened.state}, into ${opened.targetBranch}).`
|
|
702
|
+
: '';
|
|
703
|
+
return output({
|
|
704
|
+
ok: true,
|
|
705
|
+
data: {
|
|
706
|
+
...data,
|
|
707
|
+
nextAction: data.alreadyRecorded
|
|
708
|
+
? `This delivery already has its merge request recorded: ${data.alreadyRecorded.url} (${data.alreadyRecorded.state}). Nothing was opened. When the user says its state changed, record that with task.review_request.`
|
|
709
|
+
: data.reportRefused
|
|
710
|
+
? `${said} Recording it in Engineering Memory was refused: ${data.reportRefused} Tell the user; once the cause is fixed, record it with task.review_request.`
|
|
711
|
+
: data.report?.queued
|
|
712
|
+
? `${said} Engineering Memory is unreachable, so recording it is queued; it is recorded, and the work item moves, when Engineering Memory is reachable again.`
|
|
713
|
+
: `${said} ${data.report?.data?.reviewEvent?.nextAction ??
|
|
714
|
+
(data.report?.data?.reviewEvent
|
|
715
|
+
? 'It is recorded; the work item stays where it is.'
|
|
716
|
+
: 'It is recorded. The work item does not move yet: other work on it is still open or undelivered, or the task has no active work item.')}`,
|
|
717
|
+
},
|
|
718
|
+
});
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
function registerGitLabToken(server, service) {
|
|
722
|
+
server.registerTool('gitlab.token', {
|
|
723
|
+
description: "Give the user a page on this computer where they save their own GitLab personal access token, so task.open_review_request can open merge requests and Engineering Memory can follow them on a GitLab only this computer's network reaches. The token is typed only on that page: it never passes through the chat, this agent or Engineering Memory's server, and it is kept in this computer's credential store for the signed-in account. Never open or fill in the page yourself. The same page removes a saved token.",
|
|
724
|
+
inputSchema: z.strictObject({
|
|
725
|
+
repoRoot: z.string().min(1).optional(),
|
|
726
|
+
language: languageTag.optional(),
|
|
727
|
+
}),
|
|
728
|
+
}, async (input) => output(await service.gitlabToken(input)));
|
|
729
|
+
}
|
|
539
730
|
function askPullRequestBase(server, service, context, input, language, copy, owner) {
|
|
540
731
|
return askQuestionnaire(server, service, {
|
|
541
732
|
repoRoot: input.repoRoot,
|