engineering-memory 1.11.9 → 1.11.10
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/dispatcher/sections.mjs +6 -2
- package/install/codex-approval.mjs +457 -0
- package/install/files.mjs +5 -1
- package/install/installer.mjs +24 -1
- package/package.json +1 -1
- package/runtime/build.json +1 -1
- package/runtime/dist/src/config.js +1 -0
- package/runtime/dist/src/git/git-inspector.js +82 -3
- package/runtime/dist/src/git/verification-gate.js +30 -17
- package/runtime/dist/src/mcp/onboarding-tools.js +3 -16
- package/runtime/dist/src/mcp/questionnaire-tools.js +99 -15
- package/runtime/dist/src/mcp/tool-annotations.js +111 -0
- package/runtime/dist/src/mcp/tool-definitions.js +148 -28
- package/runtime/dist/src/mcp/worktree-tools.js +323 -236
- package/runtime/dist/src/project/repository.js +11 -5
- package/runtime/dist/src/runtime/active-context-store.js +3 -0
- package/runtime/dist/src/runtime/branch-preferences.js +0 -19
- package/runtime/dist/src/runtime/bridge-service.js +1131 -220
- package/runtime/dist/src/runtime/offline-outbox.js +33 -158
- package/runtime/dist/src/runtime/phase-timer.js +26 -0
- package/runtime/dist/src/runtime/privacy-detector.js +263 -0
- package/runtime/dist/src/runtime/questionnaire-store.js +210 -49
- package/runtime/dist/src/runtime/recovery-error.js +3 -1
- package/runtime/dist/src/runtime/runtime-entry.js +8 -0
- package/runtime/dist/src/runtime/runtime-host.js +1 -3
- package/runtime/dist/src/runtime/task-branch-store.js +17 -1
- package/runtime/dist/src/runtime/task-start.js +281 -0
- package/runtime/dist/src/runtime/worktree-pool.js +334 -24
- package/runtime/dist/src/utilities/process.js +1 -0
- package/skill/SKILL.md +3 -3
- package/skill/references/lifecycle.md +44 -19
- package/skill/references/memory-updates.md +9 -3
- package/skill/references/project-onboarding.md +19 -2
- package/skill/references/questionnaires.md +77 -15
|
@@ -242,6 +242,39 @@ export class GitInspector {
|
|
|
242
242
|
]);
|
|
243
243
|
return ref ? ref.replace(/^origin\//, '') : null;
|
|
244
244
|
}
|
|
245
|
+
async pushDestination(repoRoot) {
|
|
246
|
+
const branch = await this.currentBranch(repoRoot);
|
|
247
|
+
const upstream = branch
|
|
248
|
+
? await this.gitValue(repoRoot, ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'])
|
|
249
|
+
: null;
|
|
250
|
+
const listed = await this.runner.run('git', ['remote'], { cwd: repoRoot });
|
|
251
|
+
const remotes = listed.exitCode === 0 ? listed.stdout.split(/\r?\n/).filter(Boolean) : [];
|
|
252
|
+
const configured = (branch &&
|
|
253
|
+
(await this.gitValue(repoRoot, ['config', '--get', `branch.${branch}.pushRemote`]))) ||
|
|
254
|
+
(await this.gitValue(repoRoot, ['config', '--get', 'remote.pushDefault'])) ||
|
|
255
|
+
(branch && (await this.gitValue(repoRoot, ['config', '--get', `branch.${branch}.remote`]))) ||
|
|
256
|
+
'origin';
|
|
257
|
+
const remote = remotes.includes(configured) ? configured : null;
|
|
258
|
+
const pushUrl = remote
|
|
259
|
+
? await this.gitValue(repoRoot, ['remote', 'get-url', '--push', remote])
|
|
260
|
+
: null;
|
|
261
|
+
const remoteHead = remote
|
|
262
|
+
? await this.gitValue(repoRoot, [
|
|
263
|
+
'symbolic-ref',
|
|
264
|
+
'--short',
|
|
265
|
+
'-q',
|
|
266
|
+
`refs/remotes/${remote}/HEAD`,
|
|
267
|
+
])
|
|
268
|
+
: null;
|
|
269
|
+
return {
|
|
270
|
+
branch,
|
|
271
|
+
upstream,
|
|
272
|
+
remote,
|
|
273
|
+
pushUrl: pushUrl ? withoutCredentials(pushUrl) : null,
|
|
274
|
+
remoteDefaultBranch: remoteHead ? remoteHead.slice(`${remote}/`.length) : null,
|
|
275
|
+
headCommit: await this.gitValue(repoRoot, ['rev-parse', '--verify', '-q', 'HEAD']),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
245
278
|
async protectedBranchNames(repoRoot) {
|
|
246
279
|
const names = new Set(['main', 'master', 'develop', 'prod']);
|
|
247
280
|
const fallback = await this.defaultBranch(repoRoot);
|
|
@@ -286,6 +319,14 @@ export class GitInspector {
|
|
|
286
319
|
}
|
|
287
320
|
return await canonicalPath(resolve(commonDirectory, '..'));
|
|
288
321
|
}
|
|
322
|
+
async identity(repoRoot) {
|
|
323
|
+
const root = await this.findRoot(repoRoot);
|
|
324
|
+
const [head, branch] = await Promise.all([
|
|
325
|
+
this.gitValue(root, ['rev-parse', 'HEAD']),
|
|
326
|
+
this.currentBranch(root),
|
|
327
|
+
]);
|
|
328
|
+
return { repoRoot: root, head, branch };
|
|
329
|
+
}
|
|
289
330
|
async manifest(repoRoot) {
|
|
290
331
|
const root = await this.findRoot(repoRoot);
|
|
291
332
|
const head = await this.gitValue(root, ['rev-parse', 'HEAD']);
|
|
@@ -301,6 +342,30 @@ export class GitInspector {
|
|
|
301
342
|
diffHash: sha256(`${head ?? '<unborn>'}\n${stableStringify(hashManifest)}\n`),
|
|
302
343
|
};
|
|
303
344
|
}
|
|
345
|
+
async manifestAgainst(repoRoot, baseCommit) {
|
|
346
|
+
const root = await this.findRoot(repoRoot);
|
|
347
|
+
const head = await this.gitValue(root, ['rev-parse', 'HEAD']);
|
|
348
|
+
const changedPaths = await this.changedPathsAgainstHead(root, baseCommit);
|
|
349
|
+
const hashManifest = canonicalHashManifest(changedPaths);
|
|
350
|
+
return {
|
|
351
|
+
repoRoot: root,
|
|
352
|
+
head,
|
|
353
|
+
baseCommit,
|
|
354
|
+
changedPaths,
|
|
355
|
+
worktreeHash: sha256(`${stableStringify(hashManifest)}\n`),
|
|
356
|
+
diffHash: sha256(`${baseCommit}\n${head ?? '<unborn>'}\n${stableStringify(hashManifest)}\n`),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
async isAncestor(repoRoot, ancestor, descendant = 'HEAD') {
|
|
360
|
+
const result = await this.runner.run('git', ['merge-base', '--is-ancestor', ancestor, descendant], { cwd: repoRoot });
|
|
361
|
+
return result.exitCode === 0;
|
|
362
|
+
}
|
|
363
|
+
async committedPaths(repoRoot, baseCommit) {
|
|
364
|
+
const result = await this.runner.run('git', ['diff', '--name-only', '-z', '--find-renames', baseCommit, 'HEAD', '--'], { cwd: repoRoot });
|
|
365
|
+
if (result.exitCode !== 0)
|
|
366
|
+
throw new Error(`Git committed diff failed: ${result.stderr.trim()}`);
|
|
367
|
+
return result.stdout.split('\0').filter(Boolean).map(normalizeGitPath).sort();
|
|
368
|
+
}
|
|
304
369
|
async stagedManifest(repoRoot) {
|
|
305
370
|
const root = await this.findRoot(repoRoot);
|
|
306
371
|
let result = await this.runner.run('git', ['diff', '--cached', '--name-status', '-z', '--find-renames', '--find-copies', 'HEAD', '--'], { cwd: root });
|
|
@@ -357,10 +422,10 @@ export class GitInspector {
|
|
|
357
422
|
}
|
|
358
423
|
return result.sort((left, right) => left.path.localeCompare(right.path));
|
|
359
424
|
}
|
|
360
|
-
async changedPathsAgainstHead(repoRoot) {
|
|
425
|
+
async changedPathsAgainstHead(repoRoot, ref = 'HEAD') {
|
|
361
426
|
const [tracked, raw, untracked] = await Promise.all([
|
|
362
|
-
this.runner.run('git', ['diff',
|
|
363
|
-
this.runner.run('git', ['diff',
|
|
427
|
+
this.runner.run('git', ['diff', ref, '--name-status', '-z', '--find-renames', '--find-copies', '--'], { cwd: repoRoot }),
|
|
428
|
+
this.runner.run('git', ['diff', ref, '--raw', '-z', '--no-abbrev', '--'], {
|
|
364
429
|
cwd: repoRoot,
|
|
365
430
|
}),
|
|
366
431
|
this.runner.run('git', ['ls-files', '--others', '--exclude-standard', '-z'], {
|
|
@@ -699,6 +764,20 @@ function canonicalRemoteIdentity(remote) {
|
|
|
699
764
|
return `local/${normalizeRemotePath(trimmed.replace(/[?#].*$/, ''))}`;
|
|
700
765
|
}
|
|
701
766
|
}
|
|
767
|
+
function withoutCredentials(remote) {
|
|
768
|
+
if (!/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(remote))
|
|
769
|
+
return remote;
|
|
770
|
+
try {
|
|
771
|
+
const url = new URL(remote);
|
|
772
|
+
if (url.protocol === 'http:' || url.protocol === 'https:')
|
|
773
|
+
url.username = ''; // a pasted token sits here
|
|
774
|
+
url.password = '';
|
|
775
|
+
return url.toString();
|
|
776
|
+
}
|
|
777
|
+
catch {
|
|
778
|
+
return remote.replace(/\/\/[^/@]*@/, '//');
|
|
779
|
+
}
|
|
780
|
+
}
|
|
702
781
|
function normalizeRemotePath(path) {
|
|
703
782
|
const normalized = path
|
|
704
783
|
.replace(/\\/g, '/')
|
|
@@ -151,23 +151,34 @@ export class VerificationGate {
|
|
|
151
151
|
};
|
|
152
152
|
}
|
|
153
153
|
const actualSource = receipt.source ? await this.git.sourceIdentity(repoRoot) : null;
|
|
154
|
+
const ahead = receipt.branch &&
|
|
155
|
+
receipt.source &&
|
|
156
|
+
actualSource &&
|
|
157
|
+
actualSource.sourceCommit !== receipt.source.sourceCommit &&
|
|
158
|
+
(await this.git.isAncestor(repository.repoRoot, receipt.source.sourceCommit)) &&
|
|
159
|
+
(await this.git.committedPaths(repository.repoRoot, receipt.source.sourceCommit)).every((path) => receipt.taskChanges.some((change) => change.path === path))
|
|
160
|
+
? await this.git.manifestAgainst(repository.repoRoot, receipt.source.sourceCommit)
|
|
161
|
+
: null;
|
|
162
|
+
const committedAhead = ahead?.diffHash === receipt.diffHash;
|
|
163
|
+
const current = ahead && committedAhead ? ahead : manifest;
|
|
154
164
|
if (receipt.source &&
|
|
165
|
+
!committedAhead &&
|
|
155
166
|
(!actualSource ||
|
|
156
167
|
actualSource.sourceCommit !== receipt.source.sourceCommit ||
|
|
157
168
|
actualSource.treeHash !== receipt.source.treeHash)) {
|
|
158
169
|
return {
|
|
159
170
|
allowed: false,
|
|
160
171
|
reason: 'source_commit_mismatch',
|
|
161
|
-
diffHash:
|
|
172
|
+
diffHash: current.diffHash,
|
|
162
173
|
taskId: receipt.taskId,
|
|
163
174
|
details: ['Return to the task source commit or start a task for the current source.'],
|
|
164
175
|
};
|
|
165
176
|
}
|
|
166
|
-
if (receipt.diffHash !==
|
|
177
|
+
if (receipt.diffHash !== current.diffHash) {
|
|
167
178
|
return {
|
|
168
179
|
allowed: false,
|
|
169
180
|
reason: 'git_diff_changed_after_verify',
|
|
170
|
-
diffHash:
|
|
181
|
+
diffHash: current.diffHash,
|
|
171
182
|
taskId: receipt.taskId,
|
|
172
183
|
};
|
|
173
184
|
}
|
|
@@ -175,7 +186,7 @@ export class VerificationGate {
|
|
|
175
186
|
return {
|
|
176
187
|
allowed: false,
|
|
177
188
|
reason: 'no_task_changes_staged',
|
|
178
|
-
diffHash:
|
|
189
|
+
diffHash: current.diffHash,
|
|
179
190
|
taskId: receipt.taskId,
|
|
180
191
|
};
|
|
181
192
|
}
|
|
@@ -183,7 +194,7 @@ export class VerificationGate {
|
|
|
183
194
|
return {
|
|
184
195
|
allowed: false,
|
|
185
196
|
reason: 'staged_change_outside_task',
|
|
186
|
-
diffHash:
|
|
197
|
+
diffHash: current.diffHash,
|
|
187
198
|
taskId: receipt.taskId,
|
|
188
199
|
};
|
|
189
200
|
}
|
|
@@ -191,22 +202,24 @@ export class VerificationGate {
|
|
|
191
202
|
return {
|
|
192
203
|
allowed: false,
|
|
193
204
|
reason: 'online_commit_gate_unavailable',
|
|
194
|
-
diffHash:
|
|
205
|
+
diffHash: current.diffHash,
|
|
195
206
|
taskId: receipt.taskId,
|
|
196
207
|
};
|
|
197
208
|
}
|
|
198
209
|
try {
|
|
199
210
|
const attestation = await this.commitGateAttestor({
|
|
200
|
-
source:
|
|
201
|
-
?
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
211
|
+
source: committedAhead
|
|
212
|
+
? receipt.source
|
|
213
|
+
: actualSource
|
|
214
|
+
? {
|
|
215
|
+
...actualSource,
|
|
216
|
+
...(receipt.source?.linked ? { linked: receipt.source.linked } : {}),
|
|
217
|
+
}
|
|
218
|
+
: undefined,
|
|
206
219
|
taskId: receipt.taskId,
|
|
207
220
|
projectId: receipt.projectId,
|
|
208
221
|
repoFingerprint: receipt.repoFingerprint,
|
|
209
|
-
diffHash:
|
|
222
|
+
diffHash: current.diffHash,
|
|
210
223
|
expectedTaskVersion: receipt.taskVersion,
|
|
211
224
|
branch: branch ?? undefined,
|
|
212
225
|
});
|
|
@@ -214,11 +227,11 @@ export class VerificationGate {
|
|
|
214
227
|
if (result?.allowed !== true ||
|
|
215
228
|
result.taskId !== receipt.taskId ||
|
|
216
229
|
result.taskVersion !== receipt.taskVersion ||
|
|
217
|
-
result.diffHash !==
|
|
230
|
+
result.diffHash !== current.diffHash) {
|
|
218
231
|
return {
|
|
219
232
|
allowed: false,
|
|
220
233
|
reason: 'online_commit_gate_mismatch',
|
|
221
|
-
diffHash:
|
|
234
|
+
diffHash: current.diffHash,
|
|
222
235
|
taskId: receipt.taskId,
|
|
223
236
|
};
|
|
224
237
|
}
|
|
@@ -227,7 +240,7 @@ export class VerificationGate {
|
|
|
227
240
|
return {
|
|
228
241
|
allowed: false,
|
|
229
242
|
reason: 'online_commit_gate_rejected',
|
|
230
|
-
diffHash:
|
|
243
|
+
diffHash: current.diffHash,
|
|
231
244
|
taskId: receipt.taskId,
|
|
232
245
|
details: [error instanceof Error ? error.message : String(error)],
|
|
233
246
|
};
|
|
@@ -235,7 +248,7 @@ export class VerificationGate {
|
|
|
235
248
|
return {
|
|
236
249
|
allowed: true,
|
|
237
250
|
reason: 'verified',
|
|
238
|
-
diffHash:
|
|
251
|
+
diffHash: current.diffHash,
|
|
239
252
|
taskId: receipt.taskId,
|
|
240
253
|
};
|
|
241
254
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as z from 'zod/v4';
|
|
2
2
|
import { sha256, stableStringify } from '../utilities/hash.js';
|
|
3
|
-
import { askQuestionnaire } from './questionnaire-tools.js';
|
|
3
|
+
import { answerChoice, answerData, askQuestionnaire } from './questionnaire-tools.js';
|
|
4
4
|
const uuid = z.string().uuid();
|
|
5
5
|
const hash = z.string().regex(/^[a-f0-9]{64}$/);
|
|
6
6
|
const project = { projectId: uuid };
|
|
@@ -468,7 +468,7 @@ export function registerOnboardingTools(server, service) {
|
|
|
468
468
|
inputSchema: z.object(run),
|
|
469
469
|
}, async (input) => output(await service.onboardingRequest('verify', body(input))));
|
|
470
470
|
server.registerTool('memory.catalog', {
|
|
471
|
-
description: 'Page accessible memory resource headers by id
|
|
471
|
+
description: 'Page accessible memory resource headers by id: each entry names its current revision and its size in characters instead of carrying its content. Pass the response nextAfterId as afterId to read the next page; nextAfterId is null once nothing more remains. includeArchived exposes retired identities for refresh and rename preservation.',
|
|
472
472
|
inputSchema: z.object({
|
|
473
473
|
...project,
|
|
474
474
|
...page,
|
|
@@ -479,7 +479,7 @@ export function registerOnboardingTools(server, service) {
|
|
|
479
479
|
}),
|
|
480
480
|
}, async (input) => output(await service.onboardingRequest('catalog', body(input))));
|
|
481
481
|
server.registerTool('memory.read_revisions', {
|
|
482
|
-
description: 'Read selected immutable revisions in an authorized context session and record receipts. Bounds keep large project learning independent of normal task context limits.',
|
|
482
|
+
description: 'Read selected immutable revisions in an authorized context session and record receipts. One response carries what fits its size budget, in the order requested, as items; the revisions it did not carry are listed in remainingRevisionIds, to pass to the next call. Bounds keep large project learning independent of normal task context limits.',
|
|
483
483
|
inputSchema: z.object({
|
|
484
484
|
...project,
|
|
485
485
|
sessionId: uuid,
|
|
@@ -533,19 +533,6 @@ export function registerOnboardingTools(server, service) {
|
|
|
533
533
|
})));
|
|
534
534
|
});
|
|
535
535
|
}
|
|
536
|
-
function answerChoice(result) {
|
|
537
|
-
return answerData(result)?.choice;
|
|
538
|
-
}
|
|
539
|
-
function answerData(result) {
|
|
540
|
-
const response = result;
|
|
541
|
-
const text = response.content?.find((item) => item.type === 'text')?.text;
|
|
542
|
-
if (!text)
|
|
543
|
-
return undefined;
|
|
544
|
-
const value = JSON.parse(text);
|
|
545
|
-
return value.ok && value.data?.status === 'answered' && value.data.answerAvailable
|
|
546
|
-
? value.data.answer
|
|
547
|
-
: undefined;
|
|
548
|
-
}
|
|
549
536
|
async function approveOnboarding(server, service, input, context, initialize) {
|
|
550
537
|
const preparation = !input.projectId;
|
|
551
538
|
const scope = { repoRoot: input.repoRoot, preparation };
|
|
@@ -1,13 +1,26 @@
|
|
|
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, previousDefinitions = []) {
|
|
3
|
+
export async function askQuestionnaire(server, service, input, context, previousDefinitions = [], owner) {
|
|
4
4
|
try {
|
|
5
|
-
return await present(server, service, await service.questionnaireAsk(input, previousDefinitions), input.repoRoot, context, input.preparation);
|
|
5
|
+
return await present(server, service, await service.questionnaireAsk(input, previousDefinitions, owner), input.repoRoot, context, input.preparation, input.presentation);
|
|
6
6
|
}
|
|
7
7
|
catch (error) {
|
|
8
8
|
return failure(error);
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
+
export function answerChoice(result) {
|
|
12
|
+
return answerData(result)?.choice;
|
|
13
|
+
}
|
|
14
|
+
export function answerData(result) {
|
|
15
|
+
const response = result;
|
|
16
|
+
const text = response.content?.find((item) => item.type === 'text')?.text;
|
|
17
|
+
if (!text)
|
|
18
|
+
return undefined;
|
|
19
|
+
const value = JSON.parse(text);
|
|
20
|
+
return value.ok && value.data?.status === 'answered' && value.data.answerAvailable
|
|
21
|
+
? value.data.answer
|
|
22
|
+
: undefined;
|
|
23
|
+
}
|
|
11
24
|
export async function resumeQuestionnaire(server, service, input, context) {
|
|
12
25
|
try {
|
|
13
26
|
return await present(server, service, await service.questionnaireResume(input), input.repoRoot, context, input.preparation, input.presentation);
|
|
@@ -19,7 +32,7 @@ export async function resumeQuestionnaire(server, service, input, context) {
|
|
|
19
32
|
export async function answerQuestionnaireFromHost(service, input) {
|
|
20
33
|
try {
|
|
21
34
|
const resolved = await service.questionnaireAnswerFromHost(input);
|
|
22
|
-
return answered(resolved.record, resolved.answer, resolved.replayed);
|
|
35
|
+
return answered(resolved.record, resolved.answer, resolved.replayed, resolved.retry);
|
|
23
36
|
}
|
|
24
37
|
catch (error) {
|
|
25
38
|
return failure(error);
|
|
@@ -33,8 +46,11 @@ async function present(server, service, record, repoRoot, context, preparation,
|
|
|
33
46
|
answerAvailable: false,
|
|
34
47
|
nextAction: 'This decision was explicitly withdrawn. Do not continue dependent work. Use a new questionnaireId or onboarding requestKey only if the user resumes that work.',
|
|
35
48
|
});
|
|
36
|
-
if (record.status === 'answered')
|
|
37
|
-
|
|
49
|
+
if (record.status === 'answered') {
|
|
50
|
+
if (inputResponse(context.mcpReq.inputResponses, record.requestKey).kind !== 'missing')
|
|
51
|
+
consumedResponses(context).add(record.requestKey);
|
|
52
|
+
return answered(record, record.questions ? record.answers : record.answer, true);
|
|
53
|
+
}
|
|
38
54
|
if (context.mcpReq.signal.aborted)
|
|
39
55
|
return pending(record, 'interrupted');
|
|
40
56
|
if (presentation === 'host_native')
|
|
@@ -42,22 +58,27 @@ async function present(server, service, record, repoRoot, context, preparation,
|
|
|
42
58
|
const responses = context.mcpReq.inputResponses;
|
|
43
59
|
const response = inputResponse(responses, record.requestKey);
|
|
44
60
|
if (response.kind === 'elicit' && response.action === 'accept') {
|
|
45
|
-
const
|
|
61
|
+
const content = acceptedContent(responses, record.requestKey);
|
|
62
|
+
const parsed = questionnaireAnswerSchema(record).safeParse(record.questions && content ? formAnswers(record, content) : content);
|
|
63
|
+
const answer = parsed.success ? parsed.data : undefined;
|
|
46
64
|
if (!answer)
|
|
47
65
|
return pending(record, 'invalid_response');
|
|
66
|
+
consumedResponses(context).add(record.requestKey);
|
|
48
67
|
const resolved = await service.questionnaireAccept({
|
|
49
68
|
repoRoot,
|
|
50
69
|
preparation,
|
|
51
70
|
questionnaireId: record.questionnaireId,
|
|
52
71
|
requestKey: record.requestKey,
|
|
53
72
|
scope: record.scope,
|
|
54
|
-
answer,
|
|
73
|
+
answer: answer,
|
|
55
74
|
});
|
|
56
75
|
return answered(resolved.record, resolved.answer, resolved.replayed);
|
|
57
76
|
}
|
|
58
77
|
if (response.kind === 'elicit')
|
|
59
78
|
return pending(record, response.action);
|
|
60
|
-
if (
|
|
79
|
+
if (response.kind !== 'missing' ||
|
|
80
|
+
context.mcpReq.droppedInputResponseKeys?.length ||
|
|
81
|
+
Object.keys(responses ?? {}).some((key) => !consumedResponses(context).has(key)))
|
|
61
82
|
return pending(record, 'missing_or_stale_response');
|
|
62
83
|
const envelope = context.mcpReq.envelope;
|
|
63
84
|
const capabilities = envelope?.[CLIENT_CAPABILITIES_META_KEY] ?? server.server.getClientCapabilities();
|
|
@@ -65,6 +86,35 @@ async function present(server, service, record, repoRoot, context, preparation,
|
|
|
65
86
|
if (!elicitation || (!elicitation.form && elicitation.url !== undefined))
|
|
66
87
|
return pending(record, 'native_form_unavailable');
|
|
67
88
|
const copy = questionnaireCopy[record.language ?? 'en'];
|
|
89
|
+
if (record.questions) {
|
|
90
|
+
const properties = {};
|
|
91
|
+
const required = [];
|
|
92
|
+
for (const sub of record.questions) {
|
|
93
|
+
properties[sub.id] = {
|
|
94
|
+
type: 'string',
|
|
95
|
+
title: sub.message,
|
|
96
|
+
enum: sub.options.map((option) => option.id),
|
|
97
|
+
enumNames: sub.options.map((option) => option.label),
|
|
98
|
+
};
|
|
99
|
+
required.push(sub.id);
|
|
100
|
+
if (sub.textField)
|
|
101
|
+
properties[`${sub.id}_text`] = {
|
|
102
|
+
type: 'string',
|
|
103
|
+
title: sub.textField.title,
|
|
104
|
+
minLength: 1,
|
|
105
|
+
maxLength: sub.textField.maxLength,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return inputRequired({
|
|
109
|
+
inputRequests: {
|
|
110
|
+
[record.requestKey]: inputRequired.elicit({
|
|
111
|
+
mode: 'form',
|
|
112
|
+
message: presentationMessage(record),
|
|
113
|
+
requestedSchema: { type: 'object', properties, required },
|
|
114
|
+
}),
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
68
118
|
return inputRequired({
|
|
69
119
|
inputRequests: {
|
|
70
120
|
[record.requestKey]: inputRequired.elicit({
|
|
@@ -112,11 +162,18 @@ async function present(server, service, record, repoRoot, context, preparation,
|
|
|
112
162
|
},
|
|
113
163
|
});
|
|
114
164
|
}
|
|
165
|
+
const consumed = new WeakMap();
|
|
166
|
+
function consumedResponses(context) {
|
|
167
|
+
const keys = consumed.get(context.mcpReq) ?? new Set();
|
|
168
|
+
consumed.set(context.mcpReq, keys);
|
|
169
|
+
return keys;
|
|
170
|
+
}
|
|
115
171
|
const questionnaireCopy = {
|
|
116
172
|
en: {
|
|
117
173
|
pending: 'Choose an answer to resolve this decision. Closing or cancelling the form leaves it pending.',
|
|
118
174
|
freeText: ' For another answer choose Other and fill in text. Free text is not stored and cannot be replayed.',
|
|
119
175
|
storedText: ' The bounded text field is stored as part of this exact decision and is replayed after restart.',
|
|
176
|
+
multi: ' AskUserQuestion takes up to 4 questions per call. Show every question in as few calls as needed, then relay all answers in one questionnaire.answer_from_host.',
|
|
120
177
|
answer: 'Answer',
|
|
121
178
|
other: 'Other',
|
|
122
179
|
otherAnswer: 'Other answer',
|
|
@@ -125,17 +182,33 @@ const questionnaireCopy = {
|
|
|
125
182
|
pending: 'Kararını kaydetmek için bir seçenek seç. Formu kapatmak veya iptal etmek onay sayılmaz; soru beklemede kalır.',
|
|
126
183
|
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.',
|
|
127
184
|
storedText: ' Bu metin alanındaki yanıt kararınla birlikte saklanır; yeniden başlattığında korunur.',
|
|
185
|
+
multi: ' AskUserQuestion tek çağrıda en fazla 4 soru alır. Her soruyu en az çağrıyla göster, sonra tüm yanıtları tek bir questionnaire.answer_from_host çağrısında ilet.',
|
|
128
186
|
answer: 'Yanıt',
|
|
129
187
|
other: 'Diğer',
|
|
130
188
|
otherAnswer: 'Diğer yanıt',
|
|
131
189
|
},
|
|
132
190
|
};
|
|
191
|
+
function formAnswers(record, content) {
|
|
192
|
+
const answers = {};
|
|
193
|
+
for (const sub of record.questions ?? []) {
|
|
194
|
+
if (content[sub.id] === undefined)
|
|
195
|
+
continue;
|
|
196
|
+
const text = content[`${sub.id}_text`];
|
|
197
|
+
answers[sub.id] = {
|
|
198
|
+
choice: content[sub.id],
|
|
199
|
+
...(text !== undefined && sub.textField?.requiredForChoice === content[sub.id]
|
|
200
|
+
? { text }
|
|
201
|
+
: {}),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return answers;
|
|
205
|
+
}
|
|
133
206
|
function presentationMessage(record) {
|
|
134
207
|
const notice = /^memory-batch-[a-f0-9]{64}-[0-9]+$/.test(record.questionnaireId)
|
|
135
208
|
? 'For an unversioned project proposal, approval updates legacy memory only. Source-aware tasks cannot read it until an inspection explicitly retains it. Check each sourceApplicabilityNotice in memory.list_proposals before approving.'
|
|
136
209
|
: undefined;
|
|
137
210
|
const copy = questionnaireCopy[record.language ?? 'en'];
|
|
138
|
-
return `${record.message}${notice ? `\n\n${notice}` : ''}\n\n${copy.pending}${record.allowFreeText ? copy.freeText : ''}${record.textField ? copy.storedText : ''}`;
|
|
211
|
+
return `${record.message}${notice ? `\n\n${notice}` : ''}\n\n${copy.pending}${record.allowFreeText ? copy.freeText : ''}${record.textField ? copy.storedText : ''}${record.questions ? copy.multi : ''}`;
|
|
139
212
|
}
|
|
140
213
|
function pending(record, reason) {
|
|
141
214
|
return result({
|
|
@@ -149,25 +222,36 @@ function pending(record, reason) {
|
|
|
149
222
|
requestKey: record.requestKey,
|
|
150
223
|
contentHash: record.contentHash,
|
|
151
224
|
message: presentationMessage(record),
|
|
152
|
-
options: record.options,
|
|
153
225
|
language: record.language ?? 'en',
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
226
|
+
...(record.questions
|
|
227
|
+
? {
|
|
228
|
+
questions: record.questions,
|
|
229
|
+
...(record.terminalChoices ? { terminalChoices: record.terminalChoices } : {}),
|
|
230
|
+
}
|
|
231
|
+
: {
|
|
232
|
+
options: record.options,
|
|
233
|
+
allowFreeText: record.allowFreeText,
|
|
234
|
+
...(record.textField ? { textField: record.textField } : {}),
|
|
235
|
+
}),
|
|
236
|
+
instructions: record.questions
|
|
237
|
+
? 'Only if the host permits a blocking native control for this decision, display every question, its options and notices in AskUserQuestion or request_user_input, one call at a time if needed. Relay every answer together in one questionnaire.answer_from_host call as `answers`, keyed by question id. Closing, declining, timeout, prose consent and missing answers are not native answers for any question. Do not reopen a dismissed question in a loop or label decline as proof of host incompatibility. The relay records agent-reported provenance, not MCP transport attestation. Retry the owning operation after acceptance; its authority and version checks still apply.'
|
|
238
|
+
: 'Only if the host permits a blocking native control for this decision, display this exact question, all options and notices in AskUserQuestion or request_user_input. Relay only the actual returned answer with these unchanged bindings. Closing, declining, timeout, prose consent and missing answers are not native answers. Do not reopen a dismissed question in a loop or label decline as proof of host incompatibility. The relay records agent-reported provenance, not MCP transport attestation. Retry the owning operation after acceptance; its authority and version checks still apply.',
|
|
157
239
|
},
|
|
158
240
|
nextAction: reason === 'native_form_unavailable'
|
|
159
241
|
? 'This host does not advertise native MCP forms. The decision remains pending. Explain the limitation and continue only independently authorized work. Use hostFallback only with a permitted blocking native control; otherwise keep pending. Never substitute an asynchronous question.'
|
|
160
242
|
: 'The decision remains pending without expiry. Call questionnaire.resume with this questionnaireId to show the same question again. Do not infer an answer or continue dependent work.',
|
|
161
243
|
});
|
|
162
244
|
}
|
|
163
|
-
function answered(record, answer, replayed) {
|
|
245
|
+
function answered(record, answer, replayed, retry) {
|
|
246
|
+
const key = record.questions ? 'answers' : 'answer';
|
|
164
247
|
return result({
|
|
165
248
|
questionnaireId: record.questionnaireId,
|
|
166
249
|
status: 'answered',
|
|
167
250
|
answerAvailable: answer !== undefined,
|
|
168
|
-
...(answer ? { answer } : {}),
|
|
251
|
+
...(answer ? { [key]: answer } : {}),
|
|
169
252
|
replayed,
|
|
170
253
|
...(record.answerSource ? { answerSource: record.answerSource } : {}),
|
|
254
|
+
...(retry ? { retry } : {}),
|
|
171
255
|
...(!answer
|
|
172
256
|
? {
|
|
173
257
|
nextAction: 'The free-text answer was accepted earlier but was not stored for privacy. It cannot be recovered or inferred. Use the original answer if still present in this conversation; otherwise collect it again through a new questionnaire or supported native input before dependent work.',
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const read = { readOnlyHint: true, openWorldHint: false }; // receipts and registry upkeep still read
|
|
2
|
+
const write = {
|
|
3
|
+
readOnlyHint: false,
|
|
4
|
+
destructiveHint: false,
|
|
5
|
+
openWorldHint: false,
|
|
6
|
+
};
|
|
7
|
+
const repeatableWrite = { ...write, idempotentHint: true };
|
|
8
|
+
const destructive = {
|
|
9
|
+
readOnlyHint: false,
|
|
10
|
+
destructiveHint: true,
|
|
11
|
+
openWorldHint: false,
|
|
12
|
+
};
|
|
13
|
+
const outward = {
|
|
14
|
+
readOnlyHint: false,
|
|
15
|
+
destructiveHint: false,
|
|
16
|
+
openWorldHint: true,
|
|
17
|
+
};
|
|
18
|
+
export const toolAnnotations = {
|
|
19
|
+
'project.git_preferences': read,
|
|
20
|
+
'project.set_git_preferences': write,
|
|
21
|
+
'worktree.policy': read,
|
|
22
|
+
'worktree.set_policy': write,
|
|
23
|
+
'worktree.list': read,
|
|
24
|
+
'worktree.reconcile': destructive, // releaseUnowned hands a recovered checkout back
|
|
25
|
+
'task.heartbeat': repeatableWrite,
|
|
26
|
+
'task.pause': write,
|
|
27
|
+
'worktree.release': destructive,
|
|
28
|
+
'audit.list': read,
|
|
29
|
+
'questionnaire.ask': repeatableWrite,
|
|
30
|
+
'questionnaire.resume': repeatableWrite, // records an accepted answer
|
|
31
|
+
'questionnaire.answer_from_host': repeatableWrite,
|
|
32
|
+
'questionnaire.withdraw': destructive,
|
|
33
|
+
'session.entry': read,
|
|
34
|
+
'session.set_decision': repeatableWrite,
|
|
35
|
+
'session.decline_update': repeatableWrite,
|
|
36
|
+
'session.answer_shadow_notice': repeatableWrite,
|
|
37
|
+
'session.bootstrap': write,
|
|
38
|
+
'session.resume': write,
|
|
39
|
+
'context.prepare_change': write,
|
|
40
|
+
'context.refresh': write,
|
|
41
|
+
'project.inspect': read,
|
|
42
|
+
'project.onboard': write,
|
|
43
|
+
'project.onboarding_status': read,
|
|
44
|
+
'project.rework_plan': write,
|
|
45
|
+
'project.initialize': write,
|
|
46
|
+
'memory.source': read,
|
|
47
|
+
'memory.sync_start': repeatableWrite,
|
|
48
|
+
'memory.sync_status': read,
|
|
49
|
+
'memory.sync_inventory': write,
|
|
50
|
+
'memory.sync_plan': write,
|
|
51
|
+
'memory.sync_next': write,
|
|
52
|
+
'memory.sync_submit': write,
|
|
53
|
+
'memory.sync_reopen': write,
|
|
54
|
+
'memory.sync_delta': read,
|
|
55
|
+
'memory.sync_verify': repeatableWrite,
|
|
56
|
+
'memory.catalog': read,
|
|
57
|
+
'memory.sources': read,
|
|
58
|
+
'memory.source_targets': read,
|
|
59
|
+
'memory.publish_task': write,
|
|
60
|
+
'memory.read_revisions': read,
|
|
61
|
+
'memory.review_batch': write,
|
|
62
|
+
'memory.query': read,
|
|
63
|
+
'memory.history': read,
|
|
64
|
+
'memory.propose_revision': write,
|
|
65
|
+
'memory.list_proposals': read,
|
|
66
|
+
'memory.review_proposal': write,
|
|
67
|
+
'task.checkpoint': write,
|
|
68
|
+
'task.record_correction': write,
|
|
69
|
+
'task.self_review': write,
|
|
70
|
+
'task.reconcile': write,
|
|
71
|
+
'task.resolve_pending_delivery': destructive, // discards a pending delivery
|
|
72
|
+
'task.verify': write,
|
|
73
|
+
'task.close': write,
|
|
74
|
+
'task.abandon': destructive,
|
|
75
|
+
'task.branch': outward, // fetches the chosen base from the Git remote
|
|
76
|
+
'architecture.plan': read,
|
|
77
|
+
'architecture.module': read,
|
|
78
|
+
'architecture.record_application': write,
|
|
79
|
+
'organization.list': read,
|
|
80
|
+
'organization.create': write,
|
|
81
|
+
'organization.update': write,
|
|
82
|
+
'organization.member_list': read,
|
|
83
|
+
'organization.member_upsert': write,
|
|
84
|
+
'project.setup': write,
|
|
85
|
+
'project.list': read,
|
|
86
|
+
'project.resolve': repeatableWrite, // binds an explicitly selected project
|
|
87
|
+
'project.archive': destructive,
|
|
88
|
+
'project.restore': write,
|
|
89
|
+
'project.member_add': write,
|
|
90
|
+
'project.member_list': read,
|
|
91
|
+
'work_item.create': write,
|
|
92
|
+
'work_item.update': write,
|
|
93
|
+
'work_item.archive': destructive,
|
|
94
|
+
'work_item.restore': write,
|
|
95
|
+
'work_item.runs': read,
|
|
96
|
+
'work_item.record_test': write,
|
|
97
|
+
'work_item.test_results': read,
|
|
98
|
+
'work_item.plan': read,
|
|
99
|
+
'work_item.confirm_plan': write,
|
|
100
|
+
'project.update': write,
|
|
101
|
+
'work_item.list': read,
|
|
102
|
+
'work_item.get': read,
|
|
103
|
+
'project.clone': outward,
|
|
104
|
+
'project.link': write,
|
|
105
|
+
'project.unlink': destructive,
|
|
106
|
+
'project.links': read,
|
|
107
|
+
'auth.status': read,
|
|
108
|
+
'auth.signin_browser': outward,
|
|
109
|
+
'auth.logout': destructive,
|
|
110
|
+
};
|
|
111
|
+
//# sourceMappingURL=tool-annotations.js.map
|