engineering-memory 1.11.28 → 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 +13 -0
- package/runtime/dist/src/localization/catalogue.generated.js +80 -0
- package/runtime/dist/src/mcp/delivery-tools.js +185 -50
- package/runtime/dist/src/mcp/review-tools.js +369 -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 +4 -0
- package/runtime/dist/src/runtime/api-client.js +7 -0
- package/runtime/dist/src/runtime/bridge-service.js +273 -5
- package/runtime/dist/src/runtime/merge-request-sync.js +42 -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 +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engineering-memory",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.29",
|
|
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",
|
package/runtime/build.json
CHANGED
|
@@ -71,6 +71,15 @@ export const endpoints = {
|
|
|
71
71
|
taskDeliveryDeliver: (projectId, taskId) => `/projects/${projectId}/tasks/${taskId}/delivery/deliver`,
|
|
72
72
|
taskDeliveryCancel: (projectId, taskId) => `/projects/${projectId}/tasks/${taskId}/delivery/cancel`,
|
|
73
73
|
taskReviewRequest: (projectId, taskId) => `/projects/${projectId}/tasks/${taskId}/review-request`,
|
|
74
|
+
reviewQueue: (projectId) => `/projects/${projectId}/reviews`,
|
|
75
|
+
review: (projectId, taskId) => `/projects/${projectId}/tasks/${taskId}/review`,
|
|
76
|
+
reviewRounds: (projectId, taskId) => `/projects/${projectId}/tasks/${taskId}/review/rounds`,
|
|
77
|
+
reviewRoundResult: (projectId, roundId) => `/projects/${projectId}/review-rounds/${roundId}/result`,
|
|
78
|
+
reviewRoundRules: (projectId, roundId) => `/projects/${projectId}/review-rounds/${roundId}/rules`,
|
|
79
|
+
reviewFindingRejection: (projectId, findingId) => `/projects/${projectId}/review-findings/${findingId}/rejection`,
|
|
80
|
+
reviewPass: (projectId, taskId) => `/projects/${projectId}/tasks/${taskId}/review/pass`,
|
|
81
|
+
reviewSkip: (projectId, taskId) => `/projects/${projectId}/tasks/${taskId}/review/skip`,
|
|
82
|
+
reviewExemption: (projectId, taskId) => `/projects/${projectId}/tasks/${taskId}/review/exemption`,
|
|
74
83
|
projectSetup: '/projects/setup',
|
|
75
84
|
projectList: '/projects',
|
|
76
85
|
projectResolve: '/projects/resolve',
|
|
@@ -429,6 +429,19 @@ export class GitInspector {
|
|
|
429
429
|
const result = await this.runner.run('git', ['merge-base', '--is-ancestor', ancestor, descendant], { cwd: repoRoot });
|
|
430
430
|
return result.exitCode === 0;
|
|
431
431
|
}
|
|
432
|
+
async mergeBase(repoRoot, left, right) {
|
|
433
|
+
return await this.gitValue(repoRoot, ['merge-base', left, right]);
|
|
434
|
+
}
|
|
435
|
+
async changedFiles(repoRoot, base, head) {
|
|
436
|
+
const result = await this.runner.run('git', ['diff', '--name-status', '-z', '--no-renames', base, head, '--'], { cwd: repoRoot });
|
|
437
|
+
if (result.exitCode !== 0)
|
|
438
|
+
throw new Error(`Git diff failed: ${result.stderr.trim()}`);
|
|
439
|
+
const tokens = result.stdout.split('\0').filter(Boolean);
|
|
440
|
+
const files = [];
|
|
441
|
+
for (let index = 0; index + 1 < tokens.length; index += 2)
|
|
442
|
+
files.push({ status: tokens[index], path: normalizeGitPath(tokens[index + 1]) });
|
|
443
|
+
return files;
|
|
444
|
+
}
|
|
432
445
|
async remoteRefContaining(repoRoot, commit) {
|
|
433
446
|
return await this.gitValue(repoRoot, [
|
|
434
447
|
'for-each-ref',
|
|
@@ -35,6 +35,10 @@ export const bundledTexts = {
|
|
|
35
35
|
'delivery.commit_push_draft_pr.label': 'Commit, push and draft PR/MR',
|
|
36
36
|
'delivery.commit_push_pr.description': 'Push the branch and open a request ready for review.',
|
|
37
37
|
'delivery.commit_push_pr.label': 'Commit, push and PR/MR',
|
|
38
|
+
'delivery.commit_push_pr_unreviewed.description': 'The PR/MR opens ready for the team; no code review is recorded.',
|
|
39
|
+
'delivery.commit_push_pr_unreviewed.label': 'Commit, push and PR/MR without review',
|
|
40
|
+
'delivery.commit_push_review_pr.description': 'The PR/MR opens as a draft and becomes ready once the code review ends.',
|
|
41
|
+
'delivery.commit_push_review_pr.label': 'Commit, push and PR/MR, reviewed first',
|
|
38
42
|
'delivery.context': 'The task is verified and closed. This choice determines where the code goes; approval to publish memory does not authorize Git delivery.',
|
|
39
43
|
'delivery.defer.description': 'Keep the task closed and preserve its files without Git delivery.',
|
|
40
44
|
'delivery.defer.label': 'Keep it for now',
|
|
@@ -42,6 +46,9 @@ export const bundledTexts = {
|
|
|
42
46
|
'delivery.example': 'Commit keeps work on the local branch. PR/MR pushes that branch and opens it for team review.',
|
|
43
47
|
'delivery.message': 'How should we deliver these changes?',
|
|
44
48
|
'delivery.recorded': 'Already answered in Engineering Memory',
|
|
49
|
+
'delivery.reviewFix': 'These changes fix the review findings of {task}, so they go onto its open PR/MR branch; no new PR/MR is opened.',
|
|
50
|
+
'delivery.reviewOptional': 'Code review is optional in this project: choose a reviewed PR/MR or one without review.',
|
|
51
|
+
'delivery.reviewRequired': 'This project reviews code before a PR/MR is ready: the PR/MR opens as a draft and becomes ready after the review.',
|
|
45
52
|
'forgetWorktree.context': 'A checkout that cannot be read is still counted against the worktree limit although nothing can use it; forgetting it frees that slot. No file in the folder is touched or deleted, and the branch stays.',
|
|
46
53
|
'forgetWorktree.example': 'After this, {task} no longer appears in the worktree list and the {folder} folder stays on disk exactly as it is.',
|
|
47
54
|
'forgetWorktree.forget': 'Stop tracking it; leave the files alone',
|
|
@@ -263,6 +270,38 @@ export const bundledTexts = {
|
|
|
263
270
|
'reviewBatch.untitled': 'untitled {kind}',
|
|
264
271
|
'reviewBatch.unversioned': 'Written without a source inspection: {titles}. Tasks that read memory by source version will not see them until an inspection retains them.',
|
|
265
272
|
'reviewBatch.why': 'Why',
|
|
273
|
+
'reviewConclude.confirm.description': 'The review ends as described above.',
|
|
274
|
+
'reviewConclude.confirm.label': 'Yes, end it',
|
|
275
|
+
'reviewConclude.defer.description': 'The review stays open; nothing is recorded.',
|
|
276
|
+
'reviewConclude.defer.label': 'Not now',
|
|
277
|
+
'reviewConclude.example': 'After this, the PR/MR can be made ready for the team.',
|
|
278
|
+
'reviewConclude.exempted.context': 'Review is required here, so only an owner or maintainer can exempt a delivery, with a written reason that stays with the review.',
|
|
279
|
+
'reviewConclude.exempted.message': 'Exempt {task} from review?',
|
|
280
|
+
'reviewConclude.passed.context': 'The fix was pushed and no finding is open. Without a new round, the last round’s result stands.',
|
|
281
|
+
'reviewConclude.passed.message': 'End the review of {task} as passed?',
|
|
282
|
+
'reviewConclude.reasonTitle': 'Reason',
|
|
283
|
+
'reviewConclude.skipped.context': 'Code review is not required in this project, so it can be skipped. Engineering Memory records that it was skipped and by whom.',
|
|
284
|
+
'reviewConclude.skipped.message': 'Skip the review of {task}?',
|
|
285
|
+
'reviewPolicy.context': 'The setting applies to the whole project from now on; work already delivered keeps its review state. On GitHub, Engineering Memory records the setting but GitHub does not enforce it.',
|
|
286
|
+
'reviewPolicy.current': 'Current setting',
|
|
287
|
+
'reviewPolicy.example': 'With Required, a finished task’s PR/MR opens as a draft, an agent reviews it against the project’s rules, and it becomes ready once each finding is fixed or rejected with a reason.',
|
|
288
|
+
'reviewPolicy.keep.description': 'Nothing changes.',
|
|
289
|
+
'reviewPolicy.keep.label': 'Leave it as it is',
|
|
290
|
+
'reviewPolicy.message': 'Should code be reviewed before a PR/MR is ready?',
|
|
291
|
+
'reviewPolicy.none.description': 'Deliveries work as they do today, with no code review step.',
|
|
292
|
+
'reviewPolicy.none.label': 'No review',
|
|
293
|
+
'reviewPolicy.optional.description': 'The delivery question offers a reviewed PR/MR or one without review; a review can be skipped at any time.',
|
|
294
|
+
'reviewPolicy.optional.label': 'Optional',
|
|
295
|
+
'reviewPolicy.required.description': 'Every PR/MR is reviewed before it is ready; only an owner or maintainer can exempt one, with a written reason.',
|
|
296
|
+
'reviewPolicy.required.label': 'Required',
|
|
297
|
+
'reviewReject.context': 'A rejected finding needs a written reason. The reason stays with the review, where the team sees it.',
|
|
298
|
+
'reviewReject.example': 'For example: “This rule is for screens, and this file is a test helper.”',
|
|
299
|
+
'reviewReject.keep.description': 'The finding stays open.',
|
|
300
|
+
'reviewReject.keep.label': 'Keep the finding',
|
|
301
|
+
'reviewReject.message': 'Reject this review finding?',
|
|
302
|
+
'reviewReject.reasonTitle': 'Reason',
|
|
303
|
+
'reviewReject.reject.description': 'The finding no longer holds the review back.',
|
|
304
|
+
'reviewReject.reject.label': 'Reject with this reason',
|
|
266
305
|
'reworkPlan.approve.description': 'The work items are created now, exactly as listed. Implementing each phase still starts separately.',
|
|
267
306
|
'reworkPlan.approve.label': 'Create these work items',
|
|
268
307
|
'reworkPlan.branch': 'branch',
|
|
@@ -357,6 +396,7 @@ export const bundledTexts = {
|
|
|
357
396
|
'taskStart.base.other': 'Another branch on origin',
|
|
358
397
|
'taskStart.base.otherDetail': 'Enter it below.',
|
|
359
398
|
'taskStart.continuePlan': "Continue {branch}, the branch of this work item's confirmed plan",
|
|
399
|
+
'taskStart.continueReview': 'Continue {branch}, the branch of the PR/MR of {task} under review',
|
|
360
400
|
'taskStart.example': 'A worktree keeps this task in its own folder.',
|
|
361
401
|
'taskStart.keepContext': 'Continue on the current branch.',
|
|
362
402
|
'taskStart.location.busy': 'Task {task} is still running in another client in this folder, so the new branch cannot start here. Once it is paused there, this folder can be chosen too.',
|
|
@@ -461,6 +501,10 @@ export const bundledTexts = {
|
|
|
461
501
|
'delivery.commit_push_draft_pr.label': 'Commit, push ve taslak PR/MR',
|
|
462
502
|
'delivery.commit_push_pr.description': 'Dalı gönder ve incelemeye hazır bir istek aç.',
|
|
463
503
|
'delivery.commit_push_pr.label': 'Commit, push ve PR/MR',
|
|
504
|
+
'delivery.commit_push_pr_unreviewed.description': 'PR/MR ekibe hazır açılır; kod incelemesi kaydedilmez.',
|
|
505
|
+
'delivery.commit_push_pr_unreviewed.label': 'Commit, push ve incelemesiz PR/MR',
|
|
506
|
+
'delivery.commit_push_review_pr.description': 'PR/MR taslak açılır; kod incelemesi bitince hazıra alınır.',
|
|
507
|
+
'delivery.commit_push_review_pr.label': 'Commit, push ve PR/MR (önce incelenir)',
|
|
464
508
|
'delivery.context': 'Görev doğrulandı ve kapandı. Bu seçim kodun nereye gönderileceğini belirler; hafıza değişikliklerini yayımlama onayı bunun yerine geçmez.',
|
|
465
509
|
'delivery.defer.description': 'Görev kapalı kalır; dosyalar korunur, Git teslimi yapılmaz.',
|
|
466
510
|
'delivery.defer.label': 'Şimdilik beklet',
|
|
@@ -468,6 +512,9 @@ export const bundledTexts = {
|
|
|
468
512
|
'delivery.example': 'Commit yerel dalda kalır. PR/MR seçersen değişiklikler uzak dala gönderilir ve ekip incelemesine açılır.',
|
|
469
513
|
'delivery.message': 'Değişiklikleri nasıl teslim edelim?',
|
|
470
514
|
'delivery.recorded': 'Engineering Memory’de verilmiş cevap',
|
|
515
|
+
'delivery.reviewFix': 'Bu değişiklikler {task} incelemesindeki bulguları düzeltir; açık PR/MR dalına gider, yeni PR/MR açılmaz.',
|
|
516
|
+
'delivery.reviewOptional': 'Bu projede kod incelemesi isteğe bağlı: incelenen ya da incelemesiz bir PR/MR seçebilirsin.',
|
|
517
|
+
'delivery.reviewRequired': 'Bu projede PR/MR hazır olmadan önce kod incelenir: PR/MR taslak açılır, inceleme bitince hazıra alınır.',
|
|
471
518
|
'forgetWorktree.context': 'Okunamayan bir çalışma kopyası hiçbir işe yaramadığı hâlde worktree sınırına sayılmaya devam eder; takipten çıkarılırsa o yer boşalır. Klasördeki hiçbir dosyaya dokunulmaz, hiçbiri silinmez; dal da kalır.',
|
|
472
519
|
'forgetWorktree.example': 'Bundan sonra {task} worktree listesinde görünmez, {folder} klasörü ise diskte olduğu gibi kalır.',
|
|
473
520
|
'forgetWorktree.forget': 'Takip etmeyi bırak; dosyalara dokunma',
|
|
@@ -689,6 +736,38 @@ export const bundledTexts = {
|
|
|
689
736
|
'reviewBatch.untitled': 'başlıksız {kind}',
|
|
690
737
|
'reviewBatch.unversioned': 'Kaynak incelemesi yapılmadan yazılanlar: {titles}. Hafızayı kaynak sürümüne göre okuyan görevler, bir inceleme bunları saklayana kadar görmez.',
|
|
691
738
|
'reviewBatch.why': 'Nedeni',
|
|
739
|
+
'reviewConclude.confirm.description': 'İnceleme yukarıda anlatıldığı gibi sonuçlanır.',
|
|
740
|
+
'reviewConclude.confirm.label': 'Evet, bitir',
|
|
741
|
+
'reviewConclude.defer.description': 'İnceleme açık kalır; hiçbir şey kaydedilmez.',
|
|
742
|
+
'reviewConclude.defer.label': 'Şimdi değil',
|
|
743
|
+
'reviewConclude.example': 'Bundan sonra PR/MR ekip için hazıra alınabilir.',
|
|
744
|
+
'reviewConclude.exempted.context': 'Burada inceleme zorunlu; bu yüzden yalnızca sahip ya da bakımcı, incelemeyle birlikte kalacak yazılı bir gerekçeyle muaf tutabilir.',
|
|
745
|
+
'reviewConclude.exempted.message': '{task} incelemeden muaf tutulsun mu?',
|
|
746
|
+
'reviewConclude.passed.context': 'Düzeltme gönderildi ve açık bulgu yok. Yeni tur yapılmazsa son turun sonucu geçerli olur.',
|
|
747
|
+
'reviewConclude.passed.message': '{task} incelemesi geçti olarak bitsin mi?',
|
|
748
|
+
'reviewConclude.reasonTitle': 'Gerekçe',
|
|
749
|
+
'reviewConclude.skipped.context': 'Bu projede kod incelemesi zorunlu değil, atlanabilir. Engineering Memory atlandığını ve kimin atladığını kaydeder.',
|
|
750
|
+
'reviewConclude.skipped.message': '{task} incelemesi atlansın mı?',
|
|
751
|
+
'reviewPolicy.context': 'Ayar bundan sonra tüm projede geçerli olur; önceden teslim edilen işlerin inceleme durumu değişmez. GitHub’da ayarı Engineering Memory kaydeder, GitHub zorlamaz.',
|
|
752
|
+
'reviewPolicy.current': 'Şu anki ayar',
|
|
753
|
+
'reviewPolicy.example': 'Zorunlu seçilirse biten görevin PR/MR’ı taslak açılır, bir ajan projenin kurallarına göre inceler; her bulgu düzeltilince ya da gerekçeyle reddedilince hazıra alınır.',
|
|
754
|
+
'reviewPolicy.keep.description': 'Hiçbir şey değişmez.',
|
|
755
|
+
'reviewPolicy.keep.label': 'Olduğu gibi kalsın',
|
|
756
|
+
'reviewPolicy.message': 'PR/MR hazır olmadan önce kod incelensin mi?',
|
|
757
|
+
'reviewPolicy.none.description': 'Teslimler bugünkü gibi olur; kod incelemesi adımı yoktur.',
|
|
758
|
+
'reviewPolicy.none.label': 'İnceleme yok',
|
|
759
|
+
'reviewPolicy.optional.description': 'Teslim sorusu incelenen ya da incelemesiz PR/MR sunar; inceleme her an atlanabilir.',
|
|
760
|
+
'reviewPolicy.optional.label': 'İsteğe bağlı',
|
|
761
|
+
'reviewPolicy.required.description': 'Her PR/MR hazır olmadan önce incelenir; yalnızca sahip ya da bakımcı yazılı gerekçeyle muaf tutabilir.',
|
|
762
|
+
'reviewPolicy.required.label': 'Zorunlu',
|
|
763
|
+
'reviewReject.context': 'Reddedilen bulgu için yazılı gerekçe gerekir. Gerekçe incelemeyle birlikte kalır ve ekip görür.',
|
|
764
|
+
'reviewReject.example': 'Örneğin: “Bu kural ekranlar için; bu dosya bir test yardımcısı.”',
|
|
765
|
+
'reviewReject.keep.description': 'Bulgu açık kalır.',
|
|
766
|
+
'reviewReject.keep.label': 'Bulgu kalsın',
|
|
767
|
+
'reviewReject.message': 'Bu inceleme bulgusu reddedilsin mi?',
|
|
768
|
+
'reviewReject.reasonTitle': 'Gerekçe',
|
|
769
|
+
'reviewReject.reject.description': 'Bulgu artık incelemeyi bekletmez.',
|
|
770
|
+
'reviewReject.reject.label': 'Bu gerekçeyle reddet',
|
|
692
771
|
'reworkPlan.approve.description': 'İş kalemleri listelendiği gibi şimdi oluşturulur. Aşamaların kodlanması yine ayrı ayrı başlar.',
|
|
693
772
|
'reworkPlan.approve.label': 'Bu iş kalemlerini oluştur',
|
|
694
773
|
'reworkPlan.branch': 'dal',
|
|
@@ -783,6 +862,7 @@ export const bundledTexts = {
|
|
|
783
862
|
'taskStart.base.other': 'origin üzerinde başka dal',
|
|
784
863
|
'taskStart.base.otherDetail': 'Alttaki alana yaz.',
|
|
785
864
|
'taskStart.continuePlan': '{branch} dalında devam: bu iş kaleminin onaylı planındaki dal',
|
|
865
|
+
'taskStart.continueReview': '{branch} dalında devam: {task} için incelenen PR/MR’ın dalı',
|
|
786
866
|
'taskStart.example': 'Ayrı worktree, işi ayrı klasörde tutar.',
|
|
787
867
|
'taskStart.keepContext': 'Mevcut dalda devam edilecek.',
|
|
788
868
|
'taskStart.location.busy': '{task} bu klasörde başka bir oturumda hâlâ çalışıyor; yeni branch burada açılamaz. O oturumda duraklatılırsa bu klasör de seçilebilir.',
|
|
@@ -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({
|