engineering-memory 1.11.10 → 1.11.12

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.
@@ -1,9 +1,11 @@
1
1
  import { basename } from 'node:path';
2
2
  import * as z from 'zod/v4';
3
- import { cancelledDeliveryQuestionId, } from '../runtime/bridge-service.js';
3
+ import { cancelledDeliveryQuestionId, forgetUnreadableQuestionId, } from '../runtime/bridge-service.js';
4
4
  import { configuredBase, sourceLabel, taskStartChoice, taskStartDefinition, } from '../runtime/task-start.js';
5
5
  import { sha256, stableStringify } from '../utilities/hash.js';
6
+ import { unreadableWorktree } from '../runtime/worktree-pool.js';
6
7
  import { askQuestionnaire } from './questionnaire-tools.js';
8
+ import { languageTag } from '../runtime/questionnaire-store.js';
7
9
  const repo = z.string().min(1);
8
10
  const branch = z.string().min(1).max(255);
9
11
  const remote = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/);
@@ -48,6 +50,170 @@ const policy = {
48
50
  heartbeatSeconds: z.number().int().min(5).max(300),
49
51
  inactivitySeconds: z.number().int().min(60).max(86400),
50
52
  };
53
+ const preferenceCopy = {
54
+ en: {
55
+ message: 'Save the shared branch preferences of this project? Nothing is saved until you answer.',
56
+ context: 'These branches belong to the whole project: every member who starts a task here is offered them as the branch the task starts from. Only a project or organization administrator can change them. No branch is created, moved or deleted; only the preference is recorded.',
57
+ example: 'Once the development role points at origin/develop, the start form of the next task offers origin/develop as its first choice.',
58
+ roles: { development: 'development', production: 'production', test: 'test' },
59
+ ask: (role, source) => source
60
+ ? `Save ${source} as the shared ${role} branch?`
61
+ : `Record that this project has no ${role} branch?`,
62
+ save: 'Save this choice',
63
+ saved: 'This branch is recorded as the project default now, for every member.',
64
+ leave: 'Leave unchanged',
65
+ unchanged: 'Nothing changes. This role stays as it is and can be answered another time.',
66
+ },
67
+ tr: {
68
+ message: 'Projenin ortak dal tercihleri kaydedilsin mi? Sen cevaplayana kadar hiçbir şey kaydedilmez.',
69
+ context: 'Bu dallar projenin tamamı için geçerli: burada görev başlatan herkese, görevin başlayacağı dal olarak bunlar önerilir. Yalnızca proje ya da organizasyon yöneticisi değiştirebilir. Hiçbir dal oluşturulmaz, taşınmaz veya silinmez; yalnızca tercih kaydedilir.',
70
+ example: 'Geliştirme dalı origin/develop olarak kaydedilirse, bir sonraki görevin başlangıç formunda ilk seçenek origin/develop olur.',
71
+ roles: { development: 'Geliştirme', production: 'Canlı', test: 'Test' },
72
+ ask: (role, source) => source
73
+ ? `${role} dalı ${source} olarak kaydedilsin mi?`
74
+ : `Bu projede ${role.toLocaleLowerCase('tr')} dalı olmadığı kaydedilsin mi?`,
75
+ save: 'Bu tercihi kaydet',
76
+ saved: 'Bu dal şimdi proje varsayılanı olarak kaydedilir; ekipteki herkes onu görür.',
77
+ leave: 'Şimdilik değiştirme',
78
+ unchanged: 'Hiçbir şey değişmez. Bu rol olduğu gibi kalır, sonra yeniden sorulabilir.',
79
+ },
80
+ };
81
+ const fallbackCopy = {
82
+ en: {
83
+ message: (remote, local, inPlace) => `The remote ${remote} could not be fetched. Should the task start from local ${local} ${inPlace ? 'in this folder' : 'in a separate worktree'}?`,
84
+ context: 'The branch you chose cannot be reached, so its exact commit is unavailable. The local copy is whatever was fetched last: commits pushed to the remote since then are not in it. Nothing is created until you answer.',
85
+ example: (remote, local) => `Continuing creates the new branch at ${local}; ${remote} can be merged into it once the network is back.`,
86
+ approve: 'Continue',
87
+ approved: (local) => `The new branch is created at ${local} now and the task starts.`,
88
+ defer: 'Not now',
89
+ deferred: 'Nothing is created or reserved. The task waits, and it can be started again once the remote answers.',
90
+ },
91
+ tr: {
92
+ message: (remote, local, inPlace) => `${remote} uzak dalına ulaşılamadı. Görev, yereldeki ${local} üzerinden ${inPlace ? 'bu klasörde' : 'ayrı bir worktree’de'} başlasın mı?`,
93
+ context: 'Seçtiğin dala ulaşılamadığı için tam commit’i alınamıyor. Yerel kopya, en son çekilen hâli gösterir: o günden sonra uzak dala giden commit’ler burada yok. Sen cevaplayana kadar hiçbir şey oluşturulmaz.',
94
+ example: (remote, local) => `Devam edersen yeni dal ${local} üzerinden açılır; ağ düzeldiğinde ${remote} dalını bu dala birleştirebilirsin.`,
95
+ approve: 'Devam et',
96
+ approved: (local) => `Yeni dal şimdi ${local} üzerinden açılır ve görev başlar.`,
97
+ defer: 'Şimdilik başlatma',
98
+ deferred: 'Hiçbir şey oluşturulmaz, hiçbir yer ayrılmaz. Görev bekler; uzak dala ulaşıldığında yeniden başlatılabilir.',
99
+ },
100
+ };
101
+ const forgetCopy = {
102
+ en: {
103
+ message: (task, folder) => `The ${folder} folder of task ${task} can no longer be read as a Git checkout. Should Engineering Memory stop tracking it?`,
104
+ context: (pendingDelivery) => '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.' +
105
+ (pendingDelivery
106
+ ? ' That task closed without its delivery being settled, so any undelivered work exists only in that folder.'
107
+ : ''),
108
+ example: (task, folder) => `After this, ${task} no longer appears in the worktree list and the ${folder} folder stays on disk exactly as it is.`,
109
+ forget: 'Stop tracking it; leave the files alone',
110
+ forgotten: 'The record is removed and the slot is free again. The folder, its files and its branch stay untouched.',
111
+ keep: 'Keep tracking it',
112
+ kept: 'Nothing changes. The record stays and keeps counting against the limit.',
113
+ },
114
+ tr: {
115
+ message: (task, folder) => `${task} görevinin ${folder} klasörü artık bir Git çalışma kopyası olarak okunamıyor. Engineering Memory bu klasörü takip etmeyi bıraksın mı?`,
116
+ context: (pendingDelivery) => '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.' +
117
+ (pendingDelivery
118
+ ? ' Bu görev teslimi sonuçlanmadan kapandı; teslim edilmemiş iş varsa yalnızca bu klasörde duruyor.'
119
+ : ''),
120
+ example: (task, folder) => `Bundan sonra ${task} worktree listesinde görünmez, ${folder} klasörü ise diskte olduğu gibi kalır.`,
121
+ forget: 'Takip etmeyi bırak; dosyalara dokunma',
122
+ forgotten: 'Kayıt silinir ve sınırdaki yer boşalır. Klasör, içindeki dosyalar ve dal olduğu gibi kalır.',
123
+ keep: 'Takip etmeye devam et',
124
+ kept: 'Hiçbir şey değişmez. Kayıt durur ve sınıra sayılmaya devam eder.',
125
+ },
126
+ };
127
+ const retireCopy = {
128
+ en: {
129
+ missing: (folder) => `The ${folder} folder is gone but its worktree record is still here. Remove the record?`,
130
+ message: (folder, branch) => `${folder} is a worktree left in the old location (branch ${branch}). Remove the folder?`,
131
+ context: (missing) => 'Folders in the old location are no longer part of the pool: they are never reused and do not count toward the limit.' +
132
+ (missing
133
+ ? ' Only the record is left, so there is no folder to remove and no file is read or deleted.'
134
+ : ' The branch and its commits stay. Git removes only a clean folder, and the files it ignores go with it.'),
135
+ example: (folder, branch, missing) => missing
136
+ ? `After this, ${folder} is gone from the worktree list and nothing on disk changes.`
137
+ : `After this, ${folder} is gone from the worktree list and branch ${branch} still holds every commit made on it.`,
138
+ retire: 'Remove the folder',
139
+ retired: (missing) => missing
140
+ ? 'The stale record is removed. Nothing on disk changes.'
141
+ : 'Git removes the folder and Engineering Memory drops its record. The branch and its commits stay.',
142
+ keep: 'Keep it',
143
+ kept: 'Nothing changes. The folder and its record stay exactly as they are.',
144
+ },
145
+ tr: {
146
+ missing: (folder) => `${folder} klasörü artık yok ama worktree kaydı duruyor. Kayıt silinsin mi?`,
147
+ message: (folder, branch) => `${folder}, eski konumda kalmış bir worktree (dal ${branch}). Klasör kaldırılsın mı?`,
148
+ context: (missing) => 'Eski konumdaki klasörler artık havuzun parçası değil: yeniden kullanılmıyorlar ve sınıra da sayılmıyorlar.' +
149
+ (missing
150
+ ? ' Geriye yalnızca kayıt kaldı; kaldırılacak bir klasör yok, hiçbir dosya okunmaz veya silinmez.'
151
+ : ' Dal ve üzerindeki commit’ler kalır. Git yalnızca temiz bir klasörü kaldırır; kaldırırken .gitignore kapsamındaki dosyalar da gider.'),
152
+ example: (folder, branch, missing) => missing
153
+ ? `Bundan sonra ${folder} worktree listesinde görünmez; diskte hiçbir şey değişmez.`
154
+ : `Bundan sonra ${folder} worktree listesinde görünmez; ${branch} dalındaki commit’ler ise yerinde durur.`,
155
+ retire: 'Klasörü kaldır',
156
+ retired: (missing) => missing
157
+ ? 'Boşta kalan kayıt silinir. Diskte hiçbir şey değişmez.'
158
+ : 'Git klasörü kaldırır, Engineering Memory de kaydını siler. Dal ve commit’ler kalır.',
159
+ keep: 'Kalsın',
160
+ kept: 'Hiçbir şey değişmez. Klasör de kaydı da olduğu gibi kalır.',
161
+ },
162
+ };
163
+ const unownedCopy = {
164
+ en: {
165
+ message: (folder) => `Nobody is recorded as using the ${folder} folder. If the agent that worked there has stopped and its delivery is finished, should the clean checkout become reusable?`,
166
+ context: 'This checkout was found without an ownership record, so it is protected and handed to no task. Releasing it runs the safety checks again first: a folder with uncommitted work is refused. No file and no branch is ever deleted.',
167
+ example: (commit) => `Once it is released, the next task can start in that folder, and the work committed at ${commit} stays on its branch.`,
168
+ release: 'Prior work finished; release after safety checks',
169
+ released: 'The safety checks run and, if they pass, the folder returns to the pool.',
170
+ preserve: 'Keep protected',
171
+ preserved: 'Nothing changes. The folder stays protected and is given to no task.',
172
+ },
173
+ tr: {
174
+ message: (folder) => `${folder} klasörünü kimin kullandığı kayıtlı değil. Burada çalışan ajan durduysa ve teslimi bittiyse, temiz klasör yeniden kullanılabilir hâle gelsin mi?`,
175
+ context: 'Bu çalışma kopyası sahiplik kaydı olmadan bulundu; bu yüzden korunuyor ve hiçbir göreve verilmiyor. Serbest bırakmadan önce güvenlik kontrolleri yeniden çalışır: kaydedilmemiş iş varsa işlem yapılmaz. Hiçbir dosya ve hiçbir dal silinmez.',
176
+ example: (commit) => `Serbest bırakılırsa sıradaki görev bu klasörde başlayabilir; ${commit} commit’ine kaydedilmiş iş dalında durmaya devam eder.`,
177
+ release: 'Önceki iş bitti; güvenlik kontrollerinden sonra kullanılabilir yap',
178
+ released: 'Güvenlik kontrolleri çalışır; geçerse klasör havuza döner.',
179
+ preserve: 'Korumaya devam et',
180
+ preserved: 'Hiçbir şey değişmez. Klasör korunur ve hiçbir göreve verilmez.',
181
+ },
182
+ };
183
+ const cancelCopy = {
184
+ en: {
185
+ message: (task) => `${task} will not be delivered. What should happen to this folder?`,
186
+ context: (clean, managed) => 'A cancelled delivery leaves the folder reserved for this task until you say what happens to it, and a reserved folder keeps counting against the worktree limit. The branch and every commit on it stay either way.' +
187
+ (clean
188
+ ? ''
189
+ : managed
190
+ ? ' The folder has uncommitted changes; saving writes them to a Git ref they can be restored from.'
191
+ : ' The folder has uncommitted changes; this is your own folder, so its files are never touched when it is freed.'),
192
+ example: 'Freeing the folder lets the next task start in it; keeping it leaves it reserved.',
193
+ release: 'Free the folder; the branch stays',
194
+ released: 'The folder returns to the pool. Its files are left alone and the branch stays.',
195
+ save: 'Save the changes and free the folder',
196
+ saved: 'The uncommitted changes are written to a Git ref first, then the folder is freed. They can be restored from that ref.',
197
+ keep: 'Keep the folder as it is',
198
+ kept: 'Nothing changes. The folder stays reserved for this task.',
199
+ },
200
+ tr: {
201
+ message: (task) => `${task} teslim edilmeyecek. Bu klasör ne olsun?`,
202
+ context: (clean, managed) => 'Teslimi iptal edilen bir klasör, sen ne olacağını söyleyene kadar bu göreve ayrılmış kalır ve ayrılmış klasör worktree sınırına sayılmaya devam eder. Dal ve üzerindeki commit’ler her hâlükârda durur.' +
203
+ (clean
204
+ ? ''
205
+ : managed
206
+ ? ' Klasörde kaydedilmemiş değişiklikler var; kaydedilirse bir Git ref’ine yazılır ve oradan geri alınabilir.'
207
+ : ' Klasörde kaydedilmemiş değişiklikler var; burası senin kendi klasörün olduğu için boşaltılsa da dosyalara dokunulmaz.'),
208
+ example: 'Klasör boşaltılırsa sıradaki görev burada başlayabilir; kalsın dersen bu göreve ayrılmış hâlde durur.',
209
+ release: 'Klasörü boşalt; branch kalsın',
210
+ released: 'Klasör havuza döner. Dosyalarına dokunulmaz, dal kalır.',
211
+ save: 'Değişiklikleri kaydedip klasörü boşalt',
212
+ saved: 'Kaydedilmemiş değişiklikler önce bir Git ref’ine yazılır, sonra klasör boşaltılır. Değişiklikler o ref’ten geri alınabilir.',
213
+ keep: 'Klasör olduğu gibi kalsın',
214
+ kept: 'Hiçbir şey değişmez. Klasör bu göreve ayrılmış hâlde kalır.',
215
+ },
216
+ };
51
217
  const baseSchema = z.discriminatedUnion('kind', [
52
218
  z.strictObject({
53
219
  kind: z.literal('current'),
@@ -78,10 +244,11 @@ export function registerWorktreeTools(server, service) {
78
244
  development: preference.optional(),
79
245
  production: preference.optional(),
80
246
  test: preference.optional(),
81
- language: z.enum(['tr', 'en']).default('en'),
247
+ language: languageTag.optional(),
82
248
  presentation,
83
249
  }),
84
250
  }, async (input, context) => {
251
+ const language = (await service.language(input.language))?.startsWith('tr') ? 'tr' : 'en';
85
252
  const current = await service.projectGitPreferences(input);
86
253
  if (!current.ok)
87
254
  return output(current);
@@ -99,18 +266,9 @@ export function registerWorktreeTools(server, service) {
99
266
  const roles = ['development', 'production', 'test'].filter((role) => Boolean(input[role]));
100
267
  if (roles.length === 0)
101
268
  return output({ ok: true, data: { unchanged: true } });
102
- const roleLabel = (role) => {
269
+ const source = (role, none) => {
103
270
  const value = input[role];
104
- return input.language === 'tr'
105
- ? role +
106
- ' dal tercihi: ' +
107
- (value.branch ? value.remote + '/' + value.branch : 'henüz yok') +
108
- '. Ortak proje ayarını kaydedelim mi?'
109
- : 'Save the shared ' +
110
- role +
111
- ' branch preference: ' +
112
- (value.branch ? value.remote + '/' + value.branch : 'none yet') +
113
- '?';
271
+ return value.branch ? value.remote + '/' + value.branch : none;
114
272
  };
115
273
  const digest = sha256(stableStringify({
116
274
  projectId: input.projectId,
@@ -120,29 +278,44 @@ export function registerWorktreeTools(server, service) {
120
278
  ...(input.decisionAttempt ? { decisionAttempt: input.decisionAttempt } : {}),
121
279
  }));
122
280
  const questionnaireId = 'project-branches-' + digest;
123
- const definition = {
124
- repoRoot: input.repoRoot,
281
+ const definition = (turkish) => {
282
+ const copy = preferenceCopy[turkish ? 'tr' : 'en'];
283
+ return {
284
+ questionnaireId,
285
+ language: turkish ? 'tr' : 'en',
286
+ message: copy.message,
287
+ context: copy.context,
288
+ example: copy.example,
289
+ questions: roles.map((role) => ({
290
+ id: role,
291
+ message: copy.ask(copy.roles[role], source(role, null)),
292
+ options: [
293
+ { id: 'approve', label: copy.save, description: copy.saved },
294
+ { id: 'defer', label: copy.leave, description: copy.unchanged },
295
+ ],
296
+ })),
297
+ };
298
+ };
299
+ const previous = (turkish) => ({
125
300
  questionnaireId,
126
- presentation: input.presentation,
127
- language: input.language,
128
- message: input.language === 'tr'
301
+ language: turkish ? 'tr' : 'en',
302
+ message: turkish
129
303
  ? 'Ortak dal tercihlerini kaydedelim mi? Hiçbir şey kaydedilmeden önce onayını bekliyor.'
130
304
  : 'Save the shared branch preferences? Nothing is saved until you answer.',
131
305
  questions: roles.map((role) => ({
132
306
  id: role,
133
- message: roleLabel(role),
307
+ message: turkish
308
+ ? role +
309
+ ' dal tercihi: ' +
310
+ source(role, 'henüz yok') +
311
+ '. Ortak proje ayarını kaydedelim mi?'
312
+ : 'Save the shared ' + role + ' branch preference: ' + source(role, 'none yet') + '?',
134
313
  options: [
135
- {
136
- id: 'approve',
137
- label: input.language === 'tr' ? 'Bu tercihi kaydet' : 'Save this choice',
138
- },
139
- {
140
- id: 'defer',
141
- label: input.language === 'tr' ? 'Şimdilik değiştirme' : 'Leave unchanged',
142
- },
314
+ { id: 'approve', label: turkish ? 'Bu tercihi kaydet' : 'Save this choice' },
315
+ { id: 'defer', label: turkish ? 'Şimdilik değiştirme' : 'Leave unchanged' },
143
316
  ],
144
317
  })),
145
- };
318
+ });
146
319
  const owner = {
147
320
  tool: 'project.set_git_preferences',
148
321
  ...(input.decisionAttempt ? { decisionAttempt: input.decisionAttempt } : {}),
@@ -155,7 +328,11 @@ export function registerWorktreeTools(server, service) {
155
328
  language: input.language,
156
329
  }),
157
330
  };
158
- const form = await askQuestionnaire(server, service, definition, context, [], owner);
331
+ const form = await askQuestionnaire(server, service, {
332
+ ...definition(language === 'tr'),
333
+ repoRoot: input.repoRoot,
334
+ presentation: input.presentation,
335
+ }, context, [definition(language !== 'tr'), previous(true), previous(false)], owner);
159
336
  const record = await service.questionnaireResume({
160
337
  repoRoot: input.repoRoot,
161
338
  questionnaireId,
@@ -187,10 +364,11 @@ export function registerWorktreeTools(server, service) {
187
364
  name: branch.optional(),
188
365
  base: baseSchema.optional(),
189
366
  keepCurrent: z.boolean().optional(),
190
- language: z.enum(['tr', 'en']).default('en'),
367
+ language: languageTag.optional(),
191
368
  presentation,
192
369
  }),
193
370
  }, async (input, context) => {
371
+ const language = (await service.language(input.language))?.startsWith('tr') ? 'tr' : 'en';
194
372
  const preflight = await service.taskBranchPreflight(input);
195
373
  if (!preflight.ok)
196
374
  return output(preflight);
@@ -209,7 +387,7 @@ export function registerWorktreeTools(server, service) {
209
387
  recovery: 'task.branch',
210
388
  },
211
389
  });
212
- const definition = taskStartDefinition({ ...input, ...flight });
390
+ const { previous, ...definition } = taskStartDefinition({ ...input, ...flight, language });
213
391
  const owner = {
214
392
  tool: 'task.branch',
215
393
  externalTaskId: input.externalTaskId,
@@ -221,7 +399,7 @@ export function registerWorktreeTools(server, service) {
221
399
  keepCurrent: input.keepCurrent,
222
400
  }),
223
401
  };
224
- const form = await askQuestionnaire(server, service, { ...definition, repoRoot: input.repoRoot, presentation: input.presentation }, context, [], owner);
402
+ const form = await askQuestionnaire(server, service, { ...definition, repoRoot: input.repoRoot, presentation: input.presentation }, context, [previous], owner);
225
403
  const record = await service.questionnaireResume({
226
404
  repoRoot: input.repoRoot,
227
405
  questionnaireId: definition.questionnaireId,
@@ -269,16 +447,28 @@ export function registerWorktreeTools(server, service) {
269
447
  const fallbackId = 'task-branch-fallback-' +
270
448
  sha256(stableStringify({ ...request, base: fallbackBase, inPlace: choice.inPlace }));
271
449
  const fallback = async () => {
272
- const tr = input.language === 'tr';
273
450
  const from = sourceLabel(choice.base, flight.currentBranch);
274
451
  const local = sourceLabel(fallbackBase, flight.currentBranch);
275
- const fallbackForm = await askQuestionnaire(server, service, {
276
- repoRoot: input.repoRoot,
452
+ const definition = (turkish) => {
453
+ const copy = fallbackCopy[turkish ? 'tr' : 'en'];
454
+ return {
455
+ questionnaireId: fallbackId,
456
+ language: turkish ? 'tr' : 'en',
457
+ allowFreeText: false,
458
+ message: copy.message(from, local, choice.inPlace),
459
+ context: copy.context,
460
+ example: copy.example(from, local),
461
+ options: [
462
+ { id: 'approve', label: copy.approve, description: copy.approved(local) },
463
+ { id: 'defer', label: copy.defer, description: copy.deferred },
464
+ ],
465
+ };
466
+ };
467
+ const previousFallback = (turkish) => ({
277
468
  questionnaireId: fallbackId,
278
- language: input.language,
279
- presentation: input.presentation,
469
+ language: turkish ? 'tr' : 'en',
280
470
  allowFreeText: false,
281
- message: tr
471
+ message: turkish
282
472
  ? from +
283
473
  ' uzak dalına ulaşılamadı. Yereldeki ' +
284
474
  local +
@@ -291,10 +481,15 @@ export function registerWorktreeTools(server, service) {
291
481
  local +
292
482
  (choice.inPlace ? ' in this folder?' : ' in a separate worktree?'),
293
483
  options: [
294
- { id: 'approve', label: tr ? 'Devam et' : 'Continue' },
295
- { id: 'defer', label: tr ? 'Şimdilik başlatma' : 'Not now' },
484
+ { id: 'approve', label: turkish ? 'Devam et' : 'Continue' },
485
+ { id: 'defer', label: turkish ? 'Şimdilik başlatma' : 'Not now' },
296
486
  ],
297
- }, context, [], owner);
487
+ });
488
+ const fallbackForm = await askQuestionnaire(server, service, {
489
+ ...definition(language === 'tr'),
490
+ repoRoot: input.repoRoot,
491
+ presentation: input.presentation,
492
+ }, context, [definition(language !== 'tr'), previousFallback(true), previousFallback(false)], owner);
298
493
  const answer = await service.questionnaireResume({
299
494
  repoRoot: input.repoRoot,
300
495
  questionnaireId: fallbackId,
@@ -336,18 +531,104 @@ export function registerWorktreeTools(server, service) {
336
531
  inputSchema: locationSchema,
337
532
  }, async (input) => output(await service.worktreeOperation('list', input)));
338
533
  server.registerTool('worktree.reconcile', {
339
- description: 'Reconcile Git with the local registry, including unregistered legacy checkouts. Use releaseUnowned only on a selected unreserved recovered checkout after prior work has finished; a native source-bound confirmation and fresh safety checks precede reuse. Use retireLegacy only when the user wants old-location slots gone: each retired folder is removed with git worktree remove after its own native approval, and the branch stays. Otherwise never delete files or reset branches.',
534
+ description: 'Reconcile Git with the local registry, including unregistered legacy checkouts. Use releaseUnowned only on a selected unreserved recovered checkout after prior work has finished; a native source-bound confirmation and fresh safety checks precede reuse. Use retireLegacy only when the user wants old-location slots gone: each retired folder is removed with git worktree remove after its own native approval, and the branch stays. Use forgetUnreadable with the externalTaskId of a checkout that is gone or can no longer be read as a Git worktree, called from a working checkout of the same project: after its own native approval Engineering Memory stops tracking that entry, so it no longer counts toward the limit, and no file in the folder is read, moved or deleted. Otherwise never delete files or reset branches.',
340
535
  inputSchema: z.strictObject({
341
536
  repoRoot: repo,
342
537
  releaseUnowned: z.boolean().optional(),
343
538
  retireLegacy: z.boolean().optional(),
539
+ forgetUnreadable: z.strictObject({ externalTaskId: z.string().min(2).max(160) }).optional(),
344
540
  decisionAttempt: z.number().int().min(0).default(0),
345
- language: z.enum(['tr', 'en']).default('en'),
541
+ language: languageTag.optional(),
346
542
  }),
347
543
  }, async (input, context) => {
544
+ const language = (await service.language(input.language))?.startsWith('tr') ? 'tr' : 'en';
348
545
  const inspection = await service.worktreeOperation('reconcile', input);
349
- if (!inspection.ok || (!input.releaseUnowned && !input.retireLegacy))
546
+ if (!inspection.ok ||
547
+ (!input.releaseUnowned && !input.retireLegacy && !input.forgetUnreadable))
350
548
  return output(inspection);
549
+ if (input.forgetUnreadable) {
550
+ const item = inspection.data.items.find((entry) => entry.externalTaskId === input.forgetUnreadable.externalTaskId);
551
+ if (!item || item.activity === 'retired' || !unreadableWorktree(item))
552
+ return output({
553
+ ok: false,
554
+ error: {
555
+ kind: 'bridge_error',
556
+ retryable: false,
557
+ recovery: 'worktree.list',
558
+ message: 'Only a checkout that is missing or can no longer be read as a Git worktree is forgotten. Call worktree.list to see this task: a readable one is freed with worktree.release, and a retired slot is removed with worktree.reconcile and retireLegacy.',
559
+ },
560
+ });
561
+ const questionnaireId = forgetUnreadableQuestionId(item, input.decisionAttempt);
562
+ const folder = basename(item.repoRoot);
563
+ const definition = (turkish) => {
564
+ const copy = forgetCopy[turkish ? 'tr' : 'en'];
565
+ return {
566
+ questionnaireId,
567
+ language: turkish ? 'tr' : 'en',
568
+ allowFreeText: false,
569
+ message: copy.message(item.externalTaskId, folder),
570
+ context: copy.context(item.pendingDelivery === true),
571
+ example: copy.example(item.externalTaskId, folder),
572
+ options: [
573
+ { id: 'forget', label: copy.forget, description: copy.forgotten },
574
+ { id: 'keep', label: copy.keep, description: copy.kept },
575
+ ],
576
+ };
577
+ };
578
+ const previous = (turkish) => ({
579
+ questionnaireId,
580
+ language: turkish ? 'tr' : 'en',
581
+ allowFreeText: false,
582
+ message: turkish
583
+ ? item.externalTaskId +
584
+ ' görevinin ' +
585
+ folder +
586
+ ' klasörü artık bir Git çalışma kopyası olarak okunamıyor. Engineering Memory bu klasörü takip etmeyi bıraksın mı? Klasördeki hiçbir dosyaya dokunulmaz, hiçbiri silinmez.' +
587
+ (item.pendingDelivery
588
+ ? ' Bu görev teslimi sonuçlanmadan kapandı; teslim edilmemiş iş varsa yalnızca bu klasörde duruyor.'
589
+ : '')
590
+ : 'The ' +
591
+ folder +
592
+ ' folder of task ' +
593
+ item.externalTaskId +
594
+ ' can no longer be read as a Git checkout. Should Engineering Memory stop tracking it? No file in the folder is touched or deleted.' +
595
+ (item.pendingDelivery
596
+ ? ' That task closed without its delivery being settled, so any undelivered work exists only in that folder.'
597
+ : ''),
598
+ options: [
599
+ {
600
+ id: 'forget',
601
+ label: turkish
602
+ ? 'Takip etmeyi bırak; dosyalara dokunma'
603
+ : 'Stop tracking it; leave the files alone',
604
+ },
605
+ { id: 'keep', label: turkish ? 'Takip etmeye devam et' : 'Keep tracking it' },
606
+ ],
607
+ });
608
+ const form = await askQuestionnaire(server, service, { ...definition(language === 'tr'), repoRoot: input.repoRoot }, context, [definition(language !== 'tr'), previous(true), previous(false)], {
609
+ tool: 'worktree.reconcile',
610
+ ...(input.decisionAttempt ? { decisionAttempt: input.decisionAttempt } : {}),
611
+ retryArguments: json({
612
+ forgetUnreadable: input.forgetUnreadable,
613
+ language: input.language,
614
+ }),
615
+ });
616
+ const decision = await service.questionnaireResume({
617
+ repoRoot: input.repoRoot,
618
+ questionnaireId,
619
+ });
620
+ if (decision.status !== 'answered')
621
+ return form;
622
+ if (decision.answer?.choice !== 'forget')
623
+ return output({ ok: true, data: json({ kept: item.externalTaskId }) });
624
+ return output(await service.worktreeOperation('forget_unreadable', {
625
+ repoRoot: input.repoRoot,
626
+ externalTaskId: item.externalTaskId,
627
+ generation: item.generation,
628
+ decisionAttempt: input.decisionAttempt,
629
+ decisionId: questionnaireId,
630
+ }));
631
+ }
351
632
  if (input.retireLegacy) {
352
633
  const items = inspection.data.items.filter((item) => item.activity === 'retired');
353
634
  const retired = [];
@@ -357,12 +638,27 @@ export function registerWorktreeTools(server, service) {
357
638
  const questionnaireId = 'worktree-retire-' + sha256(item.repoRoot + item.generation);
358
639
  const missing = item.reasons.includes('worktree directory missing');
359
640
  const folder = basename(item.repoRoot);
360
- const form = await askQuestionnaire(server, service, {
361
- repoRoot: input.repoRoot,
641
+ const definition = (turkish) => {
642
+ const copy = retireCopy[turkish ? 'tr' : 'en'];
643
+ const named = item.branch ?? (turkish ? 'yok' : 'none');
644
+ return {
645
+ questionnaireId,
646
+ language: turkish ? 'tr' : 'en',
647
+ allowFreeText: false,
648
+ message: missing ? copy.missing(folder) : copy.message(folder, named),
649
+ context: copy.context(missing),
650
+ example: copy.example(folder, named, missing),
651
+ options: [
652
+ { id: 'retire', label: copy.retire, description: copy.retired(missing) },
653
+ { id: 'keep', label: copy.keep, description: copy.kept },
654
+ ],
655
+ };
656
+ };
657
+ const previous = (turkish) => ({
362
658
  questionnaireId,
363
- language: input.language,
659
+ language: turkish ? 'tr' : 'en',
364
660
  allowFreeText: false,
365
- message: input.language === 'tr'
661
+ message: turkish
366
662
  ? folder +
367
663
  (missing
368
664
  ? ' eski konumdaki bir worktree kaydı; klasör artık yok. Kaydı silinsin mi?'
@@ -376,13 +672,11 @@ export function registerWorktreeTools(server, service) {
376
672
  (item.branch ?? 'none') +
377
673
  '); it is no longer reused. Remove it? The branch stays. Git removes only a clean folder, and its ignored files go with it.'),
378
674
  options: [
379
- {
380
- id: 'retire',
381
- label: input.language === 'tr' ? 'Klasörü kaldır' : 'Remove the folder',
382
- },
383
- { id: 'keep', label: input.language === 'tr' ? 'Kalsın' : 'Keep it' },
675
+ { id: 'retire', label: turkish ? 'Klasörü kaldır' : 'Remove the folder' },
676
+ { id: 'keep', label: turkish ? 'Kalsın' : 'Keep it' },
384
677
  ],
385
- }, context);
678
+ });
679
+ const form = await askQuestionnaire(server, service, { ...definition(language === 'tr'), repoRoot: input.repoRoot }, context, [definition(language !== 'tr'), previous(true), previous(false)]);
386
680
  const decision = await service.questionnaireResume({
387
681
  repoRoot: input.repoRoot,
388
682
  questionnaireId,
@@ -426,27 +720,39 @@ export function registerWorktreeTools(server, service) {
426
720
  decisionAttempt: input.decisionAttempt,
427
721
  }));
428
722
  const questionnaireId = 'worktree-release-unowned-' + digest;
429
- const form = await askQuestionnaire(server, service, {
430
- repoRoot: input.repoRoot,
723
+ const definition = (turkish) => {
724
+ const copy = unownedCopy[turkish ? 'tr' : 'en'];
725
+ return {
726
+ questionnaireId,
727
+ language: turkish ? 'tr' : 'en',
728
+ allowFreeText: false,
729
+ message: copy.message(basename(entry.repoRoot)),
730
+ context: copy.context,
731
+ example: copy.example(entry.baseCommit.slice(0, 12)),
732
+ options: [
733
+ { id: 'release_unowned', label: copy.release, description: copy.released },
734
+ { id: 'preserve', label: copy.preserve, description: copy.preserved },
735
+ ],
736
+ };
737
+ };
738
+ const previous = (turkish) => ({
431
739
  questionnaireId,
432
- language: input.language,
740
+ language: turkish ? 'tr' : 'en',
433
741
  allowFreeText: false,
434
- message: input.language === 'tr'
742
+ message: turkish
435
743
  ? 'Bu eski çalışma klasörünün önceki sahiplik kaydı yok. Önceki ajan burada artık çalışmıyor ve teslim tamamlandıysa, temiz klasörü yeniden kullanılabilir yapalım mı? Dosyalar ve branch silinmeyecek.'
436
744
  : 'This legacy checkout has no prior ownership record. If the previous agent has stopped and delivery is complete, make the clean checkout reusable? No files or branches will be deleted.',
437
745
  options: [
438
746
  {
439
747
  id: 'release_unowned',
440
- label: input.language === 'tr'
748
+ label: turkish
441
749
  ? 'Önceki iş bitti; güvenli kontrollerden sonra kullanılabilir yap'
442
750
  : 'Prior work finished; release after safety checks',
443
751
  },
444
- {
445
- id: 'preserve',
446
- label: input.language === 'tr' ? 'Korumaya devam et' : 'Keep protected',
447
- },
752
+ { id: 'preserve', label: turkish ? 'Korumaya devam et' : 'Keep protected' },
448
753
  ],
449
- }, context);
754
+ });
755
+ const form = await askQuestionnaire(server, service, { ...definition(language === 'tr'), repoRoot: input.repoRoot }, context, [definition(language !== 'tr'), previous(true), previous(false)]);
450
756
  const decision = await service.questionnaireResume({
451
757
  repoRoot: input.repoRoot,
452
758
  questionnaireId,
@@ -474,9 +780,10 @@ export function registerWorktreeTools(server, service) {
474
780
  inputSchema: ownershipSchema.extend({
475
781
  deliveryOutcome: z.enum(['delivered', 'cancelled']).optional(),
476
782
  delivered: z.boolean().optional(),
477
- language: z.enum(['tr', 'en']).default('en'),
783
+ language: languageTag.optional(),
478
784
  }),
479
785
  }, async (input, context) => {
786
+ const language = (await service.language(input.language))?.startsWith('tr') ? 'tr' : 'en';
480
787
  if (input.deliveryOutcome !== 'cancelled')
481
788
  return output(await service.worktreeOperation('release', input));
482
789
  const state = await service.worktreeFolder(input);
@@ -489,28 +796,26 @@ export function registerWorktreeTools(server, service) {
489
796
  ...input,
490
797
  decisionId: questionnaireId,
491
798
  }));
492
- const release = {
493
- id: 'release',
494
- label: input.language === 'tr'
495
- ? 'Klasörü boşalt; branch kalsın'
496
- : 'Free the folder; the branch stays',
497
- };
498
- const save = {
499
- id: 'save_and_release',
500
- label: input.language === 'tr'
501
- ? 'Değişiklikleri kaydedip klasörü boşalt'
502
- : 'Save the changes and free the folder',
503
- };
504
- const keep = {
505
- id: 'keep',
506
- label: input.language === 'tr' ? 'Klasör olduğu gibi kalsın' : 'Keep the folder as it is',
799
+ const definition = (turkish) => {
800
+ const copy = cancelCopy[turkish ? 'tr' : 'en'];
801
+ const keep = { id: 'keep', label: copy.keep, description: copy.kept };
802
+ return {
803
+ questionnaireId,
804
+ language: turkish ? 'tr' : 'en',
805
+ allowFreeText: false,
806
+ message: copy.message(input.externalTaskId),
807
+ context: copy.context(folder.clean, folder.managed),
808
+ example: copy.example,
809
+ options: folder.clean || !folder.managed
810
+ ? [{ id: 'release', label: copy.release, description: copy.released }, keep]
811
+ : [{ id: 'save_and_release', label: copy.save, description: copy.saved }, keep],
812
+ };
507
813
  };
508
- const form = await askQuestionnaire(server, service, {
509
- repoRoot: input.repoRoot,
814
+ const previous = (turkish) => ({
510
815
  questionnaireId,
511
- language: input.language,
816
+ language: turkish ? 'tr' : 'en',
512
817
  allowFreeText: false,
513
- message: input.language === 'tr'
818
+ message: turkish
514
819
  ? input.externalTaskId +
515
820
  ' teslim edilmeyecek. Bu klasör ne olsun?' +
516
821
  (folder.clean
@@ -525,8 +830,33 @@ export function registerWorktreeTools(server, service) {
525
830
  : folder.managed
526
831
  ? ' It has uncommitted changes; saving keeps them in a Git ref they can be restored from.'
527
832
  : ' It has uncommitted changes; this is your own folder, so its files are never touched when it is freed.'),
528
- options: folder.clean || !folder.managed ? [release, keep] : [save, keep],
529
- }, context);
833
+ options: folder.clean || !folder.managed
834
+ ? [
835
+ {
836
+ id: 'release',
837
+ label: turkish
838
+ ? 'Klasörü boşalt; branch kalsın'
839
+ : 'Free the folder; the branch stays',
840
+ },
841
+ {
842
+ id: 'keep',
843
+ label: turkish ? 'Klasör olduğu gibi kalsın' : 'Keep the folder as it is',
844
+ },
845
+ ]
846
+ : [
847
+ {
848
+ id: 'save_and_release',
849
+ label: turkish
850
+ ? 'Değişiklikleri kaydedip klasörü boşalt'
851
+ : 'Save the changes and free the folder',
852
+ },
853
+ {
854
+ id: 'keep',
855
+ label: turkish ? 'Klasör olduğu gibi kalsın' : 'Keep the folder as it is',
856
+ },
857
+ ],
858
+ });
859
+ const form = await askQuestionnaire(server, service, { ...definition(language === 'tr'), repoRoot: input.repoRoot }, context, [definition(language !== 'tr'), previous(true), previous(false)]);
530
860
  const decision = await service.questionnaireResume({
531
861
  repoRoot: input.repoRoot,
532
862
  questionnaireId,