engineering-memory 1.10.4 → 1.10.5

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.10.4",
3
+ "version": "1.10.5",
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",
@@ -16,6 +16,42 @@ const json = z.lazy(() => z.union([
16
16
  z.record(z.string(), json),
17
17
  ]));
18
18
  const object = z.record(z.string(), json);
19
+ const inspectionCopy = {
20
+ tr: {
21
+ modes: {
22
+ refresh: 'Projenin hafızasını güncel kodla karşılaştırıp incelemeyi başlatalım mı?',
23
+ adopt: 'Mevcut projeyi mimarisiyle birlikte öğrenmeye başlayalım mı?',
24
+ rework: 'Rework planından önce mevcut projeyi incelemeye başlayalım mı?',
25
+ },
26
+ source: 'Kaynak',
27
+ scope: 'Mimari, state yönetimi, servisler, network katmanı, veri modelleri, ekranlar ve akışlar incelenecek.',
28
+ localChanges: 'Uygulama kodu değiştirilmeyecek. Commit edilmemiş değişiklikler ortak hafızaya alınmayacak.',
29
+ review: 'Bu onay incelemeyi başlatır. Kalıcı hafıza değişiklikleri ayrıca onayına sunulacak.',
30
+ rework: {
31
+ preserve: 'Rework hedefi: mevcut davranışı korumak.',
32
+ redesign: 'Rework hedefi: ürünü yeniden tasarlamak. Uygulama öncesinde hedef ayrıca onaylanacak.',
33
+ },
34
+ start: 'İncelemeyi başlat',
35
+ defer: 'Şimdilik başlatma',
36
+ },
37
+ en: {
38
+ modes: {
39
+ refresh: 'Start comparing project memory with the current code?',
40
+ adopt: 'Start learning the existing project and its architecture?',
41
+ rework: 'Start inspecting the existing project before planning its rework?',
42
+ },
43
+ source: 'Source',
44
+ scope: 'Inspect architecture, state, services, network, data models, screens and flows.',
45
+ localChanges: 'Application code will stay unchanged. Uncommitted changes will not enter shared memory.',
46
+ review: 'This approval starts the inspection. Permanent memory changes will be submitted for separate approval.',
47
+ rework: {
48
+ preserve: 'Rework goal: preserve existing behavior.',
49
+ redesign: 'Rework goal: redesign the product. The target needs separate approval before implementation.',
50
+ },
51
+ start: 'Start inspection',
52
+ defer: 'Not now',
53
+ },
54
+ };
19
55
  const reference = z.object({
20
56
  proposalId: uuid,
21
57
  contentHash: hash,
@@ -197,12 +233,16 @@ export function registerOnboardingTools(server, service) {
197
233
  }),
198
234
  }, async (input) => output(await service.memorySource(input)));
199
235
  server.registerTool('memory.sync_start', {
200
- description: 'Start or resume an idempotent committed-source inspection after native mode approval. Uploads all manifest pages, including exclusions. Request key and source are immutable. Refresh references a completed earlier run; rework requires preserve/redesign. Analyze with the current agent; no backend LLM.',
236
+ description: 'Start or resume an idempotent committed-source inspection after native mode approval. Set language from the current conversation (tr or en), never from machine locale. Uploads all manifest pages, including exclusions. Request key and source are immutable. Refresh references a completed earlier run; rework requires preserve/redesign. Analyze with the current agent; no backend LLM.',
201
237
  inputSchema: z.object({
202
238
  ...project,
203
239
  repoRoot,
204
240
  requestKey: z.string().regex(/^[a-zA-Z0-9_-]{1,120}$/),
205
241
  mode: z.enum(['adopt', 'refresh', 'rework']),
242
+ language: z
243
+ .enum(['tr', 'en'])
244
+ .optional()
245
+ .describe('Use the current conversation language: tr for Turkish, en for English. Omitted by legacy callers means English.'),
206
246
  ref: z.string().optional(),
207
247
  previousRunId: uuid.optional(),
208
248
  reworkBehavior: z.enum(['preserve', 'redesign']).optional(),
@@ -226,24 +266,51 @@ export function registerOnboardingTools(server, service) {
226
266
  recovery: 'memory.sync_status',
227
267
  },
228
268
  });
229
- const pinned = { ...input, ref: commit };
269
+ const { language = 'en', ...request } = input;
270
+ const pinned = { ...request, ref: commit };
230
271
  const digest = sha256(stableStringify(pinned));
231
- const question = await askQuestionnaire(server, service, {
232
- repoRoot: input.repoRoot,
233
- questionnaireId: 'sync-start-' + digest,
234
- message: 'Start ' +
235
- input.mode +
236
- ' for committed source ' +
237
- commit +
238
- '? Inspect architecture, state, services, network, data, screens and flows; dirty changes stay local. ' +
239
- (input.reworkBehavior ? 'Rework target: ' + input.reworkBehavior + '. ' : '') +
240
- 'Exact request: ' +
241
- digest,
242
- options: [
243
- { id: 'approve', label: 'Start this exact inspection' },
244
- { id: 'defer', label: 'Keep inspection unstarted' },
245
- ],
246
- }, context);
272
+ const reference = input.ref ?? 'HEAD';
273
+ const sourceLabel = /^[0-9a-f]{40,64}$/i.test(reference)
274
+ ? commit.slice(0, 12)
275
+ : `${reference.slice(0, 80)} · ${commit.slice(0, 12)}`;
276
+ const definitions = ['tr', 'en'].map((displayLanguage) => {
277
+ const copy = inspectionCopy[displayLanguage];
278
+ return {
279
+ questionnaireId: 'sync-start-' + digest,
280
+ language: displayLanguage,
281
+ message: [
282
+ copy.modes[input.mode],
283
+ `${copy.source}: ${sourceLabel}`,
284
+ copy.scope,
285
+ copy.localChanges,
286
+ copy.review,
287
+ ...(input.reworkBehavior ? [copy.rework[input.reworkBehavior]] : []),
288
+ ].join('\n\n'),
289
+ options: [
290
+ { id: 'approve', label: copy.start },
291
+ { id: 'defer', label: copy.defer },
292
+ ],
293
+ };
294
+ });
295
+ const definition = definitions.find((entry) => entry.language === language);
296
+ const question = await askQuestionnaire(server, service, { ...definition, repoRoot: input.repoRoot }, context, [
297
+ ...definitions,
298
+ {
299
+ questionnaireId: 'sync-start-' + digest,
300
+ message: 'Start ' +
301
+ input.mode +
302
+ ' for committed source ' +
303
+ commit +
304
+ '? Inspect architecture, state, services, network, data, screens and flows; dirty changes stay local. ' +
305
+ (input.reworkBehavior ? 'Rework target: ' + input.reworkBehavior + '. ' : '') +
306
+ 'Exact request: ' +
307
+ digest,
308
+ options: [
309
+ { id: 'approve', label: 'Start this exact inspection' },
310
+ { id: 'defer', label: 'Keep inspection unstarted' },
311
+ ],
312
+ },
313
+ ]);
247
314
  if (answerChoice(question) !== 'approve')
248
315
  return question;
249
316
  return output(await service.memorySyncStart({ ...pinned, questionnaireId: 'sync-start-' + digest }));
@@ -1,8 +1,8 @@
1
1
  import { acceptedContent, CLIENT_CAPABILITIES_META_KEY, inputRequired, inputResponse, } from '@modelcontextprotocol/server';
2
2
  import { questionnaireAnswerSchema, } from '../runtime/questionnaire-store.js';
3
- export async function askQuestionnaire(server, service, input, context) {
3
+ export async function askQuestionnaire(server, service, input, context, previousDefinitions = []) {
4
4
  try {
5
- return await present(server, service, await service.questionnaireAsk(input), input.repoRoot, context, input.preparation);
5
+ return await present(server, service, await service.questionnaireAsk(input, previousDefinitions), input.repoRoot, context, input.preparation);
6
6
  }
7
7
  catch (error) {
8
8
  return failure(error);
@@ -53,31 +53,32 @@ async function present(server, service, record, repoRoot, context, preparation)
53
53
  const elicitation = capabilities?.elicitation;
54
54
  if (!elicitation || (!elicitation.form && elicitation.url !== undefined))
55
55
  return pending(record, 'native_form_unavailable');
56
+ const copy = questionnaireCopy[record.language ?? 'en'];
56
57
  return inputRequired({
57
58
  inputRequests: {
58
59
  [record.requestKey]: inputRequired.elicit({
59
60
  mode: 'form',
60
- message: `${record.message}\n\nChoose an answer to resolve this decision. Closing or cancelling the form leaves it pending.${record.allowFreeText ? ' For another answer choose Other and fill in text. Free text is not stored and cannot be replayed.' : ''}${record.textField ? ' The bounded text field is stored as part of this exact decision and is replayed after restart.' : ''}`,
61
+ message: `${record.message}\n\n${copy.pending}${record.allowFreeText ? copy.freeText : ''}${record.textField ? copy.storedText : ''}`,
61
62
  requestedSchema: {
62
63
  type: 'object',
63
64
  properties: {
64
65
  choice: {
65
66
  type: 'string',
66
- title: 'Answer',
67
+ title: copy.answer,
67
68
  enum: [
68
69
  ...record.options.map((option) => option.id),
69
70
  ...(record.allowFreeText ? ['__other__'] : []),
70
71
  ],
71
72
  enumNames: [
72
73
  ...record.options.map((option) => option.label),
73
- ...(record.allowFreeText ? ['Other'] : []),
74
+ ...(record.allowFreeText ? [copy.other] : []),
74
75
  ],
75
76
  },
76
77
  ...(record.allowFreeText
77
78
  ? {
78
79
  text: {
79
80
  type: 'string',
80
- title: 'Other answer',
81
+ title: copy.otherAnswer,
81
82
  minLength: 1,
82
83
  maxLength: 2000,
83
84
  },
@@ -100,6 +101,24 @@ async function present(server, service, record, repoRoot, context, preparation)
100
101
  },
101
102
  });
102
103
  }
104
+ const questionnaireCopy = {
105
+ en: {
106
+ pending: 'Choose an answer to resolve this decision. Closing or cancelling the form leaves it pending.',
107
+ freeText: ' For another answer choose Other and fill in text. Free text is not stored and cannot be replayed.',
108
+ storedText: ' The bounded text field is stored as part of this exact decision and is replayed after restart.',
109
+ answer: 'Answer',
110
+ other: 'Other',
111
+ otherAnswer: 'Other answer',
112
+ },
113
+ tr: {
114
+ pending: 'Kararını kaydetmek için bir seçenek seç. Formu kapatmak veya iptal etmek onay sayılmaz; soru beklemede kalır.',
115
+ 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.',
116
+ storedText: ' Bu metin alanındaki yanıt kararınla birlikte saklanır; yeniden başlattığında korunur.',
117
+ answer: 'Yanıt',
118
+ other: 'Diğer',
119
+ otherAnswer: 'Diğer yanıt',
120
+ },
121
+ };
103
122
  function pending(record, reason) {
104
123
  return result({
105
124
  questionnaireId: record.questionnaireId,
@@ -171,7 +171,7 @@ export function registerQuestionnaireTools(server, service) {
171
171
  }
172
172
  });
173
173
  server.registerTool('questionnaire.ask', {
174
- description: 'Persist a required decision before displaying a native questionnaire. 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 tool arguments. 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.',
174
+ 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 tool arguments. 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.',
175
175
  inputSchema: questionnaireDefinitionSchema.extend({
176
176
  repoRoot: optionalRepoRoot,
177
177
  preparation: z.boolean().optional(),
@@ -36,10 +36,10 @@ export class BridgeService {
36
36
  this.dependencies = dependencies;
37
37
  this.deferDeliveries = dependencies.deferDeliveries ?? true;
38
38
  }
39
- async questionnaireAsk(input) {
39
+ async questionnaireAsk(input, previousDefinitions = []) {
40
40
  const scope = await this.questionnaireScope(input.repoRoot, input.preparation);
41
41
  const { repoRoot: _repoRoot, preparation: _preparation, ...definition } = input;
42
- return await this.dependencies.questionnaires.ask(scope, definition);
42
+ return await this.dependencies.questionnaires.ask(scope, definition, previousDefinitions);
43
43
  }
44
44
  async questionnaireResume(input) {
45
45
  const scope = await this.questionnaireScope(input.repoRoot, input.preparation);
@@ -9,6 +9,10 @@ const identifier = z.string().regex(/^[A-Za-z0-9_-]{1,100}$/);
9
9
  export const questionnaireDefinitionSchema = z.strictObject({
10
10
  questionnaireId: identifier,
11
11
  message: z.string().trim().min(1).max(2000),
12
+ language: z
13
+ .enum(['tr', 'en'])
14
+ .optional()
15
+ .describe('Language of the current conversation: tr for Turkish, en for English. Translate the message and option labels into that language too.'),
12
16
  options: z
13
17
  .array(z.strictObject({
14
18
  id: z
@@ -97,8 +101,14 @@ export class QuestionnaireStore {
97
101
  constructor(stateRoot) {
98
102
  this.root = join(stateRoot, 'questionnaires');
99
103
  }
100
- async ask(scope, input) {
104
+ async ask(scope, input, previousDefinitions = []) {
101
105
  const definition = questionnaireDefinitionSchema.parse(input);
106
+ const previousHashes = previousDefinitions.map((previous) => {
107
+ const parsed = questionnaireDefinitionSchema.parse(previous);
108
+ if (parsed.questionnaireId !== definition.questionnaireId)
109
+ throw new Error('Previous questionnaire content must belong to the same decision.');
110
+ return sha256(stableStringify(parsed));
111
+ });
102
112
  scopeSchema.parse(scope);
103
113
  assertSafeToPersist(definition);
104
114
  const contentHash = sha256(stableStringify(definition));
@@ -112,7 +122,8 @@ export class QuestionnaireStore {
112
122
  };
113
123
  await this.publish(this.pathFor(scope, definition.questionnaireId, 'pending.json'), question);
114
124
  const record = await this.get(scope, definition.questionnaireId);
115
- if (!record || record.contentHash !== contentHash) {
125
+ if (!record ||
126
+ (record.contentHash !== contentHash && !previousHashes.includes(record.contentHash))) {
116
127
  throw new Error('Questionnaire id was reused with different content. Resume the original question or use a new id.');
117
128
  }
118
129
  return record;
@@ -125,6 +136,7 @@ export class QuestionnaireStore {
125
136
  const definition = questionnaireDefinitionSchema.parse({
126
137
  questionnaireId: question.questionnaireId,
127
138
  message: question.message,
139
+ language: question.language,
128
140
  options: question.options,
129
141
  allowFreeText: question.allowFreeText,
130
142
  textField: question.textField,
@@ -144,3 +144,16 @@ gates. Cross-project phases use existing work_item.plan/confirm_plan with explic
144
144
  and independent per-repository runs. Planning tasks does not perform their implementation.
145
145
 
146
146
  A deferred individual field or final profile saves no onboarding choice. Retrying that request preserves its deferral. When the user explicitly resumes, use the returned `nextRequestKey` for fresh native decisions; never infer approval from a previous defer.
147
+
148
+ ## Inspection confirmation language
149
+
150
+ For memory.sync_start, pass language: tr in a Turkish conversation and language: en in an English
151
+ conversation. Use the conversation language, never the computer locale. Legacy calls without language
152
+ use English. The form shows a short source reference, scope, the treatment of uncommitted changes and
153
+ the separate approval needed for permanent memory. Full commit and request hashes stay in the durable
154
+ approval binding, not in the visible question. Generic questionnaire.ask messages and option labels
155
+ are written by the agent in the same language; pass language for the native help and field labels.
156
+
157
+ An existing exact-request questionnaire retains its original wording and answer across updates and
158
+ restart, including an older English form. Resume that question rather than silently replacing or
159
+ withdrawing it. Language changes never broaden the approved source, mode or previous-run scope.
@@ -239,3 +239,16 @@ again.
239
239
  `project.onboard` can save the profile before initialization. Calling `project.initialize` with the
240
240
  same request key reuses the profile answers and asks a separate exact Git-initialization confirmation.
241
241
  Saving a profile does not authorize filesystem initialization.
242
+
243
+ ## Inspection confirmation language
244
+
245
+ For memory.sync_start, pass language: tr in a Turkish conversation and language: en in an English
246
+ conversation. Use the conversation language, never the computer locale. Legacy calls without language
247
+ use English. The form shows a short source reference, scope, the treatment of uncommitted changes and
248
+ the separate approval needed for permanent memory. Full commit and request hashes stay in the durable
249
+ approval binding, not in the visible question. Generic questionnaire.ask messages and option labels
250
+ are written by the agent in the same language; pass language for the native help and field labels.
251
+
252
+ An existing exact-request questionnaire retains its original wording and answer across updates and
253
+ restart, including an older English form. Resume that question rather than silently replacing or
254
+ withdrawing it. Language changes never broaden the approved source, mode or previous-run scope.