engineering-memory 1.11.11 → 1.11.13
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 +2 -2
- package/package.json +1 -1
- package/runtime/build.json +1 -1
- package/runtime/dist/src/mcp/onboarding-tools.js +833 -154
- package/runtime/dist/src/mcp/questionnaire-tools.js +55 -16
- package/runtime/dist/src/mcp/tool-definitions.js +107 -24
- package/runtime/dist/src/mcp/worktree-tools.js +349 -91
- package/runtime/dist/src/runtime/bridge-service.js +127 -19
- package/runtime/dist/src/runtime/create-bridge-service.js +2 -0
- package/runtime/dist/src/runtime/language-store.js +25 -0
- package/runtime/dist/src/runtime/offline-outbox.js +4 -4
- package/runtime/dist/src/runtime/privacy-detector.js +14 -5
- package/runtime/dist/src/runtime/questionnaire-store.js +26 -7
- package/runtime/dist/src/runtime/task-start.js +115 -33
- package/skill/references/questionnaires.md +8 -3
|
@@ -31,6 +31,13 @@ export async function resumeQuestionnaire(server, service, input, context) {
|
|
|
31
31
|
}
|
|
32
32
|
export async function answerQuestionnaireFromHost(service, input) {
|
|
33
33
|
try {
|
|
34
|
+
if (input.answer?.choice === explain) {
|
|
35
|
+
const record = await service.questionnaireResume(input);
|
|
36
|
+
if (record.status === 'pending' &&
|
|
37
|
+
record.requestKey === input.requestKey &&
|
|
38
|
+
record.contentHash === input.contentHash)
|
|
39
|
+
return pending(record, 'needs_explanation');
|
|
40
|
+
}
|
|
34
41
|
const resolved = await service.questionnaireAnswerFromHost(input);
|
|
35
42
|
return answered(resolved.record, resolved.answer, resolved.replayed, resolved.retry);
|
|
36
43
|
}
|
|
@@ -59,6 +66,10 @@ async function present(server, service, record, repoRoot, context, preparation,
|
|
|
59
66
|
const response = inputResponse(responses, record.requestKey);
|
|
60
67
|
if (response.kind === 'elicit' && response.action === 'accept') {
|
|
61
68
|
const content = acceptedContent(responses, record.requestKey);
|
|
69
|
+
if (content?.choice === explain || content?.[explain] === true) {
|
|
70
|
+
consumedResponses(context).add(record.requestKey);
|
|
71
|
+
return pending(record, 'needs_explanation');
|
|
72
|
+
}
|
|
62
73
|
const parsed = questionnaireAnswerSchema(record).safeParse(record.questions && content ? formAnswers(record, content) : content);
|
|
63
74
|
const answer = parsed.success ? parsed.data : undefined;
|
|
64
75
|
if (!answer)
|
|
@@ -85,10 +96,9 @@ async function present(server, service, record, repoRoot, context, preparation,
|
|
|
85
96
|
const elicitation = capabilities?.elicitation;
|
|
86
97
|
if (!elicitation || (!elicitation.form && elicitation.url !== undefined))
|
|
87
98
|
return pending(record, 'native_form_unavailable');
|
|
88
|
-
const copy =
|
|
99
|
+
const copy = copyFor(record);
|
|
89
100
|
if (record.questions) {
|
|
90
101
|
const properties = {};
|
|
91
|
-
const required = [];
|
|
92
102
|
for (const sub of record.questions) {
|
|
93
103
|
properties[sub.id] = {
|
|
94
104
|
type: 'string',
|
|
@@ -96,7 +106,6 @@ async function present(server, service, record, repoRoot, context, preparation,
|
|
|
96
106
|
enum: sub.options.map((option) => option.id),
|
|
97
107
|
enumNames: sub.options.map((option) => option.label),
|
|
98
108
|
};
|
|
99
|
-
required.push(sub.id);
|
|
100
109
|
if (sub.textField)
|
|
101
110
|
properties[`${sub.id}_text`] = {
|
|
102
111
|
type: 'string',
|
|
@@ -105,12 +114,13 @@ async function present(server, service, record, repoRoot, context, preparation,
|
|
|
105
114
|
maxLength: sub.textField.maxLength,
|
|
106
115
|
};
|
|
107
116
|
}
|
|
117
|
+
properties[explain] = { type: 'boolean', title: copy.explain };
|
|
108
118
|
return inputRequired({
|
|
109
119
|
inputRequests: {
|
|
110
120
|
[record.requestKey]: inputRequired.elicit({
|
|
111
121
|
mode: 'form',
|
|
112
122
|
message: presentationMessage(record),
|
|
113
|
-
requestedSchema: { type: 'object', properties
|
|
123
|
+
requestedSchema: { type: 'object', properties },
|
|
114
124
|
}),
|
|
115
125
|
},
|
|
116
126
|
});
|
|
@@ -129,10 +139,12 @@ async function present(server, service, record, repoRoot, context, preparation,
|
|
|
129
139
|
enum: [
|
|
130
140
|
...record.options.map((option) => option.id),
|
|
131
141
|
...(record.allowFreeText ? ['__other__'] : []),
|
|
142
|
+
explain,
|
|
132
143
|
],
|
|
133
144
|
enumNames: [
|
|
134
145
|
...record.options.map((option) => option.label),
|
|
135
146
|
...(record.allowFreeText ? [copy.other] : []),
|
|
147
|
+
copy.explain,
|
|
136
148
|
],
|
|
137
149
|
},
|
|
138
150
|
...(record.allowFreeText
|
|
@@ -162,6 +174,7 @@ async function present(server, service, record, repoRoot, context, preparation,
|
|
|
162
174
|
},
|
|
163
175
|
});
|
|
164
176
|
}
|
|
177
|
+
const explain = '__explain__';
|
|
165
178
|
const consumed = new WeakMap();
|
|
166
179
|
function consumedResponses(context) {
|
|
167
180
|
const keys = consumed.get(context.mcpReq) ?? new Set();
|
|
@@ -173,7 +186,9 @@ const questionnaireCopy = {
|
|
|
173
186
|
pending: 'Choose an answer to resolve this decision. Closing or cancelling the form leaves it pending.',
|
|
174
187
|
freeText: ' For another answer choose Other and fill in text. Free text is not stored and cannot be replayed.',
|
|
175
188
|
storedText: ' The bounded text field is stored as part of this exact decision and is replayed after restart.',
|
|
176
|
-
|
|
189
|
+
everyQuestion: ' Answer every question before submitting, unless you pick an option that ends the form.',
|
|
190
|
+
example: 'Example',
|
|
191
|
+
explain: 'I did not understand; explain in more detail first',
|
|
177
192
|
answer: 'Answer',
|
|
178
193
|
other: 'Other',
|
|
179
194
|
otherAnswer: 'Other answer',
|
|
@@ -182,7 +197,9 @@ const questionnaireCopy = {
|
|
|
182
197
|
pending: 'Kararını kaydetmek için bir seçenek seç. Formu kapatmak veya iptal etmek onay sayılmaz; soru beklemede kalır.',
|
|
183
198
|
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.',
|
|
184
199
|
storedText: ' Bu metin alanındaki yanıt kararınla birlikte saklanır; yeniden başlattığında korunur.',
|
|
185
|
-
|
|
200
|
+
everyQuestion: ' Göndermeden önce her soruyu cevapla; formu bitiren bir seçenek seçtiysen gerekmez.',
|
|
201
|
+
example: 'Örnek',
|
|
202
|
+
explain: 'Anlamadım; önce daha ayrıntılı anlat',
|
|
186
203
|
answer: 'Yanıt',
|
|
187
204
|
other: 'Diğer',
|
|
188
205
|
otherAnswer: 'Diğer yanıt',
|
|
@@ -204,11 +221,29 @@ function formAnswers(record, content) {
|
|
|
204
221
|
return answers;
|
|
205
222
|
}
|
|
206
223
|
function presentationMessage(record) {
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
224
|
+
const copy = copyFor(record);
|
|
225
|
+
const explained = (item) => [
|
|
226
|
+
item.context,
|
|
227
|
+
item.example && `${copy.example}: ${item.example}`,
|
|
228
|
+
(item.options ?? [])
|
|
229
|
+
.filter((option) => option.description)
|
|
230
|
+
.map((option) => `${option.label}: ${option.description}`)
|
|
231
|
+
.join('\n'),
|
|
232
|
+
];
|
|
233
|
+
return [
|
|
234
|
+
record.message,
|
|
235
|
+
...explained(record),
|
|
236
|
+
...(record.questions ?? []).flatMap((sub) => {
|
|
237
|
+
const details = explained(sub).filter(Boolean);
|
|
238
|
+
return details.length ? [sub.message, ...details] : [];
|
|
239
|
+
}),
|
|
240
|
+
`${copy.pending}${record.questions ? copy.everyQuestion : ''}${record.allowFreeText ? copy.freeText : ''}${record.textField ? copy.storedText : ''}`,
|
|
241
|
+
]
|
|
242
|
+
.filter(Boolean)
|
|
243
|
+
.join('\n\n');
|
|
244
|
+
}
|
|
245
|
+
function copyFor(record) {
|
|
246
|
+
return questionnaireCopy[record.language?.startsWith('tr') ? 'tr' : 'en'];
|
|
212
247
|
}
|
|
213
248
|
function pending(record, reason) {
|
|
214
249
|
return result({
|
|
@@ -233,13 +268,17 @@ function pending(record, reason) {
|
|
|
233
268
|
allowFreeText: record.allowFreeText,
|
|
234
269
|
...(record.textField ? { textField: record.textField } : {}),
|
|
235
270
|
}),
|
|
236
|
-
instructions: record.questions
|
|
271
|
+
instructions: `${record.questions
|
|
237
272
|
? '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.',
|
|
273
|
+
: '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.'} Show each option's description under its label. If the person says they did not understand, including through the control's free-text answer, that is not an answer: call questionnaire.answer_from_host with answer {choice: "${explain}"} and the same bindings, and the decision stays pending.${(record.options?.length ?? 0) > 4
|
|
274
|
+
? ' The host control shows fewer options than this question has: show them three at a time with one more option that reveals the rest, and relay only the final pick.'
|
|
275
|
+
: ''}`,
|
|
239
276
|
},
|
|
240
|
-
nextAction: reason === '
|
|
241
|
-
? '
|
|
242
|
-
:
|
|
277
|
+
nextAction: reason === 'needs_explanation'
|
|
278
|
+
? 'The person asked for more detail and the decision is still pending. Explain in chat, in their language, why this is asked, what each option does next and one concrete example. Then show the same question once more with questionnaire.resume. Do not infer an answer.'
|
|
279
|
+
: reason === 'native_form_unavailable'
|
|
280
|
+
? '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.'
|
|
281
|
+
: '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.',
|
|
243
282
|
});
|
|
244
283
|
}
|
|
245
284
|
function answered(record, answer, replayed, retry) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as z from 'zod/v4';
|
|
2
2
|
import { validationIds } from '../runtime/bridge-service.js';
|
|
3
|
-
import {
|
|
3
|
+
import { stableStringify } from '../utilities/hash.js';
|
|
4
|
+
import { hostAnswerSchema, languageTag, questionnaireDefinitionSchema, } from '../runtime/questionnaire-store.js';
|
|
4
5
|
import { answerChoice, answerQuestionnaireFromHost, askQuestionnaire, resumeQuestionnaire, } from './questionnaire-tools.js';
|
|
5
6
|
const optionalRepoRoot = z.string().min(1).optional();
|
|
6
7
|
const stringList = z.array(z.string().min(1));
|
|
@@ -156,6 +157,38 @@ const reconciliationEntry = z.object({
|
|
|
156
157
|
.describe('Optional for approved_revision; resolved from the approved proposal when omitted.'),
|
|
157
158
|
reason: z.string().optional(),
|
|
158
159
|
});
|
|
160
|
+
const waiverCopy = {
|
|
161
|
+
en: {
|
|
162
|
+
ask: (task) => `${task}: skip the UI check (widget, golden or screenshot test) for this task?`,
|
|
163
|
+
files: (listed) => `Changed files that draw the interface: ${listed}`,
|
|
164
|
+
reason: (reason) => `The reason you gave: ${reason}`,
|
|
165
|
+
context: 'The UI check is what proves the screens this task changed still render. Skipping it means nothing in this task opens them, and the skip is stored in the task audit trail with the reason above.',
|
|
166
|
+
example: 'A layout this change broke would pass verification and first show up when someone opens the app.',
|
|
167
|
+
waive: [
|
|
168
|
+
'Skip the UI check',
|
|
169
|
+
'The task verifies without a widget, golden or screenshot test of these files, and the skip is recorded.',
|
|
170
|
+
],
|
|
171
|
+
require: [
|
|
172
|
+
'Keep the UI check required',
|
|
173
|
+
'Nothing is skipped. The task stays unverified until a widget, golden or screenshot test covers these files.',
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
tr: {
|
|
177
|
+
ask: (task) => `${task}: bu görev için UI kontrolünü (widget, golden veya ekran görüntüsü testi) atlayalım mı?`,
|
|
178
|
+
files: (listed) => `Arayüzü çizen değişen dosyalar: ${listed}`,
|
|
179
|
+
reason: (reason) => `Verdiğin gerekçe: ${reason}`,
|
|
180
|
+
context: 'UI kontrolü, bu görevde değişen ekranların hâlâ doğru çizildiğini gösteren testtir. Atlarsan bu görevde o ekranları açan hiçbir test kalmaz ve atlama kararı yukarıdaki gerekçesiyle görevin denetim kaydına yazılır.',
|
|
181
|
+
example: 'Bu değişiklikte bozulan bir yerleşim doğrulamadan geçer ve ancak uygulamayı açan biri fark ettiğinde ortaya çıkar.',
|
|
182
|
+
waive: [
|
|
183
|
+
'UI kontrolünü atla',
|
|
184
|
+
'Görev, bu dosyalar için widget, golden veya ekran görüntüsü testi olmadan doğrulanır ve atlama kaydedilir.',
|
|
185
|
+
],
|
|
186
|
+
require: [
|
|
187
|
+
'UI kontrolü zorunlu kalsın',
|
|
188
|
+
'Hiçbir şey atlanmaz. Bu dosyaları kapsayan bir widget, golden veya ekran görüntüsü testi gelene kadar görev doğrulanmaz.',
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
};
|
|
159
192
|
export function registerQuestionnaireTools(server, service) {
|
|
160
193
|
server.registerTool('questionnaire.answer_from_host', {
|
|
161
194
|
description: 'Relay an actual answer from a host-permitted blocking native AskUserQuestion or request_user_input control to the exact durable question. First read hostFallback from the pending operation or questionnaire.resume with presentation host_native, display its unchanged question/options/notices and await the real native result. Never infer answers, use chat consent, asynchronous controls, defaults or a declined/dismissed form. The hostTool field is agent-reported provenance, not transport-verified attestation. Bind requestKey and contentHash exactly; changed scope, withdrawn questions, invalid or conflicting answers are refused. This only saves the answer; retry the owning operation for current authority and version checks.',
|
|
@@ -195,13 +228,40 @@ export function registerQuestionnaireTools(server, service) {
|
|
|
195
228
|
}
|
|
196
229
|
});
|
|
197
230
|
server.registerTool('questionnaire.ask', {
|
|
198
|
-
description: 'Persist a required decision before displaying a native questionnaire. Write the message
|
|
231
|
+
description: 'Persist a required decision before displaying a native questionnaire. Write the message, context, example, labels and option descriptions in the language the user writes in, and set language to its BCP-47 tag (tr, en, pt-BR); it is remembered, and the call is refused while no language is known. Say why the decision is asked in context, give one concrete example of what it changes in example, and give every option a description of what happens next. Show a record by its title, never by an id or a hash. 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 questionnaire.ask arguments; use questionnaire.answer_from_host only after an actual permitted blocking native answer. 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. After the first decline in this session, pass presentation host_native to skip the doomed elicitation round trip.',
|
|
199
232
|
inputSchema: questionnaireDefinitionSchema.extend({
|
|
200
233
|
repoRoot: optionalRepoRoot,
|
|
201
234
|
preparation: z.boolean().optional(),
|
|
202
235
|
presentation: z.literal('host_native').optional(),
|
|
203
236
|
}),
|
|
204
|
-
}, async (input, context) =>
|
|
237
|
+
}, async (input, context) => {
|
|
238
|
+
if (!(await service.language(input.language)))
|
|
239
|
+
return toolResult({
|
|
240
|
+
ok: false,
|
|
241
|
+
error: {
|
|
242
|
+
kind: 'bridge_error',
|
|
243
|
+
message: 'Set language to the BCP-47 tag of the language the user is writing in, such as tr, en or pt-BR, and call questionnaire.ask again with otherwise identical arguments. It is remembered, so this is asked once.',
|
|
244
|
+
retryable: true,
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
const shown = [input, ...(input.questions ?? [])].flatMap((question) => [
|
|
248
|
+
question.message,
|
|
249
|
+
question.context,
|
|
250
|
+
question.example,
|
|
251
|
+
question.textField?.title,
|
|
252
|
+
...(question.options ?? []).flatMap((option) => [option.label, option.description]),
|
|
253
|
+
]);
|
|
254
|
+
if (shown.some((text) => /\b[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\b|[0-9a-f]{32,}/i.test(text?.replace(/`[^`]*`/g, '') ?? '')))
|
|
255
|
+
return toolResult({
|
|
256
|
+
ok: false,
|
|
257
|
+
error: {
|
|
258
|
+
kind: 'bridge_error',
|
|
259
|
+
message: 'The question shows an identifier or a hash where the person needs a name. Write the title of the record, branch or project instead; an identifier that has to be shown goes inside backticks. If this exact question is already pending, show it with questionnaire.resume instead.',
|
|
260
|
+
retryable: true,
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
return await askQuestionnaire(server, service, input, context);
|
|
264
|
+
});
|
|
205
265
|
server.registerTool('questionnaire.resume', {
|
|
206
266
|
description: 'Display the original durable questionnaire after interruption or return its saved accepted choice. No timeout or dismissal resolves a pending question. A saved free-text receipt cannot replay the text and must never be treated as a recovered answer. presentation host_native returns the original question and bindings for a permitted blocking host control without reopening the MCP form.',
|
|
207
267
|
inputSchema: z.strictObject({
|
|
@@ -240,7 +300,12 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
240
300
|
}, async (input) => toolResult(await service.auditList(input)));
|
|
241
301
|
server.registerTool('session.entry', {
|
|
242
302
|
description: 'The first call of every session in a repository, before answering anything about the project. Reports whether the user is signed in, whether this repository is bound, what this user decided about it last time, and the one thing to do now. A repository the user switched Engineering Memory off in reports that, and is left alone.',
|
|
243
|
-
inputSchema: z.object({
|
|
303
|
+
inputSchema: z.object({
|
|
304
|
+
repoRoot: optionalRepoRoot,
|
|
305
|
+
language: languageTag
|
|
306
|
+
.optional()
|
|
307
|
+
.describe('BCP-47 tag of the language the user is writing in, such as tr, en or pt-BR. It is remembered and every later form opens in it.'),
|
|
308
|
+
}),
|
|
244
309
|
}, async (input) => toolResult(await service.sessionEntry(input)));
|
|
245
310
|
server.registerTool('session.set_decision', {
|
|
246
311
|
description: 'Record what the user decided about Engineering Memory in this repository: bound once they have chosen a project, disabled when they ask for it off, or none to forget the decision when they ask for it back. Call it only from something the user actually said; it is the only thing that changes the entry state.',
|
|
@@ -445,11 +510,11 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
445
510
|
}),
|
|
446
511
|
}, async (input) => toolResult(await service.taskRecordCorrection(input)));
|
|
447
512
|
server.registerTool('task.self_review', {
|
|
448
|
-
description: 'Record that each changed file was read back against the rules that govern it: one entry per changed file (deleted files excepted), naming every rule context.prepare_change returned for that path in governingRules, and for a file no role maps, the engineering rules you read it against. Each rule gets an outcome: follows; fixed, with the issue and the change you made; or user_accepted_deviation, with the rule resourceKey and the issue. A rule conflict is a question for the user, never a judgment call: for every deviation this tool asks the user natively whether to keep the code, records the deviation only on their approval, and refuses the review when they choose to change the code. Matching the surrounding code is not an outcome; existing code is evidence, not authority. Set language to the
|
|
513
|
+
description: 'Record that each changed file was read back against the rules that govern it: one entry per changed file (deleted files excepted), naming every rule context.prepare_change returned for that path in governingRules, and for a file no role maps, the engineering rules you read it against. Each rule gets an outcome: follows; fixed, with the issue and the change you made; or user_accepted_deviation, with the rule resourceKey and the issue. A rule conflict is a question for the user, never a judgment call: for every deviation this tool asks the user natively whether to keep the code, records the deviation only on their approval, and refuses the review when they choose to change the code. Matching the surrounding code is not an outcome; existing code is evidence, not authority. Set language to the BCP-47 tag of the language the user writes in (tr, en, pt-BR); it is remembered. Returns a durable pending receipt without waiting for backend delivery; task.verify refuses until a review covers the current diff, so any further edit requires reviewing again.',
|
|
449
514
|
inputSchema: z.object({
|
|
450
515
|
repoRoot: optionalRepoRoot,
|
|
451
516
|
taskId: z.string().min(1),
|
|
452
|
-
language:
|
|
517
|
+
language: languageTag.optional(),
|
|
453
518
|
files: z
|
|
454
519
|
.array(z.object({
|
|
455
520
|
path: z.string().min(1).max(512),
|
|
@@ -478,19 +543,27 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
478
543
|
.max(500),
|
|
479
544
|
}),
|
|
480
545
|
}, async (input, context) => {
|
|
546
|
+
const language = (await service.language(input.language))?.startsWith('tr')
|
|
547
|
+
? 'tr'
|
|
548
|
+
: 'en';
|
|
549
|
+
const review = { ...input, language };
|
|
481
550
|
let questions;
|
|
482
551
|
try {
|
|
483
|
-
questions = service.ruleDeviationQuestions(
|
|
552
|
+
questions = await service.ruleDeviationQuestions(review);
|
|
484
553
|
}
|
|
485
554
|
catch {
|
|
486
|
-
return toolResult(await service.taskSelfReview(
|
|
555
|
+
return toolResult(await service.taskSelfReview(review));
|
|
487
556
|
}
|
|
488
557
|
for (const { definition, previousDefinitions } of questions) {
|
|
489
|
-
const
|
|
558
|
+
const asked = { repoRoot: input.repoRoot, questionnaireId: definition.questionnaireId };
|
|
559
|
+
const stored = await service.questionnaireResume(asked).catch(() => undefined);
|
|
560
|
+
const form = stored && stableStringify(stored.binding) === stableStringify(definition.binding)
|
|
561
|
+
? await resumeQuestionnaire(server, service, asked, context)
|
|
562
|
+
: await askQuestionnaire(server, service, { ...definition, repoRoot: input.repoRoot }, context, previousDefinitions);
|
|
490
563
|
if (!answerChoice(form))
|
|
491
564
|
return form;
|
|
492
565
|
}
|
|
493
|
-
return toolResult(await service.taskSelfReview(
|
|
566
|
+
return toolResult(await service.taskSelfReview(review));
|
|
494
567
|
});
|
|
495
568
|
server.registerTool('task.reconcile', {
|
|
496
569
|
description: 'Reconcile changed screens and components with an approved revision or an explicit no-semantic-memory-change reason. Pass every record the task touched as `entries` in one call rather than calling once per record; the whole set is applied together and rejected together. After memory.review_proposal approves a proposal, pass its reconcileEntry ({resourceId, type: approved_revision, proposalId, revisionId}) straight through here — revisionId is optional and is resolved from the approved proposal when omitted. A pending result is a durable local receipt; continue independent work without polling. Required delivery is checked before verification.',
|
|
@@ -567,10 +640,9 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
567
640
|
repoRoot: optionalRepoRoot,
|
|
568
641
|
taskId: z.string().min(1),
|
|
569
642
|
sessionId: z.string().min(1),
|
|
570
|
-
language:
|
|
571
|
-
.enum(['tr', 'en'])
|
|
643
|
+
language: languageTag
|
|
572
644
|
.optional()
|
|
573
|
-
.describe('
|
|
645
|
+
.describe('BCP-47 tag of the language the user writes in (tr, en, pt-BR), for the waiver question. It is remembered.'),
|
|
574
646
|
leaseId: z.string().min(1).optional(),
|
|
575
647
|
changedPaths: stringList.min(1).optional(),
|
|
576
648
|
validations: z
|
|
@@ -603,7 +675,8 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
603
675
|
.describe('Only when the user decides a required ui check is not needed for this change. Never inferred from prose; the tool asks the user and verifies only after they agree.'),
|
|
604
676
|
}),
|
|
605
677
|
}, async (input, context) => {
|
|
606
|
-
const { language
|
|
678
|
+
const { language: told, ...request } = input;
|
|
679
|
+
const language = (await service.language(told))?.startsWith('tr') ? 'tr' : 'en';
|
|
607
680
|
for (const waiver of request.waivers ?? []) {
|
|
608
681
|
const subject = await service.validationWaiverSubject({
|
|
609
682
|
repoRoot: request.repoRoot,
|
|
@@ -615,7 +688,21 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
615
688
|
return toolResult(subject);
|
|
616
689
|
const { questionnaireId, externalTaskId, paths, reason } = subject.data;
|
|
617
690
|
const listed = paths.slice(0, 20).join(', ') + (paths.length > 20 ? ` (+${paths.length - 20})` : '');
|
|
618
|
-
const definitions = [
|
|
691
|
+
const definitions = ['tr', 'en'].map((displayLanguage) => {
|
|
692
|
+
const copy = waiverCopy[displayLanguage];
|
|
693
|
+
return {
|
|
694
|
+
questionnaireId,
|
|
695
|
+
language: displayLanguage,
|
|
696
|
+
message: [copy.ask(externalTaskId), copy.files(listed), copy.reason(reason)].join('\n'),
|
|
697
|
+
context: copy.context,
|
|
698
|
+
example: copy.example,
|
|
699
|
+
options: [
|
|
700
|
+
{ id: 'waive', label: copy.waive[0], description: copy.waive[1] },
|
|
701
|
+
{ id: 'require', label: copy.require[0], description: copy.require[1] },
|
|
702
|
+
],
|
|
703
|
+
};
|
|
704
|
+
});
|
|
705
|
+
const earlier = [
|
|
619
706
|
{
|
|
620
707
|
questionnaireId,
|
|
621
708
|
language: 'tr',
|
|
@@ -638,7 +725,7 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
638
725
|
const form = await askQuestionnaire(server, service, {
|
|
639
726
|
...definitions.find((definition) => definition.language === language),
|
|
640
727
|
repoRoot: request.repoRoot,
|
|
641
|
-
}, context, definitions);
|
|
728
|
+
}, context, [...definitions, ...earlier]);
|
|
642
729
|
const record = await service.questionnaireResume({
|
|
643
730
|
repoRoot: request.repoRoot,
|
|
644
731
|
questionnaireId,
|
|
@@ -843,15 +930,13 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
843
930
|
}),
|
|
844
931
|
}, async (input) => toolResult(await service.workItemCreate(input)));
|
|
845
932
|
server.registerTool('work_item.update', {
|
|
846
|
-
description:
|
|
933
|
+
description: "Edit or assign a work item at its current version. Assignees must already be active project members; this does not grant project access. Status is a slug from the project's own status catalogue; a backend that still keeps the six legacy values (backlog, ready, in_progress, in_review, done, cancelled) refuses anything else and names what it accepts.",
|
|
847
934
|
inputSchema: z.object({
|
|
848
935
|
...workItemLocator,
|
|
849
936
|
data: z.object({
|
|
850
937
|
...workItemFields,
|
|
851
938
|
expectedVersion: z.number().int().min(1),
|
|
852
|
-
status: z
|
|
853
|
-
.enum(['backlog', 'ready', 'in_progress', 'in_review', 'done', 'cancelled'])
|
|
854
|
-
.optional(),
|
|
939
|
+
status: z.string().trim().min(1).max(64).optional(),
|
|
855
940
|
}),
|
|
856
941
|
}),
|
|
857
942
|
}, async (input) => toolResult(await service.workItemUpdate(input)));
|
|
@@ -929,12 +1014,10 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
929
1014
|
}),
|
|
930
1015
|
}, async (input) => toolResult(await service.projectUpdate(input)));
|
|
931
1016
|
server.registerTool('work_item.list', {
|
|
932
|
-
description:
|
|
1017
|
+
description: "List selectable work items for a project before opening an engineering run in this chat. The status filter is a slug from the project's own status catalogue; a backend that still keeps the six legacy values (backlog, ready, in_progress, in_review, done, cancelled) refuses anything else and names what it accepts.",
|
|
933
1018
|
inputSchema: z.object({
|
|
934
1019
|
projectId: z.string().uuid(),
|
|
935
|
-
status: z
|
|
936
|
-
.enum(['backlog', 'ready', 'in_progress', 'in_review', 'done', 'cancelled'])
|
|
937
|
-
.optional(),
|
|
1020
|
+
status: z.string().trim().min(1).max(64).optional(),
|
|
938
1021
|
priority: z.enum(['lowest', 'low', 'medium', 'high', 'highest']).optional(),
|
|
939
1022
|
assigneeUserId: z.string().uuid().optional(),
|
|
940
1023
|
includeArchived: z.boolean().optional(),
|