engineering-memory 1.11.28 → 1.11.30
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 +12 -0
- package/runtime/dist/src/git/git-inspector.js +13 -0
- package/runtime/dist/src/localization/catalogue.generated.js +98 -0
- package/runtime/dist/src/mcp/delivery-tools.js +209 -55
- package/runtime/dist/src/mcp/review-tools.js +372 -0
- package/runtime/dist/src/mcp/tool-annotations.js +9 -0
- package/runtime/dist/src/mcp/tool-definitions.js +16 -0
- package/runtime/dist/src/mcp/worktree-tools.js +12 -1
- package/runtime/dist/src/providers/gitlab.js +12 -0
- package/runtime/dist/src/runtime/api-client.js +12 -0
- package/runtime/dist/src/runtime/bridge-service.js +381 -12
- package/runtime/dist/src/runtime/create-bridge-service.js +1 -1
- package/runtime/dist/src/runtime/live-signals.js +3 -0
- package/runtime/dist/src/runtime/merge-request-sync.js +171 -14
- package/runtime/dist/src/runtime/review-comment.js +135 -0
- 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 -0
|
@@ -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,7 +81,48 @@ 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);
|
|
@@ -79,13 +138,7 @@ export function registerDeliveryTools(server, service) {
|
|
|
79
138
|
decisionAttempt: z.number().int().min(1).max(10000).optional(),
|
|
80
139
|
deliveryInstruction: z
|
|
81
140
|
.strictObject({
|
|
82
|
-
choice: z.enum(
|
|
83
|
-
'commit',
|
|
84
|
-
'commit_push',
|
|
85
|
-
'commit_push_draft_pr',
|
|
86
|
-
'commit_push_pr',
|
|
87
|
-
'defer',
|
|
88
|
-
]),
|
|
141
|
+
choice: z.enum(deliveryChoices),
|
|
89
142
|
userRequestExcerpt: z.string().trim().min(1).max(1200),
|
|
90
143
|
baseBranch: z.string().trim().min(1).max(240).optional(),
|
|
91
144
|
})
|
|
@@ -123,6 +176,8 @@ export function registerDeliveryTools(server, service) {
|
|
|
123
176
|
const data = parsed.data;
|
|
124
177
|
if (data.deliveryContext.mode === 'read_only')
|
|
125
178
|
return output(closed);
|
|
179
|
+
const policy = data.reviewPolicy ?? 'none';
|
|
180
|
+
const fix = data.reviewFixOf ?? null;
|
|
126
181
|
const delivered = data.delivery.alreadyDelivered;
|
|
127
182
|
if (delivered)
|
|
128
183
|
return output({
|
|
@@ -130,7 +185,12 @@ export function registerDeliveryTools(server, service) {
|
|
|
130
185
|
data: {
|
|
131
186
|
...closed.data,
|
|
132
187
|
deliveryStatus: 'delivered',
|
|
133
|
-
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
|
+
: ''),
|
|
134
194
|
},
|
|
135
195
|
});
|
|
136
196
|
const destination = data.delivery.destination;
|
|
@@ -152,6 +212,8 @@ export function registerDeliveryTools(server, service) {
|
|
|
152
212
|
language,
|
|
153
213
|
copy,
|
|
154
214
|
decisionAttempt: input.decisionAttempt ?? 0,
|
|
215
|
+
reviewPolicy: policy,
|
|
216
|
+
reviewFixOf: fix?.taskId ?? null,
|
|
155
217
|
};
|
|
156
218
|
const binding = { deliveryDigest: sha256(stableStringify(identity)) };
|
|
157
219
|
const owner = {
|
|
@@ -163,20 +225,47 @@ export function registerDeliveryTools(server, service) {
|
|
|
163
225
|
const questionnaireId = 'task-delivery-' + binding.deliveryDigest;
|
|
164
226
|
const canPush = Boolean(destination.remote && destination.pushUrl);
|
|
165
227
|
const ids = canPush
|
|
166
|
-
? [
|
|
228
|
+
? [...offered(policy, Boolean(fix)), 'defer']
|
|
167
229
|
: ['commit', 'defer'];
|
|
168
230
|
const instruction = input.deliveryInstruction;
|
|
169
|
-
const
|
|
170
|
-
|
|
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))
|
|
171
239
|
return output({
|
|
172
240
|
ok: false,
|
|
173
241
|
error: {
|
|
174
242
|
kind: 'delivery',
|
|
175
|
-
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.',
|
|
176
248
|
recovery: 'task.close',
|
|
177
249
|
retryable: false,
|
|
178
250
|
},
|
|
179
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;
|
|
180
269
|
const form = instruction
|
|
181
270
|
? null
|
|
182
271
|
: await askQuestionnaire(server, service, {
|
|
@@ -192,18 +281,15 @@ export function registerDeliveryTools(server, service) {
|
|
|
192
281
|
': ' +
|
|
193
282
|
destination.branch +
|
|
194
283
|
(canPush ? ' → ' + destination.remote + ' (' + destination.pushUrl + ')' : '') +
|
|
195
|
-
(
|
|
196
|
-
? '\n' + copy.recorded + ': ' +
|
|
197
|
-
: '')
|
|
284
|
+
(recordedLabel && copy.recorded
|
|
285
|
+
? '\n' + copy.recorded + ': ' + recordedLabel
|
|
286
|
+
: '') +
|
|
287
|
+
(line ? '\n' + line : ''),
|
|
198
288
|
example: copy.example,
|
|
199
|
-
options: ids.map((id) => ({
|
|
200
|
-
id,
|
|
201
|
-
label: copy[id].label,
|
|
202
|
-
description: copy[id].description,
|
|
203
|
-
})),
|
|
289
|
+
options: ids.map((id) => ({ id, ...wording(copy, policy, id) })),
|
|
204
290
|
binding,
|
|
205
291
|
}, context, [], owner);
|
|
206
|
-
const choice =
|
|
292
|
+
const choice = instructed ?? (form ? answerChoice(form) : null);
|
|
207
293
|
if (!choice)
|
|
208
294
|
return form;
|
|
209
295
|
const { deliveryInstruction: _instruction, ...formInput } = input;
|
|
@@ -227,11 +313,9 @@ export function registerDeliveryTools(server, service) {
|
|
|
227
313
|
return deferred();
|
|
228
314
|
service.live.deliveryStarted();
|
|
229
315
|
try {
|
|
230
|
-
let baseBranch = choice
|
|
231
|
-
? instruction?.baseBranch
|
|
232
|
-
: undefined;
|
|
316
|
+
let baseBranch = pullRequest(choice) ? instruction?.baseBranch : undefined;
|
|
233
317
|
let baseQuestionnaireId;
|
|
234
|
-
if ((choice
|
|
318
|
+
if (pullRequest(choice) && !baseBranch) {
|
|
235
319
|
baseQuestionnaireId = 'task-delivery-base-' + sha256(questionnaireId + choice);
|
|
236
320
|
const base = await askPullRequestBase(server, service, context, { ...input, questionnaireId: baseQuestionnaireId, binding: { ...binding, choice } }, language, copy, owner);
|
|
237
321
|
if (!answerChoice(base))
|
|
@@ -242,7 +326,7 @@ export function registerDeliveryTools(server, service) {
|
|
|
242
326
|
if (!baseBranch)
|
|
243
327
|
return base;
|
|
244
328
|
}
|
|
245
|
-
if (choice
|
|
329
|
+
if (pullRequest(choice)) {
|
|
246
330
|
const valid = await service.taskDeliveryBase({
|
|
247
331
|
repoRoot: input.repoRoot,
|
|
248
332
|
branch: baseBranch,
|
|
@@ -299,6 +383,9 @@ export function registerDeliveryTools(server, service) {
|
|
|
299
383
|
retryable: false,
|
|
300
384
|
},
|
|
301
385
|
});
|
|
386
|
+
const now = current.data.reviewPolicy ?? 'none';
|
|
387
|
+
if (pullRequest(choice) && now !== policy)
|
|
388
|
+
return settingChanged('task.close', now);
|
|
302
389
|
const question = instruction
|
|
303
390
|
? null
|
|
304
391
|
: await service.questionnaireResume({ repoRoot: input.repoRoot, questionnaireId });
|
|
@@ -313,14 +400,21 @@ export function registerDeliveryTools(server, service) {
|
|
|
313
400
|
projectId: record.projectId,
|
|
314
401
|
taskId: record.taskId,
|
|
315
402
|
expectedVersion: record.lockVersion,
|
|
316
|
-
choice,
|
|
403
|
+
choice: choice === 'commit_push_review_pr' ? 'commit_push_draft_pr' : choice,
|
|
317
404
|
...(baseBranch ? { baseBranch } : {}),
|
|
405
|
+
...(pullRequest(choice) && data.reviewPolicy
|
|
406
|
+
? { expectedReviewPolicy: data.reviewPolicy }
|
|
407
|
+
: {}),
|
|
318
408
|
answerSource: question ? answerSourceName(question.answerSource) : 'user_instruction',
|
|
319
409
|
})
|
|
320
410
|
: null;
|
|
321
411
|
const answered = answer?.ok ? answer.data : null;
|
|
322
412
|
const refused = answer && !answer.ok ? answer.error : null;
|
|
323
|
-
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;
|
|
324
418
|
return output({
|
|
325
419
|
ok: true,
|
|
326
420
|
data: {
|
|
@@ -352,7 +446,24 @@ export function registerDeliveryTools(server, service) {
|
|
|
352
446
|
...(baseQuestionnaireId ? { baseQuestionnaireId } : {}),
|
|
353
447
|
destination,
|
|
354
448
|
},
|
|
355
|
-
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(' '),
|
|
356
467
|
},
|
|
357
468
|
});
|
|
358
469
|
}
|
|
@@ -375,10 +486,10 @@ function registerTaskDelivery(server, service) {
|
|
|
375
486
|
}, async (input, context) => {
|
|
376
487
|
const language = await service.language(input.language);
|
|
377
488
|
const copy = language ? copySchema.safeParse(texts('delivery', language)).data : undefined;
|
|
378
|
-
const
|
|
489
|
+
const recordCopy = language
|
|
379
490
|
? recordWording.safeParse(texts('taskDelivery', language)).data
|
|
380
491
|
: undefined;
|
|
381
|
-
if (!language || !copy || !
|
|
492
|
+
if (!language || !copy || !recordCopy)
|
|
382
493
|
return output({
|
|
383
494
|
ok: false,
|
|
384
495
|
error: {
|
|
@@ -410,7 +521,14 @@ function registerTaskDelivery(server, service) {
|
|
|
410
521
|
nextAction: 'This delivery is already concluded. Tell the user what was recorded.',
|
|
411
522
|
},
|
|
412
523
|
});
|
|
413
|
-
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
|
+
};
|
|
414
532
|
const questionnaireId = 'task-delivery-record-' +
|
|
415
533
|
sha256(stableStringify({ ...binding, decisionAttempt: input.decisionAttempt ?? 0 }));
|
|
416
534
|
const owner = {
|
|
@@ -423,28 +541,38 @@ function registerTaskDelivery(server, service) {
|
|
|
423
541
|
language: input.language,
|
|
424
542
|
},
|
|
425
543
|
};
|
|
426
|
-
const ids =
|
|
427
|
-
|
|
428
|
-
'
|
|
429
|
-
'
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
+
});
|
|
434
560
|
const form = await askQuestionnaire(server, service, {
|
|
435
561
|
repoRoot: input.repoRoot,
|
|
436
562
|
presentation: input.presentation,
|
|
437
563
|
questionnaireId,
|
|
438
564
|
language,
|
|
439
565
|
impact: 'critical',
|
|
440
|
-
message: format(
|
|
441
|
-
context:
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
+
],
|
|
448
576
|
binding,
|
|
449
577
|
}, context, [], owner);
|
|
450
578
|
const choice = answerChoice(form);
|
|
@@ -466,7 +594,7 @@ function registerTaskDelivery(server, service) {
|
|
|
466
594
|
reason: 'abandoned',
|
|
467
595
|
}));
|
|
468
596
|
let baseBranch;
|
|
469
|
-
if (choice
|
|
597
|
+
if (pullRequest(choice)) {
|
|
470
598
|
const base = await askPullRequestBase(server, service, context, {
|
|
471
599
|
...input,
|
|
472
600
|
questionnaireId: 'task-delivery-base-' + sha256(questionnaireId + choice),
|
|
@@ -495,10 +623,17 @@ function registerTaskDelivery(server, service) {
|
|
|
495
623
|
projectId: input.projectId,
|
|
496
624
|
taskId: input.taskId,
|
|
497
625
|
expectedVersion: record.lockVersion,
|
|
498
|
-
choice,
|
|
626
|
+
choice: choice === 'commit_push_review_pr' ? 'commit_push_draft_pr' : choice,
|
|
499
627
|
...(baseBranch ? { baseBranch } : {}),
|
|
628
|
+
...(pullRequest(choice) && record.reviewPolicy
|
|
629
|
+
? { expectedReviewPolicy: record.reviewPolicy }
|
|
630
|
+
: {}),
|
|
500
631
|
answerSource: answerSourceName(question.answerSource),
|
|
501
632
|
});
|
|
633
|
+
const changed = answer.error?.details
|
|
634
|
+
?.reviewPolicy;
|
|
635
|
+
if (changed)
|
|
636
|
+
return settingChanged('task.delivery', changed);
|
|
502
637
|
if (!answer.ok)
|
|
503
638
|
return output(answer);
|
|
504
639
|
return output({
|
|
@@ -512,28 +647,47 @@ function registerTaskDelivery(server, service) {
|
|
|
512
647
|
}
|
|
513
648
|
function registerTaskReviewRequest(server, service) {
|
|
514
649
|
server.registerTool('task.review_request', {
|
|
515
|
-
description: "Record the pull request (GitHub) or merge request (GitLab) a closed task's changes went into, and its state: draft, ready, merged or closed. Call it with the link copied from the provider as soon as the pull request is opened, and again when the user says it became ready, was merged or was closed. Engineering Memory then moves the task's work item as the project's workflow rules say, for example to PR when every pull request of the work item is ready and to READY FOR QA when they are all merged. It works from any chat, also for a delivery session.entry lists. What is recorded is what was reported: Engineering Memory does not check it with the provider, and performs no Git or provider action.",
|
|
650
|
+
description: "Record the pull request (GitHub) or merge request (GitLab) a closed task's changes went into, and its state: draft, ready, merged or closed. Call it with the link copied from the provider as soon as the pull request is opened, and again when the user says it became ready, was merged or was closed. Pass headCommit and targetBranch when you can read them from the provider (on GitHub, gh pr view with --json headRefOid,baseRefName): a code review that had passed waits again when the pull request gets a new commit or a new target branch. Engineering Memory then moves the task's work item as the project's workflow rules say, for example to PR when every pull request of the work item is ready and to READY FOR QA when they are all merged. It works from any chat, also for a delivery session.entry lists. What is recorded is what was reported: Engineering Memory does not check it with the provider, and performs no Git or provider action.",
|
|
516
651
|
inputSchema: z.strictObject({
|
|
517
652
|
projectId: z.string().uuid(),
|
|
518
653
|
taskId: z.string().uuid(),
|
|
519
654
|
url: z.string().trim().min(1).max(500),
|
|
520
655
|
state: z.enum(['draft', 'ready', 'merged', 'closed']),
|
|
656
|
+
headCommit: z
|
|
657
|
+
.string()
|
|
658
|
+
.regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/)
|
|
659
|
+
.optional(),
|
|
660
|
+
targetBranch: z.string().trim().min(1).max(255).optional(),
|
|
521
661
|
}),
|
|
522
662
|
}, async (input) => {
|
|
523
663
|
const result = await service.taskReviewRequest(input);
|
|
524
664
|
if (!result.ok)
|
|
525
665
|
return output(result);
|
|
526
666
|
const recorded = result.data;
|
|
667
|
+
const reopened = recorded.data?.reviewReopened;
|
|
668
|
+
const job = recorded.data?.providerJob;
|
|
527
669
|
return output({
|
|
528
670
|
ok: true,
|
|
529
671
|
data: {
|
|
530
672
|
...result.data,
|
|
531
673
|
nextAction: recorded.queued
|
|
532
674
|
? 'Engineering Memory is unreachable, so this report is queued. It is recorded, and the work item moves as the rules say, when Engineering Memory is reachable again.'
|
|
533
|
-
:
|
|
534
|
-
|
|
535
|
-
?
|
|
536
|
-
: '
|
|
675
|
+
: [
|
|
676
|
+
reopened
|
|
677
|
+
? `The code review of this delivery had passed, but the pull request has ${reopened === 'new_target' ? 'a new target branch' : 'a new commit'} now, so the review waits again. Tell the user in one line; review.start runs the next round.`
|
|
678
|
+
: '',
|
|
679
|
+
job === 'make_draft'
|
|
680
|
+
? 'This project requires code review, so turn the pull request back into a draft with your own tools (on GitHub, gh pr ready --undo) and record it with state draft.'
|
|
681
|
+
: job === 'make_ready'
|
|
682
|
+
? 'Its code review has already ended: review.make_ready makes the pull request ready.'
|
|
683
|
+
: '',
|
|
684
|
+
recorded.data?.reviewEvent?.nextAction ??
|
|
685
|
+
(recorded.data?.reviewEvent
|
|
686
|
+
? 'The pull request is recorded; the work item stays where it is.'
|
|
687
|
+
: 'The pull request 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.'),
|
|
688
|
+
]
|
|
689
|
+
.filter(Boolean)
|
|
690
|
+
.join(' '),
|
|
537
691
|
},
|
|
538
692
|
});
|
|
539
693
|
});
|