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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engineering-memory",
3
- "version": "1.11.28",
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",
@@ -1,3 +1,3 @@
1
1
  {
2
- "gitHead": "7ca14a854db07f8fda3823f303e8a7a6accbebff"
2
+ "gitHead": "0bcad22013da985e82e5ac61c6fcf0f3bd598fd2"
3
3
  }
@@ -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
- ? ['commit', 'commit_push', 'commit_push_draft_pr', 'commit_push_pr', 'defer']
228
+ ? [...offered(policy, Boolean(fix)), 'defer']
167
229
  : ['commit', 'defer'];
168
230
  const instruction = input.deliveryInstruction;
169
- const recorded = data.deliveryRecord?.choice;
170
- if (instruction && !ids.some((id) => id === instruction.choice))
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: 'This delivery needs an available remote and push URL. No Git action was performed.',
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
- (recorded && copy.recorded
196
- ? '\n' + copy.recorded + ': ' + copy[recorded].label
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 = instruction?.choice ?? (form ? answerChoice(form) : null);
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 === 'commit_push_draft_pr' || choice === 'commit_push_pr'
231
- ? instruction?.baseBranch
232
- : undefined;
316
+ let baseBranch = pullRequest(choice) ? instruction?.baseBranch : undefined;
233
317
  let baseQuestionnaireId;
234
- if ((choice === 'commit_push_draft_pr' || choice === 'commit_push_pr') && !baseBranch) {
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 === 'commit_push_draft_pr' || choice === 'commit_push_pr') {
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 other = refused?.details?.delivery;
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: '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.',
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 wording = language
489
+ const recordCopy = language
379
490
  ? recordWording.safeParse(texts('taskDelivery', language)).data
380
491
  : undefined;
381
- if (!language || !copy || !wording)
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 binding = { taskId: record.taskId, lockVersion: record.lockVersion };
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
- 'commit',
428
- 'commit_push',
429
- 'commit_push_draft_pr',
430
- 'commit_push_pr',
431
- 'abandon',
432
- 'defer',
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(wording.message, language, { task: record.externalTaskId }),
441
- context: wording.context + (record.branch ? '\n' + copy.destination + ': ' + record.branch : ''),
442
- example: wording.example,
443
- options: ids.map((id) => ({
444
- id,
445
- label: id === 'abandon' ? wording.abandon.label : copy[id].label,
446
- description: id === 'abandon' ? wording.abandon.description : copy[id].description,
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 === 'commit_push_draft_pr' || choice === 'commit_push_pr') {
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({