engineering-memory 1.10.5 → 1.11.1

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.5",
3
+ "version": "1.11.1",
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 });
@@ -134,6 +134,30 @@ const onboardingInput = z.strictObject({
134
134
  reworkBehavior: z.enum(['preserve', 'redesign']).optional(),
135
135
  });
136
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)));
137
161
  server.registerTool('project.inspect', {
138
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.',
139
163
  inputSchema: z.object({ repoRoot }),
@@ -244,6 +268,7 @@ export function registerOnboardingTools(server, service) {
244
268
  .optional()
245
269
  .describe('Use the current conversation language: tr for Turkish, en for English. Omitted by legacy callers means English.'),
246
270
  ref: z.string().optional(),
271
+ baseSnapshotId: uuid.optional(),
247
272
  previousRunId: uuid.optional(),
248
273
  reworkBehavior: z.enum(['preserve', 'redesign']).optional(),
249
274
  }),
@@ -447,6 +472,7 @@ export function registerOnboardingTools(server, service) {
447
472
  inputSchema: z.object({
448
473
  ...project,
449
474
  ...page,
475
+ sessionId: uuid.optional(),
450
476
  includeArchived: z.boolean().optional(),
451
477
  includeLinked: z.boolean().optional(),
452
478
  kinds: z.array(kind).max(100).optional(),
@@ -53,12 +53,15 @@ 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;
56
59
  const copy = questionnaireCopy[record.language ?? 'en'];
57
60
  return inputRequired({
58
61
  inputRequests: {
59
62
  [record.requestKey]: inputRequired.elicit({
60
63
  mode: 'form',
61
- message: `${record.message}\n\n${copy.pending}${record.allowFreeText ? copy.freeText : ''}${record.textField ? copy.storedText : ''}`,
64
+ message: `${record.message}${presentationNotice ? `\n\n${presentationNotice}` : ''}\n\n${copy.pending}${record.allowFreeText ? copy.freeText : ''}${record.textField ? copy.storedText : ''}`,
62
65
  requestedSchema: {
63
66
  type: 'object',
64
67
  properties: {
@@ -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',
@@ -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',
@@ -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
  });
@@ -993,16 +1019,26 @@ export class BridgeService {
993
1019
  if (!run || typeof run.id !== 'string')
994
1020
  throw refuse('The inspection response has no run identity.', 'memory.sync_status');
995
1021
  if (run.state === 'inventory' || run.state === 'planning') {
996
- for (let offset = 0; offset < snapshot.files.length; offset += 1000) {
997
- const page = cleanJson({
1022
+ for (let offset = 0; offset < snapshot.files.length;) {
1023
+ const page = {
998
1024
  projectId: input.projectId,
999
1025
  runId: run.id,
1000
- files: snapshot.files.slice(offset, offset + 1000),
1001
- });
1002
- assertSafeToPersist(page);
1026
+ files: [],
1027
+ };
1028
+ let bytes = Buffer.byteLength(JSON.stringify(page));
1029
+ while (offset < snapshot.files.length && page.files.length < 1000) {
1030
+ const file = snapshot.files[offset];
1031
+ const addedBytes = Buffer.byteLength(JSON.stringify(file)) + (page.files.length ? 1 : 0);
1032
+ if (page.files.length && bytes + addedBytes > 64 * 1024)
1033
+ break;
1034
+ page.files.push(file);
1035
+ bytes += addedBytes;
1036
+ offset += 1;
1037
+ }
1038
+ assertSafeToPersist(cleanJson(page));
1003
1039
  await this.dependencies.client.request('/memory/sync/inventory', {
1004
1040
  method: 'POST',
1005
- body: page,
1041
+ body: cleanJson(page),
1006
1042
  });
1007
1043
  }
1008
1044
  await this.dependencies.client.request('/memory/sync/inventory', {
@@ -1076,10 +1112,30 @@ export class BridgeService {
1076
1112
  }),
1077
1113
  };
1078
1114
  }
1115
+ async memorySourceTargets(input) {
1116
+ return this.execute(async () => {
1117
+ const repository = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd());
1118
+ return asJsonValue(await this.dependencies.repositories.git.sourceCandidates(repository.repoRoot));
1119
+ });
1120
+ }
1121
+ async memoryPublishTask(input) {
1122
+ return this.execute(async () => {
1123
+ let proof;
1124
+ try {
1125
+ proof = await this.dependencies.gate.publicationProof(input.repoRoot ?? process.cwd(), input.taskId);
1126
+ }
1127
+ catch (error) {
1128
+ throw refuse(error instanceof Error ? error.message : 'The resulting commit must be inspected.', 'memory.sync_start');
1129
+ }
1130
+ const response = await this.dependencies.client.request('/memory/sources/publish-task', { method: 'POST', body: proof });
1131
+ return response.data;
1132
+ });
1133
+ }
1079
1134
  async onboardingRequest(operation, body) {
1080
1135
  return this.execute(async () => {
1081
1136
  assertSafeToPersist(body);
1082
1137
  const routes = {
1138
+ sources: '/memory/sources/catalog',
1083
1139
  status: '/memory/sync/status',
1084
1140
  inventory: '/memory/sync/inventory',
1085
1141
  plan: '/memory/sync/plan',
@@ -1347,7 +1403,10 @@ export class BridgeService {
1347
1403
  const pointer = await this.requireActivePointer(repository.repoFingerprint, taskId);
1348
1404
  const refreshed = await this.dependencies.client.request(endpoints.contextRefresh, {
1349
1405
  method: 'POST',
1350
- body: { sessionId: pointer.sessionId },
1406
+ body: cleanJson({
1407
+ sessionId: pointer.sessionId,
1408
+ source: await this.checkoutSource(repository, pointer.source),
1409
+ }),
1351
1410
  });
1352
1411
  const refreshData = objectValue(refreshed.data);
1353
1412
  const refreshedTask = objectValue(refreshData?.task);
@@ -1419,6 +1478,7 @@ export class BridgeService {
1419
1478
  body: {
1420
1479
  sessionId: input.sessionId,
1421
1480
  repoFingerprint: repository.repoFingerprint,
1481
+ source: (await this.checkoutSource(repository, pointer.source)) ?? null,
1422
1482
  afterSequence: 0,
1423
1483
  },
1424
1484
  });
@@ -1487,6 +1547,7 @@ export class BridgeService {
1487
1547
  ? []
1488
1548
  : await buildPathChangeManifest(repository.repoRoot, taskChanges);
1489
1549
  const verifyBody = cleanJson({
1550
+ source: await this.checkoutSource(repository, pointer.source),
1490
1551
  taskId: input.taskId,
1491
1552
  sessionId: input.sessionId,
1492
1553
  expectedTaskVersion: numericTaskVersion(snapshotTask.lockVersion),
@@ -1532,6 +1593,7 @@ export class BridgeService {
1532
1593
  if (responseData?.verified === true) {
1533
1594
  const taskVersion = numericTaskVersion(responseData.taskVersion);
1534
1595
  await this.dependencies.gate.record({
1596
+ source: sourceFromContext(snapshotTask.sourceContext),
1535
1597
  taskId: input.taskId,
1536
1598
  projectId: repository.projectId,
1537
1599
  repoFingerprint: repository.repoFingerprint,
@@ -1572,6 +1634,7 @@ export class BridgeService {
1572
1634
  body: {
1573
1635
  sessionId: pointer.sessionId,
1574
1636
  repoFingerprint: repository.repoFingerprint,
1637
+ source: (await this.checkoutSource(repository, pointer.source)) ?? null,
1575
1638
  afterSequence: 0,
1576
1639
  },
1577
1640
  });
@@ -1585,6 +1648,7 @@ export class BridgeService {
1585
1648
  : [];
1586
1649
  await this.assertTaskWorktree(repository, task, pointer);
1587
1650
  const closeBody = {
1651
+ source: (await this.checkoutSource(repository, pointer.source)) ?? null,
1588
1652
  taskId: input.taskId,
1589
1653
  sessionId: pointer.sessionId,
1590
1654
  expectedTaskVersion: numericTaskVersion(task.lockVersion),
@@ -2550,6 +2614,21 @@ export class BridgeService {
2550
2614
  }
2551
2615
  return pending.length;
2552
2616
  }
2617
+ async checkoutSource(repository, expected) {
2618
+ const current = await this.dependencies.repositories.git.sourceIdentity(repository.repoRoot);
2619
+ if (expected &&
2620
+ (!current ||
2621
+ current.sourceCommit !== expected.sourceCommit ||
2622
+ current.treeHash !== expected.treeHash)) {
2623
+ 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');
2624
+ }
2625
+ if (current && repository.git.head !== current.sourceCommit) {
2626
+ throw refuse('The Git source changed during the request. Retry session.resume with a stable checkout.', 'session.resume');
2627
+ }
2628
+ return current
2629
+ ? { ...current, ...(expected?.linked ? { linked: expected.linked } : {}) }
2630
+ : undefined;
2631
+ }
2553
2632
  async activeTaskSnapshot(taskId, projectId, repoRoot) {
2554
2633
  const repository = await this.dependencies.repositories.resolve(repoRoot ?? process.cwd(), projectId);
2555
2634
  const pointer = await this.requireActivePointer(repository.repoFingerprint, taskId);
@@ -2565,6 +2644,7 @@ export class BridgeService {
2565
2644
  body: {
2566
2645
  sessionId: pointer.sessionId,
2567
2646
  repoFingerprint: repository.repoFingerprint,
2647
+ source: (await this.checkoutSource(repository, pointer.source)) ?? null,
2568
2648
  afterSequence: 0,
2569
2649
  },
2570
2650
  });
@@ -2625,6 +2705,7 @@ export class BridgeService {
2625
2705
  }
2626
2706
  }
2627
2707
  async retryVerificationRecovery(repository, pointer, backend) {
2708
+ await this.checkoutSource(repository, pointer.source);
2628
2709
  const intent = pointer.verificationIntent;
2629
2710
  const recovery = objectValue(backend?.verificationRecovery);
2630
2711
  if (!intent) {
@@ -2717,6 +2798,7 @@ export class BridgeService {
2717
2798
  }
2718
2799
  const taskVersion = numericTaskVersion(responseData.taskVersion);
2719
2800
  await this.dependencies.gate.record({
2801
+ source: sourceFromContext(body.source),
2720
2802
  taskId: pointer.taskId,
2721
2803
  projectId: pointer.projectId,
2722
2804
  repoFingerprint: pointer.repoFingerprint,
@@ -2750,6 +2832,7 @@ export class BridgeService {
2750
2832
  return await this.deliverCloseIntent(repository, body, intent.taskChanges, true);
2751
2833
  }
2752
2834
  async deliverCloseIntent(repository, body, taskChanges, recoveredAfterResponseLoss) {
2835
+ await this.checkoutSource(repository, sourceFromContext(body.source));
2753
2836
  const touchedContract = taskChanges.some((change) => /\.controller\.[tj]s$/.test(change.path));
2754
2837
  const currentBranch = await this.dependencies.repositories.git.currentBranch(repository.repoRoot);
2755
2838
  if (typeof body.branch === 'string' && body.branch !== currentBranch)
@@ -2774,6 +2857,7 @@ export class BridgeService {
2774
2857
  }
2775
2858
  const closedTaskVersion = numericTaskVersion(closedTask.taskVersion);
2776
2859
  await this.dependencies.gate.record({
2860
+ source: sourceFromContext(body.source),
2777
2861
  taskId: String(body.taskId),
2778
2862
  projectId: repository.projectId,
2779
2863
  repoFingerprint: repository.repoFingerprint,
@@ -3482,4 +3566,29 @@ function assertValidationEvidence(validations) {
3482
3566
  seen.add(validation.validationId);
3483
3567
  }
3484
3568
  }
3569
+ function sourceFromContext(value) {
3570
+ const context = objectValue(value);
3571
+ if (!context || typeof context.sourceCommit !== 'string' || typeof context.treeHash !== 'string')
3572
+ return undefined;
3573
+ const linked = Array.isArray(context.linked)
3574
+ ? context.linked.map((entry) => {
3575
+ const item = objectValue(entry);
3576
+ if (!item ||
3577
+ typeof item.projectId !== 'string' ||
3578
+ typeof item.sourceCommit !== 'string' ||
3579
+ typeof item.treeHash !== 'string')
3580
+ throw refuse('The backend source context is incomplete. Resume this task.', 'session.resume');
3581
+ return {
3582
+ projectId: item.projectId,
3583
+ sourceCommit: item.sourceCommit,
3584
+ treeHash: item.treeHash,
3585
+ };
3586
+ })
3587
+ : undefined;
3588
+ return {
3589
+ sourceCommit: context.sourceCommit,
3590
+ treeHash: context.treeHash,
3591
+ ...(linked ? { linked } : {}),
3592
+ };
3593
+ }
3485
3594
  //# sourceMappingURL=bridge-service.js.map
@@ -6,6 +6,22 @@ import { sha256, stableStringify } from '../utilities/hash.js';
6
6
  const sensitiveKeyPattern = /(authorization|password|passcode|secret|token|cookie|email|phone|mobile|customer|account|user.?id|user.?name)/i;
7
7
  const emailPattern = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i;
8
8
  const phonePattern = /\+\d[\d ()-]{7,}\d/;
9
+ const repositoryPathFields = new Set([
10
+ 'path',
11
+ 'paths',
12
+ 'originalPath',
13
+ 'changedPaths',
14
+ 'pathPatterns',
15
+ 'evidencePaths',
16
+ ]);
17
+ const iosAssetScalePathPattern = /^(?:[\p{L}\p{N}_. -]+\/)*[\p{L}\p{N}_. -]+\.xcassets\/[\p{L}\p{N}_. -]+\.(?:appiconset|imageset)\/[\p{L}\p{N}_. -]+@[123]x\.(?:png|jpe?g)$/iu;
18
+ function emailScanValue(value, key) {
19
+ if (!repositoryPathFields.has(key) ||
20
+ !iosAssetScalePathPattern.test(value) ||
21
+ value.split('/').some((part) => part === '.' || part === '..'))
22
+ return value;
23
+ return value.replace(/@[123]x(?=\.(?:png|jpe?g)$)/i, '');
24
+ }
9
25
  const bearerPattern = /\bbearer\s+[a-z0-9._~-]+/i;
10
26
  const jwtPattern = /\beyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/;
11
27
  const privateKeyPattern = /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/;
@@ -155,7 +171,7 @@ export function assertSafeToPersist(value, key = '') {
155
171
  throw new Error(`Sensitive field cannot be persisted: ${key}`);
156
172
  }
157
173
  if (typeof value === 'string') {
158
- if (emailPattern.test(value) ||
174
+ if (emailPattern.test(emailScanValue(value, key)) ||
159
175
  phonePattern.test(value) ||
160
176
  bearerPattern.test(value) ||
161
177
  jwtPattern.test(value) ||
@@ -199,13 +215,13 @@ export function redactForCache(value) {
199
215
  return null;
200
216
  }
201
217
  }
202
- function redactValue(value) {
218
+ function redactValue(value, key = '') {
203
219
  if (typeof value === 'string') {
204
- assertSafeToPersist(value);
220
+ assertSafeToPersist(value, key);
205
221
  return value;
206
222
  }
207
223
  if (Array.isArray(value)) {
208
- return value.map((entry) => redactValue(entry));
224
+ return value.map((entry) => redactValue(entry, key));
209
225
  }
210
226
  if (value !== null && typeof value === 'object') {
211
227
  const result = {};
@@ -213,7 +229,7 @@ function redactValue(value) {
213
229
  if (sensitiveKeyPattern.test(key)) {
214
230
  continue;
215
231
  }
216
- result[key] = redactValue(entry);
232
+ result[key] = redactValue(entry, key);
217
233
  }
218
234
  return result;
219
235
  }
@@ -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,
@@ -157,3 +206,32 @@ are written by the agent in the same language; pass language for the native help
157
206
  An existing exact-request questionnaire retains its original wording and answer across updates and
158
207
  restart, including an older English form. Resume that question rather than silently replacing or
159
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.