engineering-memory 1.11.11 → 1.11.13

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.
@@ -31,6 +31,13 @@ export async function resumeQuestionnaire(server, service, input, context) {
31
31
  }
32
32
  export async function answerQuestionnaireFromHost(service, input) {
33
33
  try {
34
+ if (input.answer?.choice === explain) {
35
+ const record = await service.questionnaireResume(input);
36
+ if (record.status === 'pending' &&
37
+ record.requestKey === input.requestKey &&
38
+ record.contentHash === input.contentHash)
39
+ return pending(record, 'needs_explanation');
40
+ }
34
41
  const resolved = await service.questionnaireAnswerFromHost(input);
35
42
  return answered(resolved.record, resolved.answer, resolved.replayed, resolved.retry);
36
43
  }
@@ -59,6 +66,10 @@ async function present(server, service, record, repoRoot, context, preparation,
59
66
  const response = inputResponse(responses, record.requestKey);
60
67
  if (response.kind === 'elicit' && response.action === 'accept') {
61
68
  const content = acceptedContent(responses, record.requestKey);
69
+ if (content?.choice === explain || content?.[explain] === true) {
70
+ consumedResponses(context).add(record.requestKey);
71
+ return pending(record, 'needs_explanation');
72
+ }
62
73
  const parsed = questionnaireAnswerSchema(record).safeParse(record.questions && content ? formAnswers(record, content) : content);
63
74
  const answer = parsed.success ? parsed.data : undefined;
64
75
  if (!answer)
@@ -85,10 +96,9 @@ async function present(server, service, record, repoRoot, context, preparation,
85
96
  const elicitation = capabilities?.elicitation;
86
97
  if (!elicitation || (!elicitation.form && elicitation.url !== undefined))
87
98
  return pending(record, 'native_form_unavailable');
88
- const copy = questionnaireCopy[record.language ?? 'en'];
99
+ const copy = copyFor(record);
89
100
  if (record.questions) {
90
101
  const properties = {};
91
- const required = [];
92
102
  for (const sub of record.questions) {
93
103
  properties[sub.id] = {
94
104
  type: 'string',
@@ -96,7 +106,6 @@ async function present(server, service, record, repoRoot, context, preparation,
96
106
  enum: sub.options.map((option) => option.id),
97
107
  enumNames: sub.options.map((option) => option.label),
98
108
  };
99
- required.push(sub.id);
100
109
  if (sub.textField)
101
110
  properties[`${sub.id}_text`] = {
102
111
  type: 'string',
@@ -105,12 +114,13 @@ async function present(server, service, record, repoRoot, context, preparation,
105
114
  maxLength: sub.textField.maxLength,
106
115
  };
107
116
  }
117
+ properties[explain] = { type: 'boolean', title: copy.explain };
108
118
  return inputRequired({
109
119
  inputRequests: {
110
120
  [record.requestKey]: inputRequired.elicit({
111
121
  mode: 'form',
112
122
  message: presentationMessage(record),
113
- requestedSchema: { type: 'object', properties, required },
123
+ requestedSchema: { type: 'object', properties },
114
124
  }),
115
125
  },
116
126
  });
@@ -129,10 +139,12 @@ async function present(server, service, record, repoRoot, context, preparation,
129
139
  enum: [
130
140
  ...record.options.map((option) => option.id),
131
141
  ...(record.allowFreeText ? ['__other__'] : []),
142
+ explain,
132
143
  ],
133
144
  enumNames: [
134
145
  ...record.options.map((option) => option.label),
135
146
  ...(record.allowFreeText ? [copy.other] : []),
147
+ copy.explain,
136
148
  ],
137
149
  },
138
150
  ...(record.allowFreeText
@@ -162,6 +174,7 @@ async function present(server, service, record, repoRoot, context, preparation,
162
174
  },
163
175
  });
164
176
  }
177
+ const explain = '__explain__';
165
178
  const consumed = new WeakMap();
166
179
  function consumedResponses(context) {
167
180
  const keys = consumed.get(context.mcpReq) ?? new Set();
@@ -173,7 +186,9 @@ const questionnaireCopy = {
173
186
  pending: 'Choose an answer to resolve this decision. Closing or cancelling the form leaves it pending.',
174
187
  freeText: ' For another answer choose Other and fill in text. Free text is not stored and cannot be replayed.',
175
188
  storedText: ' The bounded text field is stored as part of this exact decision and is replayed after restart.',
176
- multi: ' AskUserQuestion takes up to 4 questions per call. Show every question in as few calls as needed, then relay all answers in one questionnaire.answer_from_host.',
189
+ everyQuestion: ' Answer every question before submitting, unless you pick an option that ends the form.',
190
+ example: 'Example',
191
+ explain: 'I did not understand; explain in more detail first',
177
192
  answer: 'Answer',
178
193
  other: 'Other',
179
194
  otherAnswer: 'Other answer',
@@ -182,7 +197,9 @@ const questionnaireCopy = {
182
197
  pending: 'Kararını kaydetmek için bir seçenek seç. Formu kapatmak veya iptal etmek onay sayılmaz; soru beklemede kalır.',
183
198
  freeText: ' Farklı bir yanıt için Diğer seçeneğini seçip metin alanını doldur. Serbest yanıt saklanmaz ve daha sonra geri getirilemez.',
184
199
  storedText: ' Bu metin alanındaki yanıt kararınla birlikte saklanır; yeniden başlattığında korunur.',
185
- multi: ' AskUserQuestion tek çağrıda en fazla 4 soru alır. Her soruyu en az çağrıyla göster, sonra tüm yanıtları tek bir questionnaire.answer_from_host çağrısında ilet.',
200
+ everyQuestion: ' Göndermeden önce her soruyu cevapla; formu bitiren bir seçenek seçtiysen gerekmez.',
201
+ example: 'Örnek',
202
+ explain: 'Anlamadım; önce daha ayrıntılı anlat',
186
203
  answer: 'Yanıt',
187
204
  other: 'Diğer',
188
205
  otherAnswer: 'Diğer yanıt',
@@ -204,11 +221,29 @@ function formAnswers(record, content) {
204
221
  return answers;
205
222
  }
206
223
  function presentationMessage(record) {
207
- const notice = /^memory-batch-[a-f0-9]{64}-[0-9]+$/.test(record.questionnaireId)
208
- ? 'For an unversioned project proposal, approval updates legacy memory only. Source-aware tasks cannot read it until an inspection explicitly retains it. Check each sourceApplicabilityNotice in memory.list_proposals before approving.'
209
- : undefined;
210
- const copy = questionnaireCopy[record.language ?? 'en'];
211
- return `${record.message}${notice ? `\n\n${notice}` : ''}\n\n${copy.pending}${record.allowFreeText ? copy.freeText : ''}${record.textField ? copy.storedText : ''}${record.questions ? copy.multi : ''}`;
224
+ const copy = copyFor(record);
225
+ const explained = (item) => [
226
+ item.context,
227
+ item.example && `${copy.example}: ${item.example}`,
228
+ (item.options ?? [])
229
+ .filter((option) => option.description)
230
+ .map((option) => `${option.label}: ${option.description}`)
231
+ .join('\n'),
232
+ ];
233
+ return [
234
+ record.message,
235
+ ...explained(record),
236
+ ...(record.questions ?? []).flatMap((sub) => {
237
+ const details = explained(sub).filter(Boolean);
238
+ return details.length ? [sub.message, ...details] : [];
239
+ }),
240
+ `${copy.pending}${record.questions ? copy.everyQuestion : ''}${record.allowFreeText ? copy.freeText : ''}${record.textField ? copy.storedText : ''}`,
241
+ ]
242
+ .filter(Boolean)
243
+ .join('\n\n');
244
+ }
245
+ function copyFor(record) {
246
+ return questionnaireCopy[record.language?.startsWith('tr') ? 'tr' : 'en'];
212
247
  }
213
248
  function pending(record, reason) {
214
249
  return result({
@@ -233,13 +268,17 @@ function pending(record, reason) {
233
268
  allowFreeText: record.allowFreeText,
234
269
  ...(record.textField ? { textField: record.textField } : {}),
235
270
  }),
236
- instructions: record.questions
271
+ instructions: `${record.questions
237
272
  ? 'Only if the host permits a blocking native control for this decision, display every question, its options and notices in AskUserQuestion or request_user_input, one call at a time if needed. Relay every answer together in one questionnaire.answer_from_host call as `answers`, keyed by question id. Closing, declining, timeout, prose consent and missing answers are not native answers for any question. Do not reopen a dismissed question in a loop or label decline as proof of host incompatibility. The relay records agent-reported provenance, not MCP transport attestation. Retry the owning operation after acceptance; its authority and version checks still apply.'
238
- : 'Only if the host permits a blocking native control for this decision, display this exact question, all options and notices in AskUserQuestion or request_user_input. Relay only the actual returned answer with these unchanged bindings. Closing, declining, timeout, prose consent and missing answers are not native answers. Do not reopen a dismissed question in a loop or label decline as proof of host incompatibility. The relay records agent-reported provenance, not MCP transport attestation. Retry the owning operation after acceptance; its authority and version checks still apply.',
273
+ : 'Only if the host permits a blocking native control for this decision, display this exact question, all options and notices in AskUserQuestion or request_user_input. Relay only the actual returned answer with these unchanged bindings. Closing, declining, timeout, prose consent and missing answers are not native answers. Do not reopen a dismissed question in a loop or label decline as proof of host incompatibility. The relay records agent-reported provenance, not MCP transport attestation. Retry the owning operation after acceptance; its authority and version checks still apply.'} Show each option's description under its label. If the person says they did not understand, including through the control's free-text answer, that is not an answer: call questionnaire.answer_from_host with answer {choice: "${explain}"} and the same bindings, and the decision stays pending.${(record.options?.length ?? 0) > 4
274
+ ? ' The host control shows fewer options than this question has: show them three at a time with one more option that reveals the rest, and relay only the final pick.'
275
+ : ''}`,
239
276
  },
240
- nextAction: reason === 'native_form_unavailable'
241
- ? 'This host does not advertise native MCP forms. The decision remains pending. Explain the limitation and continue only independently authorized work. Use hostFallback only with a permitted blocking native control; otherwise keep pending. Never substitute an asynchronous question.'
242
- : 'The decision remains pending without expiry. Call questionnaire.resume with this questionnaireId to show the same question again. Do not infer an answer or continue dependent work.',
277
+ nextAction: reason === 'needs_explanation'
278
+ ? 'The person asked for more detail and the decision is still pending. Explain in chat, in their language, why this is asked, what each option does next and one concrete example. Then show the same question once more with questionnaire.resume. Do not infer an answer.'
279
+ : reason === 'native_form_unavailable'
280
+ ? 'This host does not advertise native MCP forms. The decision remains pending. Explain the limitation and continue only independently authorized work. Use hostFallback only with a permitted blocking native control; otherwise keep pending. Never substitute an asynchronous question.'
281
+ : 'The decision remains pending without expiry. Call questionnaire.resume with this questionnaireId to show the same question again. Do not infer an answer or continue dependent work.',
243
282
  });
244
283
  }
245
284
  function answered(record, answer, replayed, retry) {
@@ -1,6 +1,7 @@
1
1
  import * as z from 'zod/v4';
2
2
  import { validationIds } from '../runtime/bridge-service.js';
3
- import { hostAnswerSchema, questionnaireDefinitionSchema } from '../runtime/questionnaire-store.js';
3
+ import { stableStringify } from '../utilities/hash.js';
4
+ import { hostAnswerSchema, languageTag, questionnaireDefinitionSchema, } from '../runtime/questionnaire-store.js';
4
5
  import { answerChoice, answerQuestionnaireFromHost, askQuestionnaire, resumeQuestionnaire, } from './questionnaire-tools.js';
5
6
  const optionalRepoRoot = z.string().min(1).optional();
6
7
  const stringList = z.array(z.string().min(1));
@@ -156,6 +157,38 @@ const reconciliationEntry = z.object({
156
157
  .describe('Optional for approved_revision; resolved from the approved proposal when omitted.'),
157
158
  reason: z.string().optional(),
158
159
  });
160
+ const waiverCopy = {
161
+ en: {
162
+ ask: (task) => `${task}: skip the UI check (widget, golden or screenshot test) for this task?`,
163
+ files: (listed) => `Changed files that draw the interface: ${listed}`,
164
+ reason: (reason) => `The reason you gave: ${reason}`,
165
+ context: 'The UI check is what proves the screens this task changed still render. Skipping it means nothing in this task opens them, and the skip is stored in the task audit trail with the reason above.',
166
+ example: 'A layout this change broke would pass verification and first show up when someone opens the app.',
167
+ waive: [
168
+ 'Skip the UI check',
169
+ 'The task verifies without a widget, golden or screenshot test of these files, and the skip is recorded.',
170
+ ],
171
+ require: [
172
+ 'Keep the UI check required',
173
+ 'Nothing is skipped. The task stays unverified until a widget, golden or screenshot test covers these files.',
174
+ ],
175
+ },
176
+ tr: {
177
+ ask: (task) => `${task}: bu görev için UI kontrolünü (widget, golden veya ekran görüntüsü testi) atlayalım mı?`,
178
+ files: (listed) => `Arayüzü çizen değişen dosyalar: ${listed}`,
179
+ reason: (reason) => `Verdiğin gerekçe: ${reason}`,
180
+ context: 'UI kontrolü, bu görevde değişen ekranların hâlâ doğru çizildiğini gösteren testtir. Atlarsan bu görevde o ekranları açan hiçbir test kalmaz ve atlama kararı yukarıdaki gerekçesiyle görevin denetim kaydına yazılır.',
181
+ example: 'Bu değişiklikte bozulan bir yerleşim doğrulamadan geçer ve ancak uygulamayı açan biri fark ettiğinde ortaya çıkar.',
182
+ waive: [
183
+ 'UI kontrolünü atla',
184
+ 'Görev, bu dosyalar için widget, golden veya ekran görüntüsü testi olmadan doğrulanır ve atlama kaydedilir.',
185
+ ],
186
+ require: [
187
+ 'UI kontrolü zorunlu kalsın',
188
+ 'Hiçbir şey atlanmaz. Bu dosyaları kapsayan bir widget, golden veya ekran görüntüsü testi gelene kadar görev doğrulanmaz.',
189
+ ],
190
+ },
191
+ };
159
192
  export function registerQuestionnaireTools(server, service) {
160
193
  server.registerTool('questionnaire.answer_from_host', {
161
194
  description: 'Relay an actual answer from a host-permitted blocking native AskUserQuestion or request_user_input control to the exact durable question. First read hostFallback from the pending operation or questionnaire.resume with presentation host_native, display its unchanged question/options/notices and await the real native result. Never infer answers, use chat consent, asynchronous controls, defaults or a declined/dismissed form. The hostTool field is agent-reported provenance, not transport-verified attestation. Bind requestKey and contentHash exactly; changed scope, withdrawn questions, invalid or conflicting answers are refused. This only saves the answer; retry the owning operation for current authority and version checks.',
@@ -195,13 +228,40 @@ export function registerQuestionnaireTools(server, service) {
195
228
  }
196
229
  });
197
230
  server.registerTool('questionnaire.ask', {
198
- description: 'Persist a required decision before displaying a native questionnaire. Write the message and labels in the conversation language and set language (tr or en) for the native form help. Use a questionnaireId unique to this decision occurrence or task, and reuse it only for identical retries. A later decision or changed wording/options requires a new id. Only a schema-validated native acceptance answers it. Dismissal, timeout and missing replies remain pending without expiry. Never supply answers in questionnaire.ask arguments; use questionnaire.answer_from_host only after an actual permitted blocking native answer. Do not put personal information or secrets in the question or options. Free text is returned once and never stored; use explicit options for replayable decisions. After the first decline in this session, pass presentation host_native to skip the doomed elicitation round trip.',
231
+ description: 'Persist a required decision before displaying a native questionnaire. Write the message, context, example, labels and option descriptions in the language the user writes in, and set language to its BCP-47 tag (tr, en, pt-BR); it is remembered, and the call is refused while no language is known. Say why the decision is asked in context, give one concrete example of what it changes in example, and give every option a description of what happens next. Show a record by its title, never by an id or a hash. Use a questionnaireId unique to this decision occurrence or task, and reuse it only for identical retries. A later decision or changed wording/options requires a new id. Only a schema-validated native acceptance answers it. Dismissal, timeout and missing replies remain pending without expiry. Never supply answers in questionnaire.ask arguments; use questionnaire.answer_from_host only after an actual permitted blocking native answer. Do not put personal information or secrets in the question or options. Free text is returned once and never stored; use explicit options for replayable decisions. After the first decline in this session, pass presentation host_native to skip the doomed elicitation round trip.',
199
232
  inputSchema: questionnaireDefinitionSchema.extend({
200
233
  repoRoot: optionalRepoRoot,
201
234
  preparation: z.boolean().optional(),
202
235
  presentation: z.literal('host_native').optional(),
203
236
  }),
204
- }, async (input, context) => await askQuestionnaire(server, service, input, context));
237
+ }, async (input, context) => {
238
+ if (!(await service.language(input.language)))
239
+ return toolResult({
240
+ ok: false,
241
+ error: {
242
+ kind: 'bridge_error',
243
+ message: 'Set language to the BCP-47 tag of the language the user is writing in, such as tr, en or pt-BR, and call questionnaire.ask again with otherwise identical arguments. It is remembered, so this is asked once.',
244
+ retryable: true,
245
+ },
246
+ });
247
+ const shown = [input, ...(input.questions ?? [])].flatMap((question) => [
248
+ question.message,
249
+ question.context,
250
+ question.example,
251
+ question.textField?.title,
252
+ ...(question.options ?? []).flatMap((option) => [option.label, option.description]),
253
+ ]);
254
+ if (shown.some((text) => /\b[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\b|[0-9a-f]{32,}/i.test(text?.replace(/`[^`]*`/g, '') ?? '')))
255
+ return toolResult({
256
+ ok: false,
257
+ error: {
258
+ kind: 'bridge_error',
259
+ message: 'The question shows an identifier or a hash where the person needs a name. Write the title of the record, branch or project instead; an identifier that has to be shown goes inside backticks. If this exact question is already pending, show it with questionnaire.resume instead.',
260
+ retryable: true,
261
+ },
262
+ });
263
+ return await askQuestionnaire(server, service, input, context);
264
+ });
205
265
  server.registerTool('questionnaire.resume', {
206
266
  description: 'Display the original durable questionnaire after interruption or return its saved accepted choice. No timeout or dismissal resolves a pending question. A saved free-text receipt cannot replay the text and must never be treated as a recovered answer. presentation host_native returns the original question and bindings for a permitted blocking host control without reopening the MCP form.',
207
267
  inputSchema: z.strictObject({
@@ -240,7 +300,12 @@ export function registerEngineeringMemoryTools(server, service) {
240
300
  }, async (input) => toolResult(await service.auditList(input)));
241
301
  server.registerTool('session.entry', {
242
302
  description: 'The first call of every session in a repository, before answering anything about the project. Reports whether the user is signed in, whether this repository is bound, what this user decided about it last time, and the one thing to do now. A repository the user switched Engineering Memory off in reports that, and is left alone.',
243
- inputSchema: z.object({ repoRoot: optionalRepoRoot }),
303
+ inputSchema: z.object({
304
+ repoRoot: optionalRepoRoot,
305
+ language: languageTag
306
+ .optional()
307
+ .describe('BCP-47 tag of the language the user is writing in, such as tr, en or pt-BR. It is remembered and every later form opens in it.'),
308
+ }),
244
309
  }, async (input) => toolResult(await service.sessionEntry(input)));
245
310
  server.registerTool('session.set_decision', {
246
311
  description: 'Record what the user decided about Engineering Memory in this repository: bound once they have chosen a project, disabled when they ask for it off, or none to forget the decision when they ask for it back. Call it only from something the user actually said; it is the only thing that changes the entry state.',
@@ -445,11 +510,11 @@ export function registerEngineeringMemoryTools(server, service) {
445
510
  }),
446
511
  }, async (input) => toolResult(await service.taskRecordCorrection(input)));
447
512
  server.registerTool('task.self_review', {
448
- description: 'Record that each changed file was read back against the rules that govern it: one entry per changed file (deleted files excepted), naming every rule context.prepare_change returned for that path in governingRules, and for a file no role maps, the engineering rules you read it against. Each rule gets an outcome: follows; fixed, with the issue and the change you made; or user_accepted_deviation, with the rule resourceKey and the issue. A rule conflict is a question for the user, never a judgment call: for every deviation this tool asks the user natively whether to keep the code, records the deviation only on their approval, and refuses the review when they choose to change the code. Matching the surrounding code is not an outcome; existing code is evidence, not authority. Set language to the conversation language (tr or en) for the question. Returns a durable pending receipt without waiting for backend delivery; task.verify refuses until a review covers the current diff, so any further edit requires reviewing again.',
513
+ description: 'Record that each changed file was read back against the rules that govern it: one entry per changed file (deleted files excepted), naming every rule context.prepare_change returned for that path in governingRules, and for a file no role maps, the engineering rules you read it against. Each rule gets an outcome: follows; fixed, with the issue and the change you made; or user_accepted_deviation, with the rule resourceKey and the issue. A rule conflict is a question for the user, never a judgment call: for every deviation this tool asks the user natively whether to keep the code, records the deviation only on their approval, and refuses the review when they choose to change the code. Matching the surrounding code is not an outcome; existing code is evidence, not authority. Set language to the BCP-47 tag of the language the user writes in (tr, en, pt-BR); it is remembered. Returns a durable pending receipt without waiting for backend delivery; task.verify refuses until a review covers the current diff, so any further edit requires reviewing again.',
449
514
  inputSchema: z.object({
450
515
  repoRoot: optionalRepoRoot,
451
516
  taskId: z.string().min(1),
452
- language: z.enum(['tr', 'en']).optional(),
517
+ language: languageTag.optional(),
453
518
  files: z
454
519
  .array(z.object({
455
520
  path: z.string().min(1).max(512),
@@ -478,19 +543,27 @@ export function registerEngineeringMemoryTools(server, service) {
478
543
  .max(500),
479
544
  }),
480
545
  }, async (input, context) => {
546
+ const language = (await service.language(input.language))?.startsWith('tr')
547
+ ? 'tr'
548
+ : 'en';
549
+ const review = { ...input, language };
481
550
  let questions;
482
551
  try {
483
- questions = service.ruleDeviationQuestions(input);
552
+ questions = await service.ruleDeviationQuestions(review);
484
553
  }
485
554
  catch {
486
- return toolResult(await service.taskSelfReview(input));
555
+ return toolResult(await service.taskSelfReview(review));
487
556
  }
488
557
  for (const { definition, previousDefinitions } of questions) {
489
- const form = await askQuestionnaire(server, service, { ...definition, repoRoot: input.repoRoot }, context, previousDefinitions);
558
+ const asked = { repoRoot: input.repoRoot, questionnaireId: definition.questionnaireId };
559
+ const stored = await service.questionnaireResume(asked).catch(() => undefined);
560
+ const form = stored && stableStringify(stored.binding) === stableStringify(definition.binding)
561
+ ? await resumeQuestionnaire(server, service, asked, context)
562
+ : await askQuestionnaire(server, service, { ...definition, repoRoot: input.repoRoot }, context, previousDefinitions);
490
563
  if (!answerChoice(form))
491
564
  return form;
492
565
  }
493
- return toolResult(await service.taskSelfReview(input));
566
+ return toolResult(await service.taskSelfReview(review));
494
567
  });
495
568
  server.registerTool('task.reconcile', {
496
569
  description: 'Reconcile changed screens and components with an approved revision or an explicit no-semantic-memory-change reason. Pass every record the task touched as `entries` in one call rather than calling once per record; the whole set is applied together and rejected together. After memory.review_proposal approves a proposal, pass its reconcileEntry ({resourceId, type: approved_revision, proposalId, revisionId}) straight through here — revisionId is optional and is resolved from the approved proposal when omitted. A pending result is a durable local receipt; continue independent work without polling. Required delivery is checked before verification.',
@@ -567,10 +640,9 @@ export function registerEngineeringMemoryTools(server, service) {
567
640
  repoRoot: optionalRepoRoot,
568
641
  taskId: z.string().min(1),
569
642
  sessionId: z.string().min(1),
570
- language: z
571
- .enum(['tr', 'en'])
643
+ language: languageTag
572
644
  .optional()
573
- .describe('Language of the current conversation, for the waiver question.'),
645
+ .describe('BCP-47 tag of the language the user writes in (tr, en, pt-BR), for the waiver question. It is remembered.'),
574
646
  leaseId: z.string().min(1).optional(),
575
647
  changedPaths: stringList.min(1).optional(),
576
648
  validations: z
@@ -603,7 +675,8 @@ export function registerEngineeringMemoryTools(server, service) {
603
675
  .describe('Only when the user decides a required ui check is not needed for this change. Never inferred from prose; the tool asks the user and verifies only after they agree.'),
604
676
  }),
605
677
  }, async (input, context) => {
606
- const { language = 'en', ...request } = input;
678
+ const { language: told, ...request } = input;
679
+ const language = (await service.language(told))?.startsWith('tr') ? 'tr' : 'en';
607
680
  for (const waiver of request.waivers ?? []) {
608
681
  const subject = await service.validationWaiverSubject({
609
682
  repoRoot: request.repoRoot,
@@ -615,7 +688,21 @@ export function registerEngineeringMemoryTools(server, service) {
615
688
  return toolResult(subject);
616
689
  const { questionnaireId, externalTaskId, paths, reason } = subject.data;
617
690
  const listed = paths.slice(0, 20).join(', ') + (paths.length > 20 ? ` (+${paths.length - 20})` : '');
618
- const definitions = [
691
+ const definitions = ['tr', 'en'].map((displayLanguage) => {
692
+ const copy = waiverCopy[displayLanguage];
693
+ return {
694
+ questionnaireId,
695
+ language: displayLanguage,
696
+ message: [copy.ask(externalTaskId), copy.files(listed), copy.reason(reason)].join('\n'),
697
+ context: copy.context,
698
+ example: copy.example,
699
+ options: [
700
+ { id: 'waive', label: copy.waive[0], description: copy.waive[1] },
701
+ { id: 'require', label: copy.require[0], description: copy.require[1] },
702
+ ],
703
+ };
704
+ });
705
+ const earlier = [
619
706
  {
620
707
  questionnaireId,
621
708
  language: 'tr',
@@ -638,7 +725,7 @@ export function registerEngineeringMemoryTools(server, service) {
638
725
  const form = await askQuestionnaire(server, service, {
639
726
  ...definitions.find((definition) => definition.language === language),
640
727
  repoRoot: request.repoRoot,
641
- }, context, definitions);
728
+ }, context, [...definitions, ...earlier]);
642
729
  const record = await service.questionnaireResume({
643
730
  repoRoot: request.repoRoot,
644
731
  questionnaireId,
@@ -843,15 +930,13 @@ export function registerEngineeringMemoryTools(server, service) {
843
930
  }),
844
931
  }, async (input) => toolResult(await service.workItemCreate(input)));
845
932
  server.registerTool('work_item.update', {
846
- description: 'Edit or assign a work item at its current version. Assignees must already be active project members; this does not grant project access.',
933
+ description: "Edit or assign a work item at its current version. Assignees must already be active project members; this does not grant project access. Status is a slug from the project's own status catalogue; a backend that still keeps the six legacy values (backlog, ready, in_progress, in_review, done, cancelled) refuses anything else and names what it accepts.",
847
934
  inputSchema: z.object({
848
935
  ...workItemLocator,
849
936
  data: z.object({
850
937
  ...workItemFields,
851
938
  expectedVersion: z.number().int().min(1),
852
- status: z
853
- .enum(['backlog', 'ready', 'in_progress', 'in_review', 'done', 'cancelled'])
854
- .optional(),
939
+ status: z.string().trim().min(1).max(64).optional(),
855
940
  }),
856
941
  }),
857
942
  }, async (input) => toolResult(await service.workItemUpdate(input)));
@@ -929,12 +1014,10 @@ export function registerEngineeringMemoryTools(server, service) {
929
1014
  }),
930
1015
  }, async (input) => toolResult(await service.projectUpdate(input)));
931
1016
  server.registerTool('work_item.list', {
932
- description: 'List selectable work items for a project before opening an engineering run in this chat.',
1017
+ description: "List selectable work items for a project before opening an engineering run in this chat. The status filter is a slug from the project's own status catalogue; a backend that still keeps the six legacy values (backlog, ready, in_progress, in_review, done, cancelled) refuses anything else and names what it accepts.",
933
1018
  inputSchema: z.object({
934
1019
  projectId: z.string().uuid(),
935
- status: z
936
- .enum(['backlog', 'ready', 'in_progress', 'in_review', 'done', 'cancelled'])
937
- .optional(),
1020
+ status: z.string().trim().min(1).max(64).optional(),
938
1021
  priority: z.enum(['lowest', 'low', 'medium', 'high', 'highest']).optional(),
939
1022
  assigneeUserId: z.string().uuid().optional(),
940
1023
  includeArchived: z.boolean().optional(),