engineering-memory 1.11.9 → 1.11.11
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 +7 -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 +2 -0
- package/runtime/dist/src/git/git-inspector.js +114 -4
- package/runtime/dist/src/git/verification-gate.js +30 -17
- package/runtime/dist/src/mcp/onboarding-tools.js +110 -16
- package/runtime/dist/src/mcp/questionnaire-tools.js +99 -15
- package/runtime/dist/src/mcp/tool-annotations.js +112 -0
- package/runtime/dist/src/mcp/tool-definitions.js +150 -29
- package/runtime/dist/src/mcp/worktree-tools.js +395 -236
- package/runtime/dist/src/project/repository.js +26 -8
- package/runtime/dist/src/runtime/active-context-store.js +3 -0
- package/runtime/dist/src/runtime/api-client.js +1 -0
- package/runtime/dist/src/runtime/branch-preferences.js +0 -19
- package/runtime/dist/src/runtime/bridge-service.js +1320 -246
- 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 +374 -26
- package/runtime/dist/src/utilities/process.js +1 -0
- package/skill/SKILL.md +3 -3
- package/skill/references/lifecycle.md +59 -19
- package/skill/references/memory-updates.md +9 -3
- package/skill/references/project-onboarding.md +19 -2
- package/skill/references/questionnaires.md +86 -15
|
@@ -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,112 @@
|
|
|
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.move_repository': write,
|
|
88
|
+
'project.archive': destructive,
|
|
89
|
+
'project.restore': write,
|
|
90
|
+
'project.member_add': write,
|
|
91
|
+
'project.member_list': read,
|
|
92
|
+
'work_item.create': write,
|
|
93
|
+
'work_item.update': write,
|
|
94
|
+
'work_item.archive': destructive,
|
|
95
|
+
'work_item.restore': write,
|
|
96
|
+
'work_item.runs': read,
|
|
97
|
+
'work_item.record_test': write,
|
|
98
|
+
'work_item.test_results': read,
|
|
99
|
+
'work_item.plan': read,
|
|
100
|
+
'work_item.confirm_plan': write,
|
|
101
|
+
'project.update': write,
|
|
102
|
+
'work_item.list': read,
|
|
103
|
+
'work_item.get': read,
|
|
104
|
+
'project.clone': outward,
|
|
105
|
+
'project.link': write,
|
|
106
|
+
'project.unlink': destructive,
|
|
107
|
+
'project.links': read,
|
|
108
|
+
'auth.status': read,
|
|
109
|
+
'auth.signin_browser': outward,
|
|
110
|
+
'auth.logout': destructive,
|
|
111
|
+
};
|
|
112
|
+
//# sourceMappingURL=tool-annotations.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as z from 'zod/v4';
|
|
2
2
|
import { validationIds } from '../runtime/bridge-service.js';
|
|
3
3
|
import { hostAnswerSchema, questionnaireDefinitionSchema } from '../runtime/questionnaire-store.js';
|
|
4
|
-
import { answerQuestionnaireFromHost, askQuestionnaire, resumeQuestionnaire, } from './questionnaire-tools.js';
|
|
4
|
+
import { answerChoice, answerQuestionnaireFromHost, askQuestionnaire, resumeQuestionnaire, } from './questionnaire-tools.js';
|
|
5
5
|
const optionalRepoRoot = z.string().min(1).optional();
|
|
6
6
|
const stringList = z.array(z.string().min(1));
|
|
7
7
|
const jsonValue = z.lazy(() => z.union([
|
|
@@ -114,6 +114,7 @@ export const engineeringMemoryToolNames = [
|
|
|
114
114
|
'project.setup',
|
|
115
115
|
'project.list',
|
|
116
116
|
'project.resolve',
|
|
117
|
+
'project.move_repository',
|
|
117
118
|
'project.archive',
|
|
118
119
|
'project.restore',
|
|
119
120
|
'project.member_add',
|
|
@@ -149,7 +150,10 @@ const reconciliationEntry = z.object({
|
|
|
149
150
|
resourceId: z.string().min(1),
|
|
150
151
|
type: z.enum(['approved_revision', 'no_semantic_memory_change', 'scaffold_applied']),
|
|
151
152
|
proposalId: z.string().optional(),
|
|
152
|
-
revisionId: z
|
|
153
|
+
revisionId: z
|
|
154
|
+
.string()
|
|
155
|
+
.optional()
|
|
156
|
+
.describe('Optional for approved_revision; resolved from the approved proposal when omitted.'),
|
|
153
157
|
reason: z.string().optional(),
|
|
154
158
|
});
|
|
155
159
|
export function registerQuestionnaireTools(server, service) {
|
|
@@ -191,10 +195,11 @@ export function registerQuestionnaireTools(server, service) {
|
|
|
191
195
|
}
|
|
192
196
|
});
|
|
193
197
|
server.registerTool('questionnaire.ask', {
|
|
194
|
-
description: 'Persist a required decision before displaying a native questionnaire. Write the message and labels in the conversation language and set language (tr or en) for the native form help. Use a questionnaireId unique to this decision occurrence or task, and reuse it only for identical retries. A later decision or changed wording/options requires a new id. Only a schema-validated native acceptance answers it. Dismissal, timeout and missing replies remain pending without expiry. Never supply answers in 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.',
|
|
198
|
+
description: 'Persist a required decision before displaying a native questionnaire. Write the message and labels in the conversation language and set language (tr or en) for the native form help. Use a questionnaireId unique to this decision occurrence or task, and reuse it only for identical retries. A later decision or changed wording/options requires a new id. Only a schema-validated native acceptance answers it. Dismissal, timeout and missing replies remain pending without expiry. Never supply answers in 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.',
|
|
195
199
|
inputSchema: questionnaireDefinitionSchema.extend({
|
|
196
200
|
repoRoot: optionalRepoRoot,
|
|
197
201
|
preparation: z.boolean().optional(),
|
|
202
|
+
presentation: z.literal('host_native').optional(),
|
|
198
203
|
}),
|
|
199
204
|
}, async (input, context) => await askQuestionnaire(server, service, input, context));
|
|
200
205
|
server.registerTool('questionnaire.resume', {
|
|
@@ -259,14 +264,32 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
259
264
|
}),
|
|
260
265
|
}, async (input) => toolResult(await service.sessionAnswerShadowNotice(input)));
|
|
261
266
|
server.registerTool('session.bootstrap', {
|
|
262
|
-
description: 'Authenticate, resolve the repository project, open or resume a write or read-only task, and load mandatory engineering context before planning.',
|
|
267
|
+
description: 'Authenticate, resolve the repository project, open or resume a write or read-only task, and load mandatory engineering context before planning. objective is one line of at most 240 characters. contextPack holds only what fit the response budget; deferredResources lists what did not, each entry carrying its revisionId so it can be read directly with memory.read_revisions rather than looked up again through memory.catalog.',
|
|
263
268
|
inputSchema: z.object({
|
|
264
269
|
repoRoot: optionalRepoRoot,
|
|
265
270
|
projectId: z.string().optional(),
|
|
266
|
-
externalTaskId: z
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
271
|
+
externalTaskId: z
|
|
272
|
+
.string()
|
|
273
|
+
.min(2)
|
|
274
|
+
.max(160)
|
|
275
|
+
.regex(/^[^\r\n]+$/, 'externalTaskId must be a single line of at most 160 characters.'),
|
|
276
|
+
objective: z
|
|
277
|
+
.string()
|
|
278
|
+
.min(2)
|
|
279
|
+
.max(240)
|
|
280
|
+
.regex(/^[^\r\n]+$/, 'objective must be a single line of at most 240 characters.')
|
|
281
|
+
.describe('One line, 2-240 characters: what this task delivers. Put detail in checkpoints, not here.'),
|
|
282
|
+
taskKind: z
|
|
283
|
+
.string()
|
|
284
|
+
.min(2)
|
|
285
|
+
.max(80)
|
|
286
|
+
.regex(/^[^\r\n]+$/, 'taskKind must be a single line of at most 80 characters.'),
|
|
287
|
+
workItemKey: z
|
|
288
|
+
.string()
|
|
289
|
+
.min(2)
|
|
290
|
+
.max(120)
|
|
291
|
+
.regex(/^[^\r\n]+$/, 'workItemKey must be a single line of at most 120 characters.')
|
|
292
|
+
.optional(),
|
|
270
293
|
workItemId: z.string().uuid().optional(),
|
|
271
294
|
mode: z.enum(['write', 'read_only', 'scaffold']).optional(),
|
|
272
295
|
linkedSources: z
|
|
@@ -300,14 +323,14 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
300
323
|
}),
|
|
301
324
|
}, async (input) => toolResult(await service.contextPrepareChange(input)));
|
|
302
325
|
server.registerTool('context.refresh', {
|
|
303
|
-
description: 'Refresh pins within the task fixed code source after memory approval. Use the task repoRoot; source changes require a new task.',
|
|
326
|
+
description: 'Refresh pins within the task fixed code source after memory approval. Use the task repoRoot; source changes require a new task. contextPack carries only the revisions that changed since the session pinned them; unchangedResources lists the rest with the revisionId to reread one through memory.read_revisions, and deferredResources entries carry theirs too.',
|
|
304
327
|
inputSchema: z.object({
|
|
305
328
|
repoRoot: optionalRepoRoot,
|
|
306
329
|
sessionId: z.string().min(1),
|
|
307
330
|
}),
|
|
308
331
|
}, async (input) => toolResult(await service.contextRefresh(input)));
|
|
309
332
|
server.registerTool('memory.query', {
|
|
310
|
-
description: 'Read the smallest project memory pack needed for screens, components, services, navigation, localization or state work.',
|
|
333
|
+
description: 'Read the smallest project memory pack needed for screens, components, services, navigation, localization or state work. resources holds what fits one response; deferredResources lists the rest, each with the revisionId to read it through memory.read_revisions.',
|
|
311
334
|
inputSchema: z.object({
|
|
312
335
|
projectId: z.string().min(1),
|
|
313
336
|
sessionId: z.string().min(1),
|
|
@@ -319,17 +342,19 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
319
342
|
}),
|
|
320
343
|
}, async (input) => toolResult(await service.memoryQuery(input)));
|
|
321
344
|
server.registerTool('memory.history', {
|
|
322
|
-
description: 'Read why the code you are about to change is the way it is: the revision history of the screen, component or contract records covering the given paths or
|
|
345
|
+
description: 'Read why the code you are about to change is the way it is: the revision history of the screen, component or contract records covering the given paths, keys or ids, each revision with the task that produced it and the stated reason, plus the earlier tasks that touched those records and how many corrections each of them took. Also returns the task references found in the Git history of those paths. Verification refuses while a record with earlier work on it was never read; required:true reads every such record of the current lease in one call. A path no record covers comes back in uncoveredPaths instead of an error.',
|
|
323
346
|
inputSchema: z.object({
|
|
324
347
|
repoRoot: optionalRepoRoot,
|
|
325
348
|
sessionId: z.string().min(1),
|
|
326
349
|
resourceKeys: z.array(z.string().min(1)).optional(),
|
|
350
|
+
resourceIds: z.array(z.string().uuid()).max(50).optional(),
|
|
327
351
|
paths: z.array(z.string().min(1)).optional(),
|
|
352
|
+
required: z.boolean().optional(),
|
|
328
353
|
depth: z.number().int().min(1).max(50).optional(),
|
|
329
354
|
}),
|
|
330
355
|
}, async (input) => toolResult(await service.memoryHistory(input)));
|
|
331
356
|
server.registerTool('memory.propose_revision', {
|
|
332
|
-
description:
|
|
357
|
+
description: "Create an inactive, reviewable proposal for permanent product, organization or project memory. Do not block independent task work on drafting, submission or approval. Use authorized host background agents or concurrent tools and continue useful work; collect the result only before an operation needs its proposal or revision ID. Without concurrency, checkpoint the pending draft and defer this call until needed. Keep one writer per resource and preserve baseRevision and task/source identity. A response distinguishes stored (queued:false) from outbox-queued; neither means approved. A stored proposal reports reviewableByCaller: ask this user to approve it only when that is true; otherwise report it as submitted, with its id, to the reviewPath it names (product_release, organization_owner or project_maintainer). Task verification remains required. metadata carries only the structured fields the resource kind defines, such as a project profile's pathRoles, architecture and resourceDiscovery, appliesToStacks, or an architecture template's manifest; classification, containsPii, containsSecrets and resourceDescriptor are computed by the server and ignored when sent, so metadata copied from a served revision is accepted. A project profile's metadata.architecture, when present, is exactly {adopted: [{structure, pressure}], declined: [{structure, why}]} (structure up to 80 characters, pressure/why up to 600, at most 40 entries each) and no other key; a free-text style such as 'MVVM' is not that shape and goes in metadata.architectureStyle (a plain string) or the profile content instead.",
|
|
333
358
|
inputSchema: z.object({
|
|
334
359
|
repoRoot: optionalRepoRoot,
|
|
335
360
|
sourceRunId: z.string().uuid().optional(),
|
|
@@ -374,10 +399,15 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
374
399
|
}),
|
|
375
400
|
}, async (input) => toolResult(await service.memoryProposeRevision(input)));
|
|
376
401
|
server.registerTool('memory.list_proposals', {
|
|
377
|
-
description:
|
|
402
|
+
description: "List the permanent memory proposals still waiting for review, so an authorized reviewer can find them without database access. Each entry carries a reasonPreview, not the full reason; pass proposalId to read one proposal in full, with its reason and proposed content. Narrow a long list with taskId, scope or status (defaults to pending); page through the rest with limit (default 50, max 100) and afterId, set to the response's nextAfterId to continue, which is null once nothing more is waiting. Each proposal reports reviewableByCaller; when it is false, the caller cannot approve it, so never ask this user for approval: report it as waiting on the reviewPath it names.",
|
|
378
403
|
inputSchema: z.object({
|
|
379
404
|
projectId: z.string().min(1),
|
|
380
405
|
proposalId: z.string().min(1).optional(),
|
|
406
|
+
taskId: z.string().min(1).optional(),
|
|
407
|
+
scope: z.enum(['project', 'organization', 'product']).optional(),
|
|
408
|
+
status: z.enum(['pending', 'approved', 'rejected', 'superseded']).optional(),
|
|
409
|
+
afterId: z.string().min(1).optional(),
|
|
410
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
381
411
|
}),
|
|
382
412
|
}, async (input) => toolResult(await service.memoryListProposals(input)));
|
|
383
413
|
server.registerTool('memory.review_proposal', {
|
|
@@ -415,21 +445,55 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
415
445
|
}),
|
|
416
446
|
}, async (input) => toolResult(await service.taskRecordCorrection(input)));
|
|
417
447
|
server.registerTool('task.self_review', {
|
|
418
|
-
description: 'Record that
|
|
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 conversation language (tr or en) for the question. 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.',
|
|
419
449
|
inputSchema: z.object({
|
|
420
450
|
repoRoot: optionalRepoRoot,
|
|
421
451
|
taskId: z.string().min(1),
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
452
|
+
language: z.enum(['tr', 'en']).optional(),
|
|
453
|
+
files: z
|
|
454
|
+
.array(z.object({
|
|
455
|
+
path: z.string().min(1).max(512),
|
|
456
|
+
rules: z
|
|
457
|
+
.array(z.discriminatedUnion('outcome', [
|
|
458
|
+
z.object({
|
|
459
|
+
resourceId: z.string().uuid(),
|
|
460
|
+
outcome: z.literal('follows'),
|
|
461
|
+
}),
|
|
462
|
+
z.object({
|
|
463
|
+
resourceId: z.string().uuid(),
|
|
464
|
+
outcome: z.literal('fixed'),
|
|
465
|
+
issue: z.string().min(8).max(2000),
|
|
466
|
+
change: z.string().min(8).max(2000),
|
|
467
|
+
}),
|
|
468
|
+
z.object({
|
|
469
|
+
resourceId: z.string().uuid(),
|
|
470
|
+
outcome: z.literal('user_accepted_deviation'),
|
|
471
|
+
resourceKey: z.string().min(1).max(240),
|
|
472
|
+
issue: z.string().min(8).max(1000),
|
|
473
|
+
}),
|
|
474
|
+
]))
|
|
475
|
+
.min(1)
|
|
476
|
+
.max(50),
|
|
477
|
+
}))
|
|
478
|
+
.max(500),
|
|
429
479
|
}),
|
|
430
|
-
}, async (input) =>
|
|
480
|
+
}, async (input, context) => {
|
|
481
|
+
let questions;
|
|
482
|
+
try {
|
|
483
|
+
questions = service.ruleDeviationQuestions(input);
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
return toolResult(await service.taskSelfReview(input));
|
|
487
|
+
}
|
|
488
|
+
for (const { definition, previousDefinitions } of questions) {
|
|
489
|
+
const form = await askQuestionnaire(server, service, { ...definition, repoRoot: input.repoRoot }, context, previousDefinitions);
|
|
490
|
+
if (!answerChoice(form))
|
|
491
|
+
return form;
|
|
492
|
+
}
|
|
493
|
+
return toolResult(await service.taskSelfReview(input));
|
|
494
|
+
});
|
|
431
495
|
server.registerTool('task.reconcile', {
|
|
432
|
-
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. A pending result is a durable local receipt; continue independent work without polling. Required delivery is checked before verification.',
|
|
496
|
+
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.',
|
|
433
497
|
inputSchema: z
|
|
434
498
|
.object({
|
|
435
499
|
repoRoot: optionalRepoRoot,
|
|
@@ -464,10 +528,10 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
464
528
|
return;
|
|
465
529
|
}
|
|
466
530
|
for (const entry of entries) {
|
|
467
|
-
if (entry.type === 'approved_revision' &&
|
|
531
|
+
if (entry.type === 'approved_revision' && !entry.proposalId) {
|
|
468
532
|
context.addIssue({
|
|
469
533
|
code: 'custom',
|
|
470
|
-
message: 'Approved reconciliation requires proposalId
|
|
534
|
+
message: 'Approved reconciliation requires proposalId',
|
|
471
535
|
});
|
|
472
536
|
}
|
|
473
537
|
if (entry.type === 'no_semantic_memory_change' &&
|
|
@@ -498,11 +562,15 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
498
562
|
}),
|
|
499
563
|
}, async (input) => toolResult(await service.taskResolvePendingDelivery(input)));
|
|
500
564
|
server.registerTool('task.verify', {
|
|
501
|
-
description: 'Verify a write task with its lease or a read-only task against its pinned Git baseline, mandatory checkpoints, structured validations and synchronized outbox.',
|
|
565
|
+
description: 'Verify a write task with its lease or a read-only task against its pinned Git baseline, mandatory checkpoints, structured validations and synchronized outbox. A refusal lists every unmet requirement at once. If the user decides a required check is not needed for this task, pass it in waivers; the tool asks them natively and records the answer.',
|
|
502
566
|
inputSchema: z.object({
|
|
503
567
|
repoRoot: optionalRepoRoot,
|
|
504
568
|
taskId: z.string().min(1),
|
|
505
569
|
sessionId: z.string().min(1),
|
|
570
|
+
language: z
|
|
571
|
+
.enum(['tr', 'en'])
|
|
572
|
+
.optional()
|
|
573
|
+
.describe('Language of the current conversation, for the waiver question.'),
|
|
506
574
|
leaseId: z.string().min(1).optional(),
|
|
507
575
|
changedPaths: stringList.min(1).optional(),
|
|
508
576
|
validations: z
|
|
@@ -525,8 +593,61 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
525
593
|
.max(20)
|
|
526
594
|
.optional()
|
|
527
595
|
.describe('Architectural structures this task adds that the project did not have: a cache, a broker, a read replica, a second deployable, a projection, an event store. Verification refuses any the project profile has not recorded under architecture.adopted with the pressure it relieves.'),
|
|
528
|
-
|
|
529
|
-
|
|
596
|
+
waivers: z
|
|
597
|
+
.array(z.object({
|
|
598
|
+
validationId: z.enum(['ui']),
|
|
599
|
+
reason: z.string().min(10).max(500),
|
|
600
|
+
}))
|
|
601
|
+
.max(3)
|
|
602
|
+
.optional()
|
|
603
|
+
.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
|
+
}),
|
|
605
|
+
}, async (input, context) => {
|
|
606
|
+
const { language = 'en', ...request } = input;
|
|
607
|
+
for (const waiver of request.waivers ?? []) {
|
|
608
|
+
const subject = await service.validationWaiverSubject({
|
|
609
|
+
repoRoot: request.repoRoot,
|
|
610
|
+
taskId: request.taskId,
|
|
611
|
+
sessionId: request.sessionId,
|
|
612
|
+
...waiver,
|
|
613
|
+
});
|
|
614
|
+
if (!subject.ok)
|
|
615
|
+
return toolResult(subject);
|
|
616
|
+
const { questionnaireId, externalTaskId, paths, reason } = subject.data;
|
|
617
|
+
const listed = paths.slice(0, 20).join(', ') + (paths.length > 20 ? ` (+${paths.length - 20})` : '');
|
|
618
|
+
const definitions = [
|
|
619
|
+
{
|
|
620
|
+
questionnaireId,
|
|
621
|
+
language: 'tr',
|
|
622
|
+
message: `${externalTaskId}: Bu görev için arayüz (UI) kontrolü atlansın mı? Değişen şu dosyalar ekranda görünen arayüzü oluşturuyor: ${listed}. Belirtilen gerekçe: ${reason}. Görev, bu dosyaların widget, golden veya ekran görüntüsü testi olmadan doğrulanır. Karar görevin denetim kaydına yazılır.`,
|
|
623
|
+
options: [
|
|
624
|
+
{ id: 'waive', label: 'Bu dosyalar için UI kontrolünü atla' },
|
|
625
|
+
{ id: 'require', label: 'UI kontrolü zorunlu kalsın' },
|
|
626
|
+
],
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
questionnaireId,
|
|
630
|
+
language: 'en',
|
|
631
|
+
message: `${externalTaskId}: skip the UI check for this task? These changed files render UI: ${listed}. Reason given: ${reason}. The task will verify without a widget, golden or screenshot check of them. The decision is stored in the task's audit trail.`,
|
|
632
|
+
options: [
|
|
633
|
+
{ id: 'waive', label: 'Skip the UI check for these files' },
|
|
634
|
+
{ id: 'require', label: 'Keep the UI check required' },
|
|
635
|
+
],
|
|
636
|
+
},
|
|
637
|
+
];
|
|
638
|
+
const form = await askQuestionnaire(server, service, {
|
|
639
|
+
...definitions.find((definition) => definition.language === language),
|
|
640
|
+
repoRoot: request.repoRoot,
|
|
641
|
+
}, context, definitions);
|
|
642
|
+
const record = await service.questionnaireResume({
|
|
643
|
+
repoRoot: request.repoRoot,
|
|
644
|
+
questionnaireId,
|
|
645
|
+
});
|
|
646
|
+
if (record.status !== 'answered' || record.answer?.choice !== 'waive')
|
|
647
|
+
return form;
|
|
648
|
+
}
|
|
649
|
+
return toolResult(await service.taskVerify(request));
|
|
650
|
+
});
|
|
530
651
|
server.registerTool('task.close', {
|
|
531
652
|
description: 'Close a task only when backend verification still matches the current Git diff hash.',
|
|
532
653
|
inputSchema: z.object({
|
|
@@ -652,7 +773,7 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
652
773
|
inputSchema: z.object({ includeArchived: z.boolean().optional() }),
|
|
653
774
|
}, async (input) => toolResult(await service.projectList(input)));
|
|
654
775
|
server.registerTool('project.resolve', {
|
|
655
|
-
description: 'Resolve the
|
|
776
|
+
description: 'Resolve the project this checkout belongs to, or bind an explicitly selected project, then store the user-local binding only after backend authorization succeeds. A result with projectMoved means this checkout points at a repository the project left; a bind refused with recovery project.move_repository means the project lives in another repository and an owner can move it here.',
|
|
656
777
|
inputSchema: z.object({
|
|
657
778
|
repoRoot: optionalRepoRoot,
|
|
658
779
|
bind: z.boolean().optional(),
|