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.
Files changed (34) hide show
  1. package/dispatcher/sections.mjs +6 -2
  2. package/install/codex-approval.mjs +457 -0
  3. package/install/files.mjs +5 -1
  4. package/install/installer.mjs +24 -1
  5. package/package.json +1 -1
  6. package/runtime/build.json +1 -1
  7. package/runtime/dist/src/config.js +1 -0
  8. package/runtime/dist/src/git/git-inspector.js +82 -3
  9. package/runtime/dist/src/git/verification-gate.js +30 -17
  10. package/runtime/dist/src/mcp/onboarding-tools.js +3 -16
  11. package/runtime/dist/src/mcp/questionnaire-tools.js +99 -15
  12. package/runtime/dist/src/mcp/tool-annotations.js +111 -0
  13. package/runtime/dist/src/mcp/tool-definitions.js +148 -28
  14. package/runtime/dist/src/mcp/worktree-tools.js +323 -236
  15. package/runtime/dist/src/project/repository.js +11 -5
  16. package/runtime/dist/src/runtime/active-context-store.js +3 -0
  17. package/runtime/dist/src/runtime/branch-preferences.js +0 -19
  18. package/runtime/dist/src/runtime/bridge-service.js +1131 -220
  19. package/runtime/dist/src/runtime/offline-outbox.js +33 -158
  20. package/runtime/dist/src/runtime/phase-timer.js +26 -0
  21. package/runtime/dist/src/runtime/privacy-detector.js +263 -0
  22. package/runtime/dist/src/runtime/questionnaire-store.js +210 -49
  23. package/runtime/dist/src/runtime/recovery-error.js +3 -1
  24. package/runtime/dist/src/runtime/runtime-entry.js +8 -0
  25. package/runtime/dist/src/runtime/runtime-host.js +1 -3
  26. package/runtime/dist/src/runtime/task-branch-store.js +17 -1
  27. package/runtime/dist/src/runtime/task-start.js +281 -0
  28. package/runtime/dist/src/runtime/worktree-pool.js +334 -24
  29. package/runtime/dist/src/utilities/process.js +1 -0
  30. package/skill/SKILL.md +3 -3
  31. package/skill/references/lifecycle.md +44 -19
  32. package/skill/references/memory-updates.md +9 -3
  33. package/skill/references/project-onboarding.md +19 -2
  34. 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', 'HEAD', '--name-status', '-z', '--find-renames', '--find-copies', '--'], { cwd: repoRoot }),
363
- this.runner.run('git', ['diff', 'HEAD', '--raw', '-z', '--no-abbrev', '--'], {
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: manifest.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 !== manifest.diffHash) {
177
+ if (receipt.diffHash !== current.diffHash) {
167
178
  return {
168
179
  allowed: false,
169
180
  reason: 'git_diff_changed_after_verify',
170
- diffHash: manifest.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: manifest.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: manifest.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: manifest.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: actualSource
201
- ? {
202
- ...actualSource,
203
- ...(receipt.source?.linked ? { linked: receipt.source.linked } : {}),
204
- }
205
- : undefined,
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: manifest.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 !== manifest.diffHash) {
230
+ result.diffHash !== current.diffHash) {
218
231
  return {
219
232
  allowed: false,
220
233
  reason: 'online_commit_gate_mismatch',
221
- diffHash: manifest.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: manifest.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: manifest.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. Read all pages using the last id when a page is full. includeArchived exposes retired identities for refresh and rename preservation.',
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
- return answered(record, record.answer, true);
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 answer = acceptedContent(responses, record.requestKey, questionnaireAnswerSchema(record));
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 (responses !== undefined || context.mcpReq.droppedInputResponseKeys?.length)
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
- allowFreeText: record.allowFreeText,
155
- ...(record.textField ? { textField: record.textField } : {}),
156
- instructions: '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.',
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