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 +1 -1
- package/runtime/dist/src/git/git-inspector.js +112 -0
- package/runtime/dist/src/git/verification-gate.js +43 -0
- package/runtime/dist/src/mcp/onboarding-tools.js +111 -18
- package/runtime/dist/src/mcp/questionnaire-tools.js +28 -6
- package/runtime/dist/src/mcp/tool-definitions.js +15 -2
- package/runtime/dist/src/runtime/active-context-store.js +8 -0
- package/runtime/dist/src/runtime/api-client.js +2 -0
- package/runtime/dist/src/runtime/bridge-service.js +105 -6
- package/runtime/dist/src/runtime/questionnaire-store.js +14 -2
- package/skill/references/lifecycle.md +35 -1
- package/skill/references/project-onboarding.md +91 -0
- package/skill/references/questionnaires.md +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engineering-memory",
|
|
3
|
-
"version": "1.
|
|
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
|
|
294
|
+
const { language = 'en', ...request } = input;
|
|
295
|
+
const pinned = { ...request, ref: commit };
|
|
230
296
|
const digest = sha256(stableStringify(pinned));
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
'
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
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:
|
|
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 ? [
|
|
77
|
+
...(record.allowFreeText ? [copy.other] : []),
|
|
74
78
|
],
|
|
75
79
|
},
|
|
76
80
|
...(record.allowFreeText
|
|
77
81
|
? {
|
|
78
82
|
text: {
|
|
79
83
|
type: 'string',
|
|
80
|
-
title:
|
|
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: '
|
|
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 ||
|
|
@@ -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: {
|
|
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 ||
|
|
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
|
-
##
|
|
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.
|