engineering-memory 1.10.4 → 1.11.0

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.11.0",
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",
@@ -82,6 +82,118 @@ export class GitInspector {
82
82
  async currentBranch(repoRoot) {
83
83
  return await this.gitValue(repoRoot, ['symbolic-ref', '--short', '-q', 'HEAD']);
84
84
  }
85
+ async sourceIdentity(repoRoot, ref = 'HEAD') {
86
+ const sourceCommit = await this.gitValue(repoRoot, [
87
+ 'rev-parse',
88
+ '--verify',
89
+ '--end-of-options',
90
+ `${ref}^{commit}`,
91
+ ]);
92
+ if (!sourceCommit)
93
+ return null;
94
+ const treeHash = await this.gitValue(repoRoot, [
95
+ 'rev-parse',
96
+ '--verify',
97
+ `${sourceCommit}^{tree}`,
98
+ ]);
99
+ if (!treeHash ||
100
+ !/^([a-f0-9]{40}|[a-f0-9]{64})$/.test(sourceCommit) ||
101
+ !/^([a-f0-9]{40}|[a-f0-9]{64})$/.test(treeHash))
102
+ throw new Error('Git did not return an exact commit and tree identity');
103
+ return { sourceCommit, treeHash };
104
+ }
105
+ async committedTaskProof(repoRoot, source, changes) {
106
+ const result = await this.sourceIdentity(repoRoot);
107
+ if (!result)
108
+ throw new Error('The task result has not been committed');
109
+ const baseline = await this.sourceIdentity(repoRoot, source.sourceCommit);
110
+ if (!baseline ||
111
+ baseline.treeHash !== source.treeHash ||
112
+ result.sourceCommit === source.sourceCommit)
113
+ throw new Error('The verified task base is unavailable or differs from its recorded tree. Inspect the resulting commit with memory.sync_start.');
114
+ const output = await this.runner.run('git', [
115
+ 'diff',
116
+ '--raw',
117
+ '-z',
118
+ '--no-abbrev',
119
+ '--no-renames',
120
+ source.sourceCommit,
121
+ result.sourceCommit,
122
+ '--',
123
+ ], { cwd: repoRoot });
124
+ if (output.exitCode !== 0)
125
+ throw new Error('The committed task diff could not be read');
126
+ const expected = new Map();
127
+ const deleted = new Set();
128
+ for (const entry of changes) {
129
+ const status = semanticStatus(entry.status);
130
+ expected.set(entry.path, {
131
+ path: entry.path,
132
+ blobOid: status === 'D' ? null : entry.blobOid,
133
+ mode: status === 'D' ? null : entry.mode,
134
+ });
135
+ if (status === 'R' && entry.originalPath)
136
+ deleted.add(entry.originalPath);
137
+ }
138
+ for (const path of deleted)
139
+ if (!expected.has(path))
140
+ expected.set(path, { path, blobOid: null, mode: null });
141
+ const actual = [];
142
+ const tokens = output.stdout.split('\0').filter(Boolean);
143
+ for (let i = 0; i < tokens.length;) {
144
+ const header = tokens[i++].split(' ');
145
+ const path = normalizeGitPath(tokens[i++] ?? '');
146
+ if (header.length !== 5 ||
147
+ !header[0]?.startsWith(':') ||
148
+ !['A', 'M', 'D'].includes(header[4]))
149
+ throw new Error('Unsupported committed change; inspect this source explicitly.');
150
+ actual.push({
151
+ path,
152
+ blobOid: header[4] === 'D' ? null : header[3],
153
+ mode: header[4] === 'D' ? null : header[1],
154
+ });
155
+ }
156
+ const order = (a, b) => a.path.localeCompare(b.path);
157
+ if (!expected.size ||
158
+ stableStringify([...expected.values()].sort(order)) !== stableStringify(actual.sort(order)))
159
+ throw new Error('The committed tree differs from this task verified files. Inspect the resulting source with memory.sync_start.');
160
+ return { ...result, baseCommit: source.sourceCommit };
161
+ }
162
+ async sourceCandidates(repoRoot) {
163
+ const current = await this.sourceIdentity(repoRoot);
164
+ const output = await this.runner.run('git', [
165
+ 'for-each-ref',
166
+ '--count=51',
167
+ '--sort=-committerdate',
168
+ '--format=%(refname)',
169
+ 'refs/heads',
170
+ 'refs/remotes',
171
+ 'refs/tags',
172
+ ], { cwd: repoRoot });
173
+ if (output.exitCode !== 0)
174
+ throw new Error('Git source references could not be read');
175
+ const refs = output.stdout.trim().split(/\r?\n/).filter(Boolean);
176
+ const candidates = [];
177
+ if (current)
178
+ candidates.push({ ref: 'HEAD', ...current, aliases: [], checkedOut: true });
179
+ for (const ref of refs.slice(0, 50)) {
180
+ const source = await this.sourceIdentity(repoRoot, ref);
181
+ if (!source)
182
+ continue;
183
+ const previous = candidates.find((candidate) => candidate.sourceCommit === source.sourceCommit);
184
+ if (previous) {
185
+ previous.aliases.push(ref);
186
+ continue;
187
+ }
188
+ candidates.push({ ref, ...source, aliases: [], checkedOut: false });
189
+ }
190
+ return {
191
+ candidates,
192
+ truncated: refs.length > 50,
193
+ environmentMeaning: 'unknown',
194
+ nextAction: 'Choose the committed sources to learn with the user through native memory.sync_start confirmations. Branch names do not establish production, testing or development roles. Start only the chosen sources; use an explicit ref for an unlisted source.',
195
+ };
196
+ }
85
197
  async defaultBranch(repoRoot) {
86
198
  const ref = await this.gitValue(repoRoot, [
87
199
  'symbolic-ref',
@@ -26,6 +26,7 @@ export class VerificationGate {
26
26
  const worktreeId = worktreeIdentity(await this.git.findRoot(input.repoRoot));
27
27
  await writeJson(this.pathFor(input.repoFingerprint, worktreeId, input.taskId), {
28
28
  schemaVersion: 5,
29
+ source: input.source,
29
30
  worktreeId,
30
31
  taskId: input.taskId,
31
32
  projectId: input.projectId,
@@ -110,6 +111,19 @@ export class VerificationGate {
110
111
  details: [receipt.branch, branch ?? 'HEAD'],
111
112
  };
112
113
  }
114
+ const actualSource = receipt.source ? await this.git.sourceIdentity(repoRoot) : null;
115
+ if (receipt.source &&
116
+ (!actualSource ||
117
+ actualSource.sourceCommit !== receipt.source.sourceCommit ||
118
+ actualSource.treeHash !== receipt.source.treeHash)) {
119
+ return {
120
+ allowed: false,
121
+ reason: 'source_commit_mismatch',
122
+ diffHash: manifest.diffHash,
123
+ taskId: receipt.taskId,
124
+ details: ['Return to the task source commit or start a task for the current source.'],
125
+ };
126
+ }
113
127
  if (receipt.diffHash !== manifest.diffHash) {
114
128
  return {
115
129
  allowed: false,
@@ -144,6 +158,12 @@ export class VerificationGate {
144
158
  }
145
159
  try {
146
160
  const attestation = await this.commitGateAttestor({
161
+ source: actualSource
162
+ ? {
163
+ ...actualSource,
164
+ ...(receipt.source?.linked ? { linked: receipt.source.linked } : {}),
165
+ }
166
+ : undefined,
147
167
  taskId: receipt.taskId,
148
168
  projectId: receipt.projectId,
149
169
  repoFingerprint: receipt.repoFingerprint,
@@ -180,6 +200,29 @@ export class VerificationGate {
180
200
  taskId: receipt.taskId,
181
201
  };
182
202
  }
203
+ async publicationProof(repoRoot, taskId) {
204
+ const repository = await this.repositories.resolve(repoRoot);
205
+ const receipts = await this.receiptsFor(repository.repoFingerprint, worktreeIdentity(repository.repoRoot));
206
+ const receipt = receipts.find((candidate) => candidate.taskId === taskId &&
207
+ candidate.worktreeId === worktreeIdentity(repository.repoRoot));
208
+ if (!receipt?.source ||
209
+ !receipt.taskClosed ||
210
+ !receipt.projectId ||
211
+ !receipt.taskVersion ||
212
+ !isTaskManifest(receipt.taskChanges))
213
+ throw new Error('A closed source-aware task receipt is required. Otherwise inspect the resulting source with memory.sync_start.');
214
+ if ((await this.outbox.listForTask(taskId)).length)
215
+ throw new Error('Resolve this task pending deliveries before publishing its source memory.');
216
+ const proof = await this.git.committedTaskProof(repository.repoRoot, receipt.source, receipt.taskChanges);
217
+ return {
218
+ ...proof,
219
+ projectId: receipt.projectId,
220
+ taskId,
221
+ repoFingerprint: receipt.repoFingerprint,
222
+ expectedTaskVersion: receipt.taskVersion,
223
+ diffHash: receipt.diffHash,
224
+ };
225
+ }
183
226
  async invalidateTask(taskId) {
184
227
  await ensureManagedDirectory(this.root, this.root);
185
228
  const entries = await readdir(this.root, { withFileTypes: true });
@@ -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,
@@ -98,6 +134,30 @@ const onboardingInput = z.strictObject({
98
134
  reworkBehavior: z.enum(['preserve', 'redesign']).optional(),
99
135
  });
100
136
  export function registerOnboardingTools(server, service) {
137
+ server.registerTool('memory.source_targets', {
138
+ description: 'Read bounded Git ref candidates without assuming environment roles from their names. Confirm only the chosen committed sources with native memory.sync_start questionnaires; an explicit ref can select a source outside this preview.',
139
+ inputSchema: z.object({ repoRoot: z.string().optional() }),
140
+ }, async (input) => output(await service.memorySourceTargets(input)));
141
+ server.registerTool('memory.sources', {
142
+ description: 'Page completed immutable source memory snapshots for an accessible project, optionally filtered by repository fingerprint and treeHash. Use commit/tree identities for task context or an explicit baseSnapshotId for source inspection; never infer applicability from branch labels.',
143
+ inputSchema: z.object({
144
+ projectId: uuid,
145
+ afterId: uuid.optional(),
146
+ repoFingerprint: z
147
+ .string()
148
+ .regex(/^[a-f0-9]{64}$/)
149
+ .optional(),
150
+ treeHash: z
151
+ .string()
152
+ .regex(/^([a-f0-9]{40}|[a-f0-9]{64})$/)
153
+ .optional(),
154
+ limit: z.number().int().min(1).max(100).optional(),
155
+ }),
156
+ }, async (input) => output(await service.onboardingRequest('sources', body(input))));
157
+ server.registerTool('memory.publish_task', {
158
+ description: 'After committing a closed verified task, compare the actual committed tree with its local verification receipt and publish the task memory for that resulting source. A changed base, extra files, missing files or different blobs require memory.sync_start instead.',
159
+ inputSchema: z.object({ repoRoot: z.string().optional(), taskId: uuid }),
160
+ }, async (input) => output(await service.memoryPublishTask(input)));
101
161
  server.registerTool('project.inspect', {
102
162
  description: 'Read-only maturity evidence for missing, empty, starter or established projects. Inspect architecture, state, services, network and persistence as well as screens; recommendations never override the user. Follow with a native mode questionnaire.',
103
163
  inputSchema: z.object({ repoRoot }),
@@ -197,13 +257,18 @@ export function registerOnboardingTools(server, service) {
197
257
  }),
198
258
  }, async (input) => output(await service.memorySource(input)));
199
259
  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.',
260
+ 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
261
  inputSchema: z.object({
202
262
  ...project,
203
263
  repoRoot,
204
264
  requestKey: z.string().regex(/^[a-zA-Z0-9_-]{1,120}$/),
205
265
  mode: z.enum(['adopt', 'refresh', 'rework']),
266
+ language: z
267
+ .enum(['tr', 'en'])
268
+ .optional()
269
+ .describe('Use the current conversation language: tr for Turkish, en for English. Omitted by legacy callers means English.'),
206
270
  ref: z.string().optional(),
271
+ baseSnapshotId: uuid.optional(),
207
272
  previousRunId: uuid.optional(),
208
273
  reworkBehavior: z.enum(['preserve', 'redesign']).optional(),
209
274
  }),
@@ -226,24 +291,51 @@ export function registerOnboardingTools(server, service) {
226
291
  recovery: 'memory.sync_status',
227
292
  },
228
293
  });
229
- const pinned = { ...input, ref: commit };
294
+ const { language = 'en', ...request } = input;
295
+ const pinned = { ...request, ref: commit };
230
296
  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);
297
+ const reference = input.ref ?? 'HEAD';
298
+ const sourceLabel = /^[0-9a-f]{40,64}$/i.test(reference)
299
+ ? commit.slice(0, 12)
300
+ : `${reference.slice(0, 80)} · ${commit.slice(0, 12)}`;
301
+ const definitions = ['tr', 'en'].map((displayLanguage) => {
302
+ const copy = inspectionCopy[displayLanguage];
303
+ return {
304
+ questionnaireId: 'sync-start-' + digest,
305
+ language: displayLanguage,
306
+ message: [
307
+ copy.modes[input.mode],
308
+ `${copy.source}: ${sourceLabel}`,
309
+ copy.scope,
310
+ copy.localChanges,
311
+ copy.review,
312
+ ...(input.reworkBehavior ? [copy.rework[input.reworkBehavior]] : []),
313
+ ].join('\n\n'),
314
+ options: [
315
+ { id: 'approve', label: copy.start },
316
+ { id: 'defer', label: copy.defer },
317
+ ],
318
+ };
319
+ });
320
+ const definition = definitions.find((entry) => entry.language === language);
321
+ const question = await askQuestionnaire(server, service, { ...definition, repoRoot: input.repoRoot }, context, [
322
+ ...definitions,
323
+ {
324
+ questionnaireId: 'sync-start-' + digest,
325
+ message: 'Start ' +
326
+ input.mode +
327
+ ' for committed source ' +
328
+ commit +
329
+ '? Inspect architecture, state, services, network, data, screens and flows; dirty changes stay local. ' +
330
+ (input.reworkBehavior ? 'Rework target: ' + input.reworkBehavior + '. ' : '') +
331
+ 'Exact request: ' +
332
+ digest,
333
+ options: [
334
+ { id: 'approve', label: 'Start this exact inspection' },
335
+ { id: 'defer', label: 'Keep inspection unstarted' },
336
+ ],
337
+ },
338
+ ]);
247
339
  if (answerChoice(question) !== 'approve')
248
340
  return question;
249
341
  return output(await service.memorySyncStart({ ...pinned, questionnaireId: 'sync-start-' + digest }));
@@ -380,6 +472,7 @@ export function registerOnboardingTools(server, service) {
380
472
  inputSchema: z.object({
381
473
  ...project,
382
474
  ...page,
475
+ sessionId: uuid.optional(),
383
476
  includeArchived: z.boolean().optional(),
384
477
  includeLinked: z.boolean().optional(),
385
478
  kinds: z.array(kind).max(100).optional(),
@@ -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,35 @@ 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 presentationNotice = /^memory-batch-[a-f0-9]{64}-[0-9]+$/.test(record.questionnaireId)
57
+ ? '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.'
58
+ : undefined;
59
+ const copy = questionnaireCopy[record.language ?? 'en'];
56
60
  return inputRequired({
57
61
  inputRequests: {
58
62
  [record.requestKey]: inputRequired.elicit({
59
63
  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.' : ''}`,
64
+ message: `${record.message}${presentationNotice ? `\n\n${presentationNotice}` : ''}\n\n${copy.pending}${record.allowFreeText ? copy.freeText : ''}${record.textField ? copy.storedText : ''}`,
61
65
  requestedSchema: {
62
66
  type: 'object',
63
67
  properties: {
64
68
  choice: {
65
69
  type: 'string',
66
- title: 'Answer',
70
+ title: copy.answer,
67
71
  enum: [
68
72
  ...record.options.map((option) => option.id),
69
73
  ...(record.allowFreeText ? ['__other__'] : []),
70
74
  ],
71
75
  enumNames: [
72
76
  ...record.options.map((option) => option.label),
73
- ...(record.allowFreeText ? ['Other'] : []),
77
+ ...(record.allowFreeText ? [copy.other] : []),
74
78
  ],
75
79
  },
76
80
  ...(record.allowFreeText
77
81
  ? {
78
82
  text: {
79
83
  type: 'string',
80
- title: 'Other answer',
84
+ title: copy.otherAnswer,
81
85
  minLength: 1,
82
86
  maxLength: 2000,
83
87
  },
@@ -100,6 +104,24 @@ async function present(server, service, record, repoRoot, context, preparation)
100
104
  },
101
105
  });
102
106
  }
107
+ const questionnaireCopy = {
108
+ en: {
109
+ pending: 'Choose an answer to resolve this decision. Closing or cancelling the form leaves it pending.',
110
+ freeText: ' For another answer choose Other and fill in text. Free text is not stored and cannot be replayed.',
111
+ storedText: ' The bounded text field is stored as part of this exact decision and is replayed after restart.',
112
+ answer: 'Answer',
113
+ other: 'Other',
114
+ otherAnswer: 'Other answer',
115
+ },
116
+ tr: {
117
+ pending: 'Kararını kaydetmek için bir seçenek seç. Formu kapatmak veya iptal etmek onay sayılmaz; soru beklemede kalır.',
118
+ 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.',
119
+ storedText: ' Bu metin alanındaki yanıt kararınla birlikte saklanır; yeniden başlattığında korunur.',
120
+ answer: 'Yanıt',
121
+ other: 'Diğer',
122
+ otherAnswer: 'Diğer yanıt',
123
+ },
124
+ };
103
125
  function pending(record, reason) {
104
126
  return result({
105
127
  questionnaireId: record.questionnaireId,
@@ -74,6 +74,9 @@ export const engineeringMemoryToolNames = [
74
74
  'memory.sync_delta',
75
75
  'memory.sync_verify',
76
76
  'memory.catalog',
77
+ 'memory.sources',
78
+ 'memory.source_targets',
79
+ 'memory.publish_task',
77
80
  'memory.read_revisions',
78
81
  'memory.review_batch',
79
82
  'memory.query',
@@ -171,7 +174,7 @@ export function registerQuestionnaireTools(server, service) {
171
174
  }
172
175
  });
173
176
  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.',
177
+ 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
178
  inputSchema: questionnaireDefinitionSchema.extend({
176
179
  repoRoot: optionalRepoRoot,
177
180
  preparation: z.boolean().optional(),
@@ -248,6 +251,14 @@ export function registerEngineeringMemoryTools(server, service) {
248
251
  workItemKey: z.string().min(1).optional(),
249
252
  workItemId: z.string().uuid().optional(),
250
253
  mode: z.enum(['write', 'read_only', 'scaffold']).optional(),
254
+ linkedSources: z
255
+ .array(z.object({
256
+ projectId: z.string().uuid(),
257
+ sourceCommit: z.string().regex(/^([a-f0-9]{40}|[a-f0-9]{64})$/),
258
+ treeHash: z.string().regex(/^([a-f0-9]{40}|[a-f0-9]{64})$/),
259
+ }))
260
+ .max(20)
261
+ .optional(),
251
262
  knownRevisions: z.record(z.string(), z.number().int().min(0)).optional(),
252
263
  }),
253
264
  }, async (input) => toolResult(await service.sessionBootstrap(input)));
@@ -271,8 +282,9 @@ export function registerEngineeringMemoryTools(server, service) {
271
282
  }),
272
283
  }, async (input) => toolResult(await service.contextPrepareChange(input)));
273
284
  server.registerTool('context.refresh', {
274
- description: 'Invalidate stale revision pins after an approved memory change and require a new prepare-change lease.',
285
+ description: 'Refresh pins within the task fixed code source after memory approval. Use the task repoRoot; source changes require a new task.',
275
286
  inputSchema: z.object({
287
+ repoRoot: optionalRepoRoot,
276
288
  sessionId: z.string().min(1),
277
289
  }),
278
290
  }, async (input) => toolResult(await service.contextRefresh(input)));
@@ -302,6 +314,7 @@ export function registerEngineeringMemoryTools(server, service) {
302
314
  description: 'Create an inactive, reviewable proposal for a permanent project or organization engineering memory revision.',
303
315
  inputSchema: z.object({
304
316
  repoRoot: optionalRepoRoot,
317
+ sourceRunId: z.string().uuid().optional(),
305
318
  resourceId: z.string().optional(),
306
319
  projectId: z.string().min(1),
307
320
  taskId: z.string().optional(),
@@ -215,6 +215,14 @@ export class ActiveContextStore {
215
215
  await this.writePointer(next);
216
216
  });
217
217
  }
218
+ async findBySessionId(sessionId) {
219
+ if (!isUuid(sessionId))
220
+ throw new Error('Context session identifier is invalid');
221
+ const matches = (await this.allPointers()).filter((pointer) => pointer.sessionId === sessionId);
222
+ if (matches.length > 1)
223
+ throw new Error('Context session has more than one active task pointer');
224
+ return matches[0] ?? null;
225
+ }
218
226
  async findByTaskId(taskId, projectId, requireClean = true) {
219
227
  const matches = (await this.allPointers()).filter((pointer) => pointer.taskId === taskId && pointer.projectId === projectId);
220
228
  if (matches.length !== 1 ||
@@ -21,6 +21,8 @@ export const backendRecoveryOperationNames = [
21
21
  'context.refresh',
22
22
  'context.prepare_change',
23
23
  'memory.sync_status',
24
+ 'memory.sync_plan',
25
+ 'memory.sync_start',
24
26
  'project.onboarding_status',
25
27
  'memory.query',
26
28
  'memory.catalog',
@@ -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);
@@ -183,6 +183,9 @@ export class BridgeService {
183
183
  throw refuse('This repository has no selected project. Select its project before bootstrapping.', 'project.resolve');
184
184
  }
185
185
  const bootstrapMode = persistedBootstrap.mode ?? 'write';
186
+ const source = await this.checkoutSource(repository);
187
+ if (source && input.linkedSources)
188
+ source.linked = input.linkedSources;
186
189
  const decision = bootstrapMode === 'read_only'
187
190
  ? null
188
191
  : await this.requireBranchDecision(repository, input.externalTaskId);
@@ -194,6 +197,7 @@ export class BridgeService {
194
197
  body: cleanJson({
195
198
  projectId,
196
199
  externalTaskId: persistedBootstrap.externalTaskId,
200
+ source,
197
201
  objective: persistedBootstrap.objective,
198
202
  taskKind: persistedBootstrap.taskKind,
199
203
  workItemKey: persistedBootstrap.workItemKey,
@@ -234,6 +238,7 @@ export class BridgeService {
234
238
  projectId,
235
239
  taskId: task.id,
236
240
  taskSlug: input.externalTaskId,
241
+ source: sourceFromContext(task.sourceContext),
237
242
  worktreeId: sha256(repository.repoRoot),
238
243
  ...(decision
239
244
  ? { branch: typeof task.branch === 'string' ? task.branch : decision.branch }
@@ -370,15 +375,17 @@ export class BridgeService {
370
375
  const recoveredDeliveries = await this.recoverJournalOutbox(projectId, taskSlug);
371
376
  const outbox = await this.flushOutbox(pointer?.taskId);
372
377
  const afterSequence = input.afterSequence ?? 0;
378
+ const source = await this.checkoutSource(repository, pointer?.source);
373
379
  const response = await this.dependencies.client.request(endpoints.sessionResume, {
374
380
  method: 'POST',
375
381
  cacheKey: resumeCacheKey(sessionId, repository.repoFingerprint, afterSequence),
376
382
  allowStaleOnUnavailable: true,
377
- body: {
383
+ body: cleanJson({
378
384
  sessionId,
379
385
  repoFingerprint: repository.repoFingerprint,
380
386
  afterSequence,
381
- },
387
+ source,
388
+ }),
382
389
  });
383
390
  const backend = objectValue(response.data);
384
391
  const backendTask = objectValue(backend?.task);
@@ -506,6 +513,7 @@ export class BridgeService {
506
513
  projectId,
507
514
  taskId: backendTask.id,
508
515
  taskSlug,
516
+ source: sourceFromContext(backendTask.sourceContext),
509
517
  worktreeId: pointer?.worktreeId ?? sha256(repository.repoRoot),
510
518
  ...(typeof backendTask.branch === 'string'
511
519
  ? { branch: backendTask.branch }
@@ -610,6 +618,7 @@ export class BridgeService {
610
618
  sessionId: input.sessionId,
611
619
  changedPaths,
612
620
  baselineDiffHash,
621
+ source: await this.checkoutSource(repository, pointer?.source),
613
622
  ...(decision ? { branch: decision.branch } : {}),
614
623
  ...(input.transitionToWrite
615
624
  ? { transitionToWrite: true, expectedTaskVersion: transitionTaskVersion }
@@ -787,9 +796,24 @@ export class BridgeService {
787
796
  }
788
797
  async contextRefresh(input) {
789
798
  return await this.execute(async () => {
799
+ const pointer = await this.dependencies.activeContexts.findBySessionId(input.sessionId);
800
+ const repoRoot = input.repoRoot;
801
+ if (!repoRoot && pointer?.source)
802
+ throw refuse('Pass repoRoot to context.refresh for this source-aware session.', 'context.refresh');
803
+ const repository = repoRoot
804
+ ? await this.dependencies.repositories.resolve(repoRoot, pointer?.projectId)
805
+ : null;
806
+ if (pointer &&
807
+ repository &&
808
+ (repository.repoFingerprint !== pointer.repoFingerprint ||
809
+ (pointer.worktreeId && pointer.worktreeId !== sha256(repository.repoRoot))))
810
+ throw refuse('Refresh this session from its own repository and worktree.', 'session.resume');
811
+ const source = repository
812
+ ? await this.checkoutSource(repository, pointer?.source)
813
+ : undefined;
790
814
  const response = await this.dependencies.client.request(endpoints.contextRefresh, {
791
815
  method: 'POST',
792
- body: { sessionId: input.sessionId },
816
+ body: cleanJson({ sessionId: input.sessionId, source }),
793
817
  });
794
818
  await this.dependencies.activeContexts.clearSessionConflict(input.sessionId, 'approved_memory_revision_requires_refresh');
795
819
  return asJsonValue({
@@ -981,6 +1005,8 @@ export class BridgeService {
981
1005
  reworkBehavior: input.reworkBehavior,
982
1006
  sourceCommit: snapshot.commit,
983
1007
  treeHash: snapshot.treeHash,
1008
+ sourceBound: true,
1009
+ baseSnapshotId: input.baseSnapshotId,
984
1010
  manifestHash: snapshot.manifestHash,
985
1011
  expectedFiles: snapshot.files.length,
986
1012
  });
@@ -1076,10 +1102,30 @@ export class BridgeService {
1076
1102
  }),
1077
1103
  };
1078
1104
  }
1105
+ async memorySourceTargets(input) {
1106
+ return this.execute(async () => {
1107
+ const repository = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd());
1108
+ return asJsonValue(await this.dependencies.repositories.git.sourceCandidates(repository.repoRoot));
1109
+ });
1110
+ }
1111
+ async memoryPublishTask(input) {
1112
+ return this.execute(async () => {
1113
+ let proof;
1114
+ try {
1115
+ proof = await this.dependencies.gate.publicationProof(input.repoRoot ?? process.cwd(), input.taskId);
1116
+ }
1117
+ catch (error) {
1118
+ throw refuse(error instanceof Error ? error.message : 'The resulting commit must be inspected.', 'memory.sync_start');
1119
+ }
1120
+ const response = await this.dependencies.client.request('/memory/sources/publish-task', { method: 'POST', body: proof });
1121
+ return response.data;
1122
+ });
1123
+ }
1079
1124
  async onboardingRequest(operation, body) {
1080
1125
  return this.execute(async () => {
1081
1126
  assertSafeToPersist(body);
1082
1127
  const routes = {
1128
+ sources: '/memory/sources/catalog',
1083
1129
  status: '/memory/sync/status',
1084
1130
  inventory: '/memory/sync/inventory',
1085
1131
  plan: '/memory/sync/plan',
@@ -1347,7 +1393,10 @@ export class BridgeService {
1347
1393
  const pointer = await this.requireActivePointer(repository.repoFingerprint, taskId);
1348
1394
  const refreshed = await this.dependencies.client.request(endpoints.contextRefresh, {
1349
1395
  method: 'POST',
1350
- body: { sessionId: pointer.sessionId },
1396
+ body: cleanJson({
1397
+ sessionId: pointer.sessionId,
1398
+ source: await this.checkoutSource(repository, pointer.source),
1399
+ }),
1351
1400
  });
1352
1401
  const refreshData = objectValue(refreshed.data);
1353
1402
  const refreshedTask = objectValue(refreshData?.task);
@@ -1419,6 +1468,7 @@ export class BridgeService {
1419
1468
  body: {
1420
1469
  sessionId: input.sessionId,
1421
1470
  repoFingerprint: repository.repoFingerprint,
1471
+ source: (await this.checkoutSource(repository, pointer.source)) ?? null,
1422
1472
  afterSequence: 0,
1423
1473
  },
1424
1474
  });
@@ -1487,6 +1537,7 @@ export class BridgeService {
1487
1537
  ? []
1488
1538
  : await buildPathChangeManifest(repository.repoRoot, taskChanges);
1489
1539
  const verifyBody = cleanJson({
1540
+ source: await this.checkoutSource(repository, pointer.source),
1490
1541
  taskId: input.taskId,
1491
1542
  sessionId: input.sessionId,
1492
1543
  expectedTaskVersion: numericTaskVersion(snapshotTask.lockVersion),
@@ -1532,6 +1583,7 @@ export class BridgeService {
1532
1583
  if (responseData?.verified === true) {
1533
1584
  const taskVersion = numericTaskVersion(responseData.taskVersion);
1534
1585
  await this.dependencies.gate.record({
1586
+ source: sourceFromContext(snapshotTask.sourceContext),
1535
1587
  taskId: input.taskId,
1536
1588
  projectId: repository.projectId,
1537
1589
  repoFingerprint: repository.repoFingerprint,
@@ -1572,6 +1624,7 @@ export class BridgeService {
1572
1624
  body: {
1573
1625
  sessionId: pointer.sessionId,
1574
1626
  repoFingerprint: repository.repoFingerprint,
1627
+ source: (await this.checkoutSource(repository, pointer.source)) ?? null,
1575
1628
  afterSequence: 0,
1576
1629
  },
1577
1630
  });
@@ -1585,6 +1638,7 @@ export class BridgeService {
1585
1638
  : [];
1586
1639
  await this.assertTaskWorktree(repository, task, pointer);
1587
1640
  const closeBody = {
1641
+ source: (await this.checkoutSource(repository, pointer.source)) ?? null,
1588
1642
  taskId: input.taskId,
1589
1643
  sessionId: pointer.sessionId,
1590
1644
  expectedTaskVersion: numericTaskVersion(task.lockVersion),
@@ -2550,6 +2604,21 @@ export class BridgeService {
2550
2604
  }
2551
2605
  return pending.length;
2552
2606
  }
2607
+ async checkoutSource(repository, expected) {
2608
+ const current = await this.dependencies.repositories.git.sourceIdentity(repository.repoRoot);
2609
+ if (expected &&
2610
+ (!current ||
2611
+ current.sourceCommit !== expected.sourceCommit ||
2612
+ current.treeHash !== expected.treeHash)) {
2613
+ throw refuse('The checkout no longer matches this task source. Return to its commit or bootstrap a new task for the current committed source.', 'session.bootstrap');
2614
+ }
2615
+ if (current && repository.git.head !== current.sourceCommit) {
2616
+ throw refuse('The Git source changed during the request. Retry session.resume with a stable checkout.', 'session.resume');
2617
+ }
2618
+ return current
2619
+ ? { ...current, ...(expected?.linked ? { linked: expected.linked } : {}) }
2620
+ : undefined;
2621
+ }
2553
2622
  async activeTaskSnapshot(taskId, projectId, repoRoot) {
2554
2623
  const repository = await this.dependencies.repositories.resolve(repoRoot ?? process.cwd(), projectId);
2555
2624
  const pointer = await this.requireActivePointer(repository.repoFingerprint, taskId);
@@ -2565,6 +2634,7 @@ export class BridgeService {
2565
2634
  body: {
2566
2635
  sessionId: pointer.sessionId,
2567
2636
  repoFingerprint: repository.repoFingerprint,
2637
+ source: (await this.checkoutSource(repository, pointer.source)) ?? null,
2568
2638
  afterSequence: 0,
2569
2639
  },
2570
2640
  });
@@ -2625,6 +2695,7 @@ export class BridgeService {
2625
2695
  }
2626
2696
  }
2627
2697
  async retryVerificationRecovery(repository, pointer, backend) {
2698
+ await this.checkoutSource(repository, pointer.source);
2628
2699
  const intent = pointer.verificationIntent;
2629
2700
  const recovery = objectValue(backend?.verificationRecovery);
2630
2701
  if (!intent) {
@@ -2717,6 +2788,7 @@ export class BridgeService {
2717
2788
  }
2718
2789
  const taskVersion = numericTaskVersion(responseData.taskVersion);
2719
2790
  await this.dependencies.gate.record({
2791
+ source: sourceFromContext(body.source),
2720
2792
  taskId: pointer.taskId,
2721
2793
  projectId: pointer.projectId,
2722
2794
  repoFingerprint: pointer.repoFingerprint,
@@ -2750,6 +2822,7 @@ export class BridgeService {
2750
2822
  return await this.deliverCloseIntent(repository, body, intent.taskChanges, true);
2751
2823
  }
2752
2824
  async deliverCloseIntent(repository, body, taskChanges, recoveredAfterResponseLoss) {
2825
+ await this.checkoutSource(repository, sourceFromContext(body.source));
2753
2826
  const touchedContract = taskChanges.some((change) => /\.controller\.[tj]s$/.test(change.path));
2754
2827
  const currentBranch = await this.dependencies.repositories.git.currentBranch(repository.repoRoot);
2755
2828
  if (typeof body.branch === 'string' && body.branch !== currentBranch)
@@ -2774,6 +2847,7 @@ export class BridgeService {
2774
2847
  }
2775
2848
  const closedTaskVersion = numericTaskVersion(closedTask.taskVersion);
2776
2849
  await this.dependencies.gate.record({
2850
+ source: sourceFromContext(body.source),
2777
2851
  taskId: String(body.taskId),
2778
2852
  projectId: repository.projectId,
2779
2853
  repoFingerprint: repository.repoFingerprint,
@@ -3482,4 +3556,29 @@ function assertValidationEvidence(validations) {
3482
3556
  seen.add(validation.validationId);
3483
3557
  }
3484
3558
  }
3559
+ function sourceFromContext(value) {
3560
+ const context = objectValue(value);
3561
+ if (!context || typeof context.sourceCommit !== 'string' || typeof context.treeHash !== 'string')
3562
+ return undefined;
3563
+ const linked = Array.isArray(context.linked)
3564
+ ? context.linked.map((entry) => {
3565
+ const item = objectValue(entry);
3566
+ if (!item ||
3567
+ typeof item.projectId !== 'string' ||
3568
+ typeof item.sourceCommit !== 'string' ||
3569
+ typeof item.treeHash !== 'string')
3570
+ throw refuse('The backend source context is incomplete. Resume this task.', 'session.resume');
3571
+ return {
3572
+ projectId: item.projectId,
3573
+ sourceCommit: item.sourceCommit,
3574
+ treeHash: item.treeHash,
3575
+ };
3576
+ })
3577
+ : undefined;
3578
+ return {
3579
+ sourceCommit: context.sourceCommit,
3580
+ treeHash: context.treeHash,
3581
+ ...(linked ? { linked } : {}),
3582
+ };
3583
+ }
3485
3584
  //# sourceMappingURL=bridge-service.js.map
@@ -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,
@@ -209,6 +209,40 @@ Closing is not the end of the turn either. `task.close` returns the delivery que
209
209
 
210
210
  When a pull request has been opened, keep going: check whether it merges cleanly, report the result with the link, and if it conflicts, name the files and ask whether to resolve them. Never end the turn on a pull request whose mergeability was never checked, and never resolve a conflict without being told to.
211
211
 
212
- ## Offline Behavior
212
+ ## Code source and memory applicability
213
+
214
+ The bridge sends the checkout's committed Git commit and tree at bootstrap. Task and session
215
+ sourceContext distinguish exact inspected memory, unknown source and legacy-unverified project
216
+ memory. Branch names are labels; never infer an environment or choose newer project behavior from
217
+ their spelling. Check the returned coverage before describing project behavior as known.
218
+
219
+ On an opted-in project, an unknown source can open a read-only learning session but cannot start
220
+ implementation. Use memory.source_targets and memory.sources, explain the source to the user and
221
+ start its inspection through the native memory.sync_start form. After completion, context.refresh
222
+ can resolve that same previously unknown code state. A different commit requires a new task and
223
+ source selection; changing a branch name does not make memory compatible. Use the task repoRoot
224
+ for refresh as well as resume and prepare_change.
225
+
226
+ Ordinary memory reads, history, selectors, discovery and verification use the task's pinned source
227
+ mapping. Project proposals made with taskId form that task's approved overlay; they do not alter
228
+ its base commit or another task. Product and organization policy and all membership checks remain
229
+ current. Select linkedSources explicitly by accessible project id, commit and tree at bootstrap;
230
+ do not guess the counterpart's source from this repository's branch. Unselected or unknown linked
231
+ sources are not verified endpoint contracts.
232
+
233
+ After the already-authorized commit is created, call memory.publish_task with the closed task id
234
+ and repoRoot. The bridge proves the resulting commit has the task's base parent and exactly its
235
+ verified files, blobs, modes and deletions. Backend publication rechecks the task, current access
236
+ and immutable source identity and atomically saves the approved overlay for the resulting commit.
237
+ It never publishes dirty worktree contents. Repeating the same result is idempotent. If merging,
238
+ squashing, rebasing or extra changes prevent that proof, inspect the actual resulting source with
239
+ memory.sync_start and an explicit baseSnapshotId; never mark it compatible from ancestry alone.
240
+
241
+ An interrupted verify/close retains the original source in its durable intent. Resume the same
242
+ source; do not change the stored request to fit a different checkout. Old receipts still require
243
+ online attestation and cannot bypass a project's source-aware requirement. Global minimum client
244
+ version is not automatically raised; projects opt in by starting a source-bound inspection.
245
+
246
+ ## Offline source context
213
247
 
214
248
  An unexpired cached context pack may be used for implementation. The bridge records checkpoints in its outbox. Permanent proposal review, strict verification, task close, and Git commit require the backend and synchronization of the relevant task deliveries. An unrelated task or repository queue does not block this task; account switching and logout still require all deliveries to be resolved. Commit receipts are retained per task and worktree and still require an online attestation for the selected closed task and exact staged content.
@@ -56,6 +56,55 @@ unavailable service contracts explicit rather than inventing endpoint behavior.
56
56
 
57
57
  ## Durable deep learning
58
58
 
59
+ Before choosing learning targets, use memory.source_targets for a bounded preview of local,
60
+ remote-tracking and tag refs, and memory.sources to page this project's completed source snapshots.
61
+ The preview groups aliases of the same commit and says when it is truncated. Ref names do not
62
+ establish deployment meaning; the user need not classify them as develop/test/prod. Explain the
63
+ checked-out source and other plausible targets, then use the native memory.sync_start confirmation
64
+ for each source the user chose. Never scan every historical branch automatically. An explicit ref
65
+ can choose a source outside the preview. The form pins its resolved commit even if a branch moves.
66
+
67
+ New inspections are source-bound. Supply sourceRunId on their project proposals, with complete
68
+ selectors and provenance. Approval updates only that target's proposed view; sync completion is
69
+ what publishes the immutable mapping. Starting an inventory does not activate source enforcement.
70
+ The first completed source-bound snapshot activates it for new tasks; already-open legacy tasks
71
+ keep their original context through verification, close and commit. Every active prior project
72
+ record must be retained with evidence or explicitly archived for the inspected source.
73
+ On a first capture there is no base snapshot for unchanged references. Propose reviewed
74
+ source-specific revisions retaining the existing content or archiving the record, using sourceRunId,
75
+ and submit them as unit evidence. Core records must be retained. With a base snapshot, unchanged
76
+ references are also available. A grandfathered task can still approve unversioned memory, but its
77
+ sourceApplicabilityNotice explains that this revision will reach no source-aware task until an
78
+ inspection explicitly retains it; an incremental inspection must include it deliberately too.
79
+ Select baseSnapshotId explicitly when carrying inspected
80
+ memory across a commit change. Reuse requires unchanged source and dependency evidence; it is not
81
+ permission to copy behavior merely because one commit is an ancestor. Existing unversioned records
82
+ remain historical material, never automatic proof for a newly selected source. No-change refreshes
83
+ reuse revisions without manufacturing additional revision history.
84
+
85
+ The source snapshot includes historical path/symbol/tag/stack selectors and archival state as well
86
+ as revision ids. Thus learning a newer source cannot change which records an older task receives.
87
+ Current access and security policy still apply. Missing coverage stays explicit and leads back to
88
+ inspection rather than substituting the project's latest behavior. The same complete tree in the
89
+ same repository/project reuses its inspected mapping even after squash, rebase or a merge only when
90
+ the candidate mapping hashes agree. Conflicting mappings require comparing memory.sources and
91
+ starting memory.sync_start for the exact commit with a selected baseSnapshotId; never silently pick
92
+ the newest fallback. A task that already has an exact snapshot keeps that pin. The task
93
+ still pins its actual commit. Different content is not covered by ancestry alone: start a delta
94
+ inspection with previousRunId and baseSnapshotId, preserving evidenced unchanged units and reviewing
95
+ changed/dependent ones. After a verified task closes and its result is committed, memory.publish_task
96
+ checks its full result tree against the task manifest; the number or shape of commit parents does
97
+ not decide publication. Extra or missing changed content requires inspection instead.
98
+ This proof establishes file content, not who authored it or which intermediate commits produced it.
99
+ Git inspection is performed by the local bridge. The backend does not fetch or execute Git: it
100
+ trusts the authenticated client's source identity assertion and independently enforces task ownership,
101
+ verified manifest identity, publication scope and current access. Do not describe that boundary as
102
+ server-side Git verification.
103
+
104
+ Use the task's explicit repoRoot with context.refresh. It never guesses the repository from the
105
+ bridge process directory. An unknown selected linked commit can become exact after that same source
106
+ is inspected; refresh keeps the selected identities and existing exact snapshots fixed.
107
+
59
108
  Adopt, refresh and the learning stage of rework do not modify application source. They inventory and
60
109
  learn the whole architecture: modules, service classes, network and API contracts, state management,
61
110
  persistence/data models/migrations, navigation, screens/components, localization, deployment,
@@ -144,3 +193,45 @@ gates. Cross-project phases use existing work_item.plan/confirm_plan with explic
144
193
  and independent per-repository runs. Planning tasks does not perform their implementation.
145
194
 
146
195
  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.
196
+
197
+ ## Inspection confirmation language
198
+
199
+ For memory.sync_start, pass language: tr in a Turkish conversation and language: en in an English
200
+ conversation. Use the conversation language, never the computer locale. Legacy calls without language
201
+ use English. The form shows a short source reference, scope, the treatment of uncommitted changes and
202
+ the separate approval needed for permanent memory. Full commit and request hashes stay in the durable
203
+ approval binding, not in the visible question. Generic questionnaire.ask messages and option labels
204
+ are written by the agent in the same language; pass language for the native help and field labels.
205
+
206
+ An existing exact-request questionnaire retains its original wording and answer across updates and
207
+ restart, including an older English form. Resume that question rather than silently replacing or
208
+ withdrawing it. Language changes never broaden the approved source, mode or previous-run scope.
209
+
210
+ ## Source-specific refresh and history
211
+
212
+ During refresh an existing own or optional linked unknown may become exact if unambiguous;
213
+ conflicting mappings leave it unknown without blocking the existing read-only reader task.
214
+ Exact snapshots remain pinned. Bootstrap still refuses an ambiguous requested source. Never
215
+ substitute another source's records. Use memory.sources with projectId, repoFingerprint and
216
+ treeHash to compare candidates, then inspect in that project's checkout with contributor access.
217
+ A new task may omit an optional linked selection; this cannot change an existing task's identity.
218
+
219
+ Retry memory.sync_verify after a lost response: a completed run returns its stored result and
220
+ snapshot even if memory changed later. New work requires a new run. Source-aware project history
221
+ contains the effective selected revision and current-task reconciliations. The depth argument
222
+ bounds shared/legacy history; it does not widen a source-bound project's single selected revision.
223
+ Headers include the total revision count for each authorized selected resource so the history-read
224
+ gate still applies to rules, profiles and quality records. This count does not authorize reading
225
+ unrelated branch revisions, reasons, objectives or reconciliations.
226
+
227
+ Present sourceApplicabilityNotice from memory.list_proposals before permanent approval.
228
+ The native exact-batch form repeats a conditional legacy-only advisory on both ask and resume,
229
+ including restart on a host that supports native forms. It does not change saved messages,
230
+ request keys, digests or answers. An unsupported host leaves the decision pending. The advisory
231
+ is not a claim that every proposal in the batch is unversioned; inspect its actual notices.
232
+
233
+ Current own-project authority, discipline, repository, branch, source, task version and diff remain
234
+ required for verified delivery. Verified resume, close and commit attestation do not fetch fresh
235
+ counterpart memory or depend on a counterpart link/membership that was revoked after verification.
236
+ Fresh linked-source selection and work still check current access. Publishing new source memory
237
+ with memory.publish_task is a separate operation and still requires current linked authority.
@@ -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.