engineering-memory 1.11.14 → 1.11.16
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/questionnaire-tools.js +105 -50
- package/runtime/dist/src/mcp/worktree-tools.js +4 -3
- package/runtime/dist/src/runtime/bridge-service.js +12 -6
- package/runtime/dist/src/runtime/task-start.js +124 -0
- package/runtime/dist/src/runtime/worktree-editor.js +184 -0
- package/runtime/dist/src/runtime/worktree-gradle.js +318 -0
- package/runtime/dist/src/runtime/worktree-pool.js +181 -3
- package/runtime/dist/src/runtime/worktree-preparation.js +901 -0
- package/runtime/dist/src/runtime/worktree-readiness-types.js +18 -0
- package/skill/references/lifecycle.md +3 -1
- package/skill/references/questionnaires.md +9 -5
package/dispatcher/sections.mjs
CHANGED
|
@@ -20,7 +20,7 @@ Never answer a question about a bound repository from the working tree alone. Wh
|
|
|
20
20
|
|
|
21
21
|
After compaction, a new chat, interruption, or handoff, call \`session.resume\` before continuing.
|
|
22
22
|
|
|
23
|
-
Before a new write/scaffold task, call \`task.branch\` directly: do not ask the user where, from which branch or under which name first, and do not call \`worktree.list\` for it. Its
|
|
23
|
+
Before a new write/scaffold task, call \`task.branch\` directly: do not ask the user where, from which branch or under which name first, and do not call \`worktree.list\` for it. Its short native form asks only the starting branch and where to work. It generates the new branch name automatically; do not ask the user to name it unless they request a specific name. Nothing is created until the user answers. The pool reuses a safely available worktree slot before creating another; never pass or invent a worktree path. Reconcile protected legacy entries through the native recovery flow; clean files alone do not establish that the old agent and delivery have finished. Run all commands from its returned \`repoRoot\`. Renew \`task.heartbeat\` during actual work, use \`task.pause\` on handoff, and resume before writing again. Inactivity never permits force checkout or cleanup. Keep closed worktrees until authorized delivery completes. When \`session.entry\` says the user can manage them, unanswered shared branch preferences are asked together in one native form via \`project.set_git_preferences\`; other members choose only their own task base in \`task.branch\`. Answering "Not now" returns \`deferred\` with nothing allocated and a \`reconsider\` call; say so in one line, keep working on anything that does not depend on it, and make that \`reconsider\` call only when the user actually asks to start that task.
|
|
24
24
|
|
|
25
25
|
Do not edit until the skill lifecycle has completed discovery, its checkpoint, and \`context.prepare_change\`. Do not claim completion until \`task.verify\` succeeds.
|
|
26
26
|
|
|
@@ -28,7 +28,7 @@ Do not block independent task work on \`memory.propose_revision\` drafting, subm
|
|
|
28
28
|
|
|
29
29
|
A check whose answer decides the next action — delivery preconditions such as the commit hook, the remote and the branch; whether verification or close can pass; what a waiver allows — is run by the agent that takes that action, in its own turn. Hand work to another agent only when you have other real work to do while it runs, and never take the action a delegated check gates before its answer is back. Work another agent checked is neither the user's approval nor task verification. Size delegation to the task — a one- or two-file change needs no multi-agent workflow — and once work is delegated, wait for its real completion signal, never a sleep loop polling its output.
|
|
30
30
|
|
|
31
|
-
Use the host's native questionnaire for every question to the user, including implementation choices, names, clarification, branch/worktree decisions and delivery. For a required decision, use \`questionnaire.ask\` to open a durable native MCP form and \`questionnaire.resume\` to return to the same unanswered question. Read the skill's questionnaires reference first. Write every question in the user's language, say in \`context\` why it is asked, give in \`example\` one concrete thing the decision changes, and give every option a \`description\` of what happens next. Show a record by its title, never by an id or a hash. When the user says they did not understand, that is not an answer: explain in chat and show the same question again. Never use request_user_input_async for a required decision: it does not wait for an answer. When the durable form is unavailable, use a blocking native control only where the host permits it: request_user_input in Codex or AskUserQuestion in Claude. Never replace the questionnaire with a chat instruction such as 'type this', 'reply yes', or 'write X if you want Y'. Do not open a survey web page. If the required native control is unavailable or prohibited for that kind of question, follow the host's tool restrictions, explain the limitation, and continue only work already authorized; do not fabricate a survey or silently choose an answer. A timeout, dismissed form, empty response or ended turn is not an answer. Keep the decision pending and resume it; do not start dependent work or report it as resolved. Existing answers remain valid through retries and handoffs.
|
|
31
|
+
Choose question detail to suit the decision: task start is short, while complex decisions retain context and examples. Put a deliberate recommended choice first and preserve the host's free-input path without requiring an option or adding a duplicate Other choice; feedback is not consent. Use the host's native questionnaire for every question to the user, including implementation choices, names, clarification, branch/worktree decisions and delivery. For a required decision, use \`questionnaire.ask\` to open a durable native MCP form and \`questionnaire.resume\` to return to the same unanswered question. Read the skill's questionnaires reference first. Write every question in the user's language, say in \`context\` why it is asked, give in \`example\` one concrete thing the decision changes, and give every option a \`description\` of what happens next. Show a record by its title, never by an id or a hash. When the user says they did not understand, that is not an answer: explain in chat and show the same question again. Never use request_user_input_async for a required decision: it does not wait for an answer. When the durable form is unavailable, use a blocking native control only where the host permits it: request_user_input in Codex or AskUserQuestion in Claude. Never replace the questionnaire with a chat instruction such as 'type this', 'reply yes', or 'write X if you want Y'. Do not open a survey web page. If the required native control is unavailable or prohibited for that kind of question, follow the host's tool restrictions, explain the limitation, and continue only work already authorized; do not fabricate a survey or silently choose an answer. A timeout, dismissed form, empty response or ended turn is not an answer. Keep the decision pending and resume it; do not start dependent work or report it as resolved. Existing answers remain valid through retries and handoffs.
|
|
32
32
|
|
|
33
33
|
When a durable MCP form cannot be displayed, read its \`hostFallback\` or call \`questionnaire.resume\` with \`presentation: host_native\` to get the original question without reopening the MCP form. Display the same question, all choices and notices through a blocking native control only if the host permits that control for this decision. After an actual native answer, call \`questionnaire.answer_from_host\` with the unchanged questionnaireId, requestKey and contentHash, the hostTool name and the returned choice/text. This is an agent-reported relay, not MCP transport attestation. Then retry the owning operation; its authority, content and version checks still apply. Never relay prose consent, a default, an asynchronous response or a cancelled/declined/missing answer. Decline alone is not proof that the host cannot display forms. Keep the decision pending if no permitted native control can represent it.
|
|
34
34
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engineering-memory",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.16",
|
|
4
4
|
"description": "Installs the Engineering Memory skill and its local MCP bridge. Sign in after installing; your organization and project are resolved from your account.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
package/runtime/build.json
CHANGED
|
@@ -42,12 +42,21 @@ export async function resumeQuestionnaire(server, service, input, context) {
|
|
|
42
42
|
}
|
|
43
43
|
export async function answerQuestionnaireFromHost(service, input) {
|
|
44
44
|
try {
|
|
45
|
-
if (input.answer?.choice === explain
|
|
45
|
+
if (input.answer?.choice === explain ||
|
|
46
|
+
input.answer?.choice === '__other__' ||
|
|
47
|
+
Object.values(input.answers ?? {}).some((answer) => answer.choice === '__other__')) {
|
|
46
48
|
const record = await service.questionnaireResume(input);
|
|
47
49
|
if (record.status === 'pending' &&
|
|
48
50
|
record.requestKey === input.requestKey &&
|
|
49
|
-
record.contentHash === input.contentHash)
|
|
50
|
-
|
|
51
|
+
record.contentHash === input.contentHash) {
|
|
52
|
+
if (input.answer?.choice === explain)
|
|
53
|
+
return pending(record, 'needs_explanation');
|
|
54
|
+
if (record.questions || !record.allowFreeText) {
|
|
55
|
+
const feedback = input.answer ??
|
|
56
|
+
Object.values(input.answers ?? {}).find((answer) => answer.choice === '__other__');
|
|
57
|
+
return feedbackResult(record, feedback?.text);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
51
60
|
}
|
|
52
61
|
const resolved = await service.questionnaireAnswerFromHost(input);
|
|
53
62
|
return answered(resolved.record, resolved.answer, resolved.replayed, resolved.retry);
|
|
@@ -77,11 +86,33 @@ async function present(server, service, record, repoRoot, context, preparation,
|
|
|
77
86
|
const response = inputResponse(responses, record.requestKey);
|
|
78
87
|
if (response.kind === 'elicit' && response.action === 'accept') {
|
|
79
88
|
const content = acceptedContent(responses, record.requestKey);
|
|
80
|
-
if (content?.choice === explain ||
|
|
89
|
+
if (content?.choice === explain ||
|
|
90
|
+
content?.[explain] === true ||
|
|
91
|
+
record.questions?.some((question) => content?.[question.id] === explain)) {
|
|
81
92
|
consumedResponses(context).add(record.requestKey);
|
|
82
93
|
return pending(record, 'needs_explanation');
|
|
83
94
|
}
|
|
84
|
-
|
|
95
|
+
if (content?.__feedback__ !== undefined &&
|
|
96
|
+
!(typeof content.__feedback__ === 'string' && !content.__feedback__.trim())) {
|
|
97
|
+
consumedResponses(context).add(record.requestKey);
|
|
98
|
+
return feedbackResult(record, content.__feedback__);
|
|
99
|
+
}
|
|
100
|
+
if (!record.questions &&
|
|
101
|
+
!record.allowFreeText &&
|
|
102
|
+
!content?.choice &&
|
|
103
|
+
typeof content?.text === 'string' &&
|
|
104
|
+
content.text.trim()) {
|
|
105
|
+
consumedResponses(context).add(record.requestKey);
|
|
106
|
+
return feedbackResult(record, content.text);
|
|
107
|
+
}
|
|
108
|
+
const { __feedback__: _feedback, ...answerContent } = content ?? {};
|
|
109
|
+
if (!record.questions &&
|
|
110
|
+
record.allowFreeText &&
|
|
111
|
+
!answerContent.choice &&
|
|
112
|
+
typeof answerContent.text === 'string' &&
|
|
113
|
+
answerContent.text.trim())
|
|
114
|
+
answerContent.choice = '__other__';
|
|
115
|
+
const parsed = questionnaireAnswerSchema(record).safeParse(record.questions ? formAnswers(record, answerContent) : answerContent);
|
|
85
116
|
const answer = parsed.success ? parsed.data : undefined;
|
|
86
117
|
if (!answer)
|
|
87
118
|
return pending(record, 'invalid_response');
|
|
@@ -109,15 +140,22 @@ async function present(server, service, record, repoRoot, context, preparation,
|
|
|
109
140
|
return pending(record, 'native_form_unavailable');
|
|
110
141
|
const copy = copyFor(record);
|
|
111
142
|
if (record.questions) {
|
|
143
|
+
const compact = record.questionnaireId.startsWith('task-start-') && !!record.binding;
|
|
112
144
|
const properties = {};
|
|
113
145
|
for (const sub of record.questions) {
|
|
146
|
+
const options = compact && sub.otherAsText
|
|
147
|
+
? sub.options.filter((option) => option.id !== 'other')
|
|
148
|
+
: sub.options;
|
|
114
149
|
properties[sub.id] = {
|
|
115
150
|
type: 'string',
|
|
116
151
|
title: sub.message,
|
|
117
|
-
|
|
118
|
-
|
|
152
|
+
description: [sub.context, sub.example && `${copy.example}: ${sub.example}`]
|
|
153
|
+
.filter(Boolean)
|
|
154
|
+
.join('\n'),
|
|
155
|
+
enum: options.map((option) => option.id),
|
|
156
|
+
enumNames: optionLabels(options, copy),
|
|
119
157
|
};
|
|
120
|
-
if (sub.textField)
|
|
158
|
+
if (sub.textField && !(compact && sub.otherAsText))
|
|
121
159
|
properties[`${sub.id}_text`] = {
|
|
122
160
|
type: 'string',
|
|
123
161
|
title: sub.textField.title,
|
|
@@ -125,12 +163,12 @@ async function present(server, service, record, repoRoot, context, preparation,
|
|
|
125
163
|
maxLength: sub.textField.maxLength,
|
|
126
164
|
};
|
|
127
165
|
}
|
|
128
|
-
properties
|
|
166
|
+
properties.__feedback__ = { type: 'string', title: copy.feedback, maxLength: 2000 };
|
|
129
167
|
return inputRequired({
|
|
130
168
|
inputRequests: {
|
|
131
169
|
[record.requestKey]: inputRequired.elicit({
|
|
132
170
|
mode: 'form',
|
|
133
|
-
message: presentationMessage(record),
|
|
171
|
+
message: presentationMessage(record, compact),
|
|
134
172
|
requestedSchema: { type: 'object', properties },
|
|
135
173
|
}),
|
|
136
174
|
},
|
|
@@ -147,23 +185,14 @@ async function present(server, service, record, repoRoot, context, preparation,
|
|
|
147
185
|
choice: {
|
|
148
186
|
type: 'string',
|
|
149
187
|
title: copy.answer,
|
|
150
|
-
enum:
|
|
151
|
-
|
|
152
|
-
...(record.allowFreeText ? ['__other__'] : []),
|
|
153
|
-
explain,
|
|
154
|
-
],
|
|
155
|
-
enumNames: [
|
|
156
|
-
...record.options.map((option) => option.label),
|
|
157
|
-
...(record.allowFreeText ? [copy.other] : []),
|
|
158
|
-
copy.explain,
|
|
159
|
-
],
|
|
188
|
+
enum: record.options.map((option) => option.id),
|
|
189
|
+
enumNames: optionLabels(record.options, copy),
|
|
160
190
|
},
|
|
161
191
|
...(record.allowFreeText
|
|
162
192
|
? {
|
|
163
193
|
text: {
|
|
164
194
|
type: 'string',
|
|
165
|
-
title: copy.
|
|
166
|
-
minLength: 1,
|
|
195
|
+
title: copy.freeAnswer,
|
|
167
196
|
maxLength: 2000,
|
|
168
197
|
},
|
|
169
198
|
}
|
|
@@ -178,8 +207,10 @@ async function present(server, service, record, repoRoot, context, preparation,
|
|
|
178
207
|
},
|
|
179
208
|
}
|
|
180
209
|
: {}),
|
|
210
|
+
...(!record.allowFreeText
|
|
211
|
+
? { __feedback__: { type: 'string', title: copy.feedback, maxLength: 2000 } }
|
|
212
|
+
: {}),
|
|
181
213
|
},
|
|
182
|
-
required: ['choice'],
|
|
183
214
|
},
|
|
184
215
|
}),
|
|
185
216
|
},
|
|
@@ -195,25 +226,25 @@ function consumedResponses(context) {
|
|
|
195
226
|
const questionnaireCopy = {
|
|
196
227
|
en: {
|
|
197
228
|
pending: 'Choose an answer to resolve this decision. Closing or cancelling the form leaves it pending.',
|
|
198
|
-
freeText: '
|
|
229
|
+
freeText: ' You can type a different answer without choosing an option. Free text is not stored and cannot be replayed.',
|
|
199
230
|
storedText: ' The bounded text field is stored as part of this exact decision and is replayed after restart.',
|
|
200
|
-
everyQuestion: '
|
|
231
|
+
everyQuestion: ' To record a decision, answer each question or choose an option that ends the form. You can send a comment without selecting any option.',
|
|
201
232
|
example: 'Example',
|
|
202
|
-
explain: 'I did not understand; explain in more detail first',
|
|
203
233
|
answer: 'Answer',
|
|
204
|
-
|
|
205
|
-
|
|
234
|
+
freeAnswer: 'Write another answer (optional)',
|
|
235
|
+
recommended: 'Recommended',
|
|
236
|
+
feedback: 'Write an answer or comment (optional)',
|
|
206
237
|
},
|
|
207
238
|
tr: {
|
|
208
239
|
pending: 'Kararını kaydetmek için bir seçenek seç. Formu kapatmak veya iptal etmek onay sayılmaz; soru beklemede kalır.',
|
|
209
|
-
freeText: '
|
|
240
|
+
freeText: ' Bir seçenek seçmeden farklı yanıtını yazabilirsin. Serbest yanıt saklanmaz ve daha sonra geri getirilemez.',
|
|
210
241
|
storedText: ' Bu metin alanındaki yanıt kararınla birlikte saklanır; yeniden başlattığında korunur.',
|
|
211
|
-
everyQuestion: '
|
|
242
|
+
everyQuestion: ' Karar vermek için soruları cevapla veya formu bitiren bir seçenek seç. Hiçbir seçenek seçmeden yorum da gönderebilirsin.',
|
|
212
243
|
example: 'Örnek',
|
|
213
|
-
explain: 'Anlamadım; önce daha ayrıntılı anlat',
|
|
214
244
|
answer: 'Yanıt',
|
|
215
|
-
|
|
216
|
-
|
|
245
|
+
freeAnswer: 'Farklı yanıtını yaz (isteğe bağlı)',
|
|
246
|
+
recommended: 'Önerilen',
|
|
247
|
+
feedback: 'Yanıtını veya yorumunu yaz (isteğe bağlı)',
|
|
217
248
|
},
|
|
218
249
|
};
|
|
219
250
|
function formAnswers(record, content) {
|
|
@@ -231,23 +262,26 @@ function formAnswers(record, content) {
|
|
|
231
262
|
}
|
|
232
263
|
return answers;
|
|
233
264
|
}
|
|
234
|
-
function presentationMessage(record) {
|
|
265
|
+
function presentationMessage(record, compact = false) {
|
|
235
266
|
const copy = copyFor(record);
|
|
267
|
+
if (compact)
|
|
268
|
+
return [
|
|
269
|
+
record.message,
|
|
270
|
+
record.context,
|
|
271
|
+
record.example,
|
|
272
|
+
record.language?.startsWith('tr')
|
|
273
|
+
? 'İptal edersen başlamaz.'
|
|
274
|
+
: 'Cancelling does not start the task.',
|
|
275
|
+
]
|
|
276
|
+
.filter(Boolean)
|
|
277
|
+
.join('\n');
|
|
236
278
|
const explained = (item) => [
|
|
237
279
|
item.context,
|
|
238
280
|
item.example && `${copy.example}: ${item.example}`,
|
|
239
|
-
(item.options ?? [])
|
|
240
|
-
.filter((option) => option.description)
|
|
241
|
-
.map((option) => `${option.label}: ${option.description}`)
|
|
242
|
-
.join('\n'),
|
|
243
281
|
];
|
|
244
282
|
return [
|
|
245
283
|
record.message,
|
|
246
284
|
...explained(record),
|
|
247
|
-
...(record.questions ?? []).flatMap((sub) => {
|
|
248
|
-
const details = explained(sub).filter(Boolean);
|
|
249
|
-
return details.length ? [sub.message, ...details] : [];
|
|
250
|
-
}),
|
|
251
285
|
`${copy.pending}${record.questions ? copy.everyQuestion : ''}${record.allowFreeText ? copy.freeText : ''}${record.textField ? copy.storedText : ''}`,
|
|
252
286
|
]
|
|
253
287
|
.filter(Boolean)
|
|
@@ -256,19 +290,38 @@ function presentationMessage(record) {
|
|
|
256
290
|
function copyFor(record) {
|
|
257
291
|
return questionnaireCopy[record.language?.startsWith('tr') ? 'tr' : 'en'];
|
|
258
292
|
}
|
|
259
|
-
function
|
|
293
|
+
function optionLabels(options, copy) {
|
|
294
|
+
const recommended = options.find((option) => option.label.includes(`(${copy.recommended})`))?.id ?? options[0].id;
|
|
295
|
+
return options.map((option) => {
|
|
296
|
+
const label = option.id === recommended && !option.label.includes(`(${copy.recommended})`)
|
|
297
|
+
? `${option.label} (${copy.recommended})`
|
|
298
|
+
: option.label;
|
|
299
|
+
return option.description ? `${label} — ${option.description}` : label;
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
function feedbackResult(record, text) {
|
|
303
|
+
if (typeof text !== 'string' || !text.trim() || text.length > 2000)
|
|
304
|
+
return pending(record, 'invalid_response');
|
|
305
|
+
return pending(record, 'feedback', { text: text.trim() });
|
|
306
|
+
}
|
|
307
|
+
function pending(record, reason, feedback) {
|
|
260
308
|
return result({
|
|
261
309
|
questionnaireId: record.questionnaireId,
|
|
262
310
|
status: 'pending',
|
|
263
311
|
reason,
|
|
312
|
+
...(feedback ? { feedback } : {}),
|
|
264
313
|
answerAvailable: false,
|
|
265
314
|
hostFallback: {
|
|
266
315
|
operation: 'questionnaire.answer_from_host',
|
|
267
316
|
questionnaireId: record.questionnaireId,
|
|
268
317
|
requestKey: record.requestKey,
|
|
269
318
|
contentHash: record.contentHash,
|
|
270
|
-
message: presentationMessage(record),
|
|
319
|
+
message: presentationMessage(record, record.questionnaireId.startsWith('task-start-') && !!record.binding),
|
|
271
320
|
language: record.language ?? 'en',
|
|
321
|
+
freeTextFeedback: {
|
|
322
|
+
relayChoice: '__other__',
|
|
323
|
+
recordsDecision: !record.questions && record.allowFreeText,
|
|
324
|
+
},
|
|
272
325
|
...(record.questions
|
|
273
326
|
? {
|
|
274
327
|
questions: record.questions,
|
|
@@ -281,15 +334,17 @@ function pending(record, reason) {
|
|
|
281
334
|
}),
|
|
282
335
|
instructions: `${record.questions
|
|
283
336
|
? '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.'
|
|
284
|
-
: '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
|
|
337
|
+
: '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.'} Keep the question proportional to the decision: short for task start, detailed when needed; do not omit context, examples or consequences. Mark the explicitly recommended option, otherwise the first option, as recommended without treating it as an answer. Use the host control's existing free-text input so the person can write without picking an option. Do not add a duplicate Other choice or a second feedback question. Relay that actual native text through the internal __other__ choice with text and preserve all bindings. This internal relay identifier is not a visible option. 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
|
|
285
338
|
? ' 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.'
|
|
286
339
|
: ''}`,
|
|
287
340
|
},
|
|
288
|
-
nextAction: reason === '
|
|
289
|
-
? 'The person
|
|
290
|
-
: reason === '
|
|
291
|
-
? '
|
|
292
|
-
:
|
|
341
|
+
nextAction: reason === 'feedback'
|
|
342
|
+
? 'The person provided feedback, not consent. Address it in their language. The original decision is still pending and no choice has been saved. Resume it after explaining, or compose a new bound question if the requested choices change. Do not persist arbitrary feedback text.'
|
|
343
|
+
: reason === 'needs_explanation'
|
|
344
|
+
? '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.'
|
|
345
|
+
: reason === 'native_form_unavailable'
|
|
346
|
+
? '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.'
|
|
347
|
+
: '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.',
|
|
293
348
|
});
|
|
294
349
|
}
|
|
295
350
|
function answered(record, answer, replayed, retry) {
|
|
@@ -358,7 +358,7 @@ export function registerWorktreeTools(server, service) {
|
|
|
358
358
|
inputSchema: z.strictObject(policy),
|
|
359
359
|
}, async (input) => output(await service.worktreePolicy(json(input))));
|
|
360
360
|
server.registerTool('task.branch', {
|
|
361
|
-
description: 'Start a write task with ONE durable native form:
|
|
361
|
+
description: 'Start a write task with ONE durable native form: two short questions for the starting branch (unless base is given) and the folder. A unique branch name is generated automatically unless name is supplied. Folder options: a separate managed worktree, this folder as a new branch when it is clean (moving out an unfinished task that holds it, named in the option), this folder on its current branch, or not now. Nothing is created until the user answers; the next call with the same arguments allocates. A remote base is fetched and its exact commit pinned. Retry/resume preserves the allocation. Not now returns status deferred with the exact reconsider call; retries keep the same decisionAttempt. Use the returned repoRoot for ALL commands. A read-only task allocates only when transitioning to write.',
|
|
362
362
|
inputSchema: z.strictObject({
|
|
363
363
|
repoRoot: repo,
|
|
364
364
|
externalTaskId: z.string().min(2).max(160),
|
|
@@ -401,14 +401,15 @@ export function registerWorktreeTools(server, service) {
|
|
|
401
401
|
keepCurrent: input.keepCurrent,
|
|
402
402
|
}),
|
|
403
403
|
};
|
|
404
|
-
const form = await askQuestionnaire(server, service, { ...definition, repoRoot: input.repoRoot, presentation: input.presentation }, context,
|
|
404
|
+
const form = await askQuestionnaire(server, service, { ...definition, repoRoot: input.repoRoot, presentation: input.presentation }, context, previous, owner);
|
|
405
405
|
const record = await service
|
|
406
406
|
.questionnaireResume({
|
|
407
407
|
repoRoot: input.repoRoot,
|
|
408
408
|
questionnaireId: definition.questionnaireId,
|
|
409
409
|
})
|
|
410
410
|
.catch(() => undefined);
|
|
411
|
-
const choice = record?.binding &&
|
|
411
|
+
const choice = record?.binding &&
|
|
412
|
+
[definition, ...previous].some((offered) => stableStringify(record.binding) === stableStringify(offered.binding))
|
|
412
413
|
? taskStartChoice(record)
|
|
413
414
|
: null;
|
|
414
415
|
if (!choice)
|
|
@@ -544,7 +544,7 @@ export class BridgeService {
|
|
|
544
544
|
if ((!sourceCommit && !(previous && previous.baseCommit === null && !previous.managed)) ||
|
|
545
545
|
branch === undefined)
|
|
546
546
|
throw refuse('The task branch and base must be restored before resuming its worktree.', 'worktree.reconcile');
|
|
547
|
-
const allocation = await pool.allocate({
|
|
547
|
+
const allocation = await pool.prepare(await pool.allocate({
|
|
548
548
|
projectId,
|
|
549
549
|
projectName: projectId,
|
|
550
550
|
repoFingerprint: repository.repoFingerprint,
|
|
@@ -554,7 +554,7 @@ export class BridgeService {
|
|
|
554
554
|
baseCommit: sourceCommit ?? null,
|
|
555
555
|
keepCurrent: previous ? !previous.managed : false,
|
|
556
556
|
resume: true,
|
|
557
|
-
}, await this.poolPolicy());
|
|
557
|
+
}, await this.poolPolicy()));
|
|
558
558
|
await pool.bind(projectId, allocation.repoRoot, allocation.generation, pointer.taskId);
|
|
559
559
|
allocation.taskId = pointer.taskId;
|
|
560
560
|
this.poolAllocations.set(allocation.repoRoot, allocation);
|
|
@@ -2276,7 +2276,13 @@ export class BridgeService {
|
|
|
2276
2276
|
for (const candidate of candidates)
|
|
2277
2277
|
if (!(await this.unusableBranchName(repoRoot, candidate)))
|
|
2278
2278
|
return candidate;
|
|
2279
|
-
|
|
2279
|
+
const git = this.dependencies.repositories.git;
|
|
2280
|
+
let suffix = candidates.length + 1;
|
|
2281
|
+
while (await git.branchExists(repoRoot, candidates[0] + '-' + suffix))
|
|
2282
|
+
suffix++;
|
|
2283
|
+
const name = candidates[0] + '-' + suffix;
|
|
2284
|
+
await git.validateBranch(repoRoot, name);
|
|
2285
|
+
return name;
|
|
2280
2286
|
}
|
|
2281
2287
|
async unusableBranchName(repoRoot, name) {
|
|
2282
2288
|
const git = this.dependencies.repositories.git;
|
|
@@ -2609,7 +2615,7 @@ export class BridgeService {
|
|
|
2609
2615
|
const allocationTimer = startPhaseTimer('task.branch.allocate');
|
|
2610
2616
|
const allocationPolicy = await this.poolPolicy(true);
|
|
2611
2617
|
allocationTimer.mark('policy');
|
|
2612
|
-
const allocation = await pool.allocate({
|
|
2618
|
+
const allocation = await pool.prepare(await pool.allocate({
|
|
2613
2619
|
projectId,
|
|
2614
2620
|
projectName: preferences.projectName ?? projectId,
|
|
2615
2621
|
repoFingerprint: repository.repoFingerprint,
|
|
@@ -2620,7 +2626,7 @@ export class BridgeService {
|
|
|
2620
2626
|
keepCurrent: input.keepCurrent,
|
|
2621
2627
|
inPlace: !existing && input.inPlace,
|
|
2622
2628
|
resume: Boolean(existing),
|
|
2623
|
-
}, allocationPolicy);
|
|
2629
|
+
}, allocationPolicy));
|
|
2624
2630
|
allocationTimer.mark('pool_allocate');
|
|
2625
2631
|
allocationTimer.finish();
|
|
2626
2632
|
this.poolAllocations.set(allocation.repoRoot, allocation);
|
|
@@ -2642,7 +2648,7 @@ export class BridgeService {
|
|
|
2642
2648
|
return asJsonValue({
|
|
2643
2649
|
...allocation,
|
|
2644
2650
|
kept: !allocation.managed,
|
|
2645
|
-
nextAction: 'Run every file, terminal, lifecycle and delivery operation from this repoRoot.
|
|
2651
|
+
nextAction: 'Run every file, terminal, lifecycle and delivery operation from this repoRoot. Inspect readiness separately: a file problem does not cancel the task, and an editor request does not prove its window is visible. Resolve reported local file issues without overwriting existing files, then retry task.branch or session.resume. Use task.heartbeat during long work and pause before handoff.',
|
|
2646
2652
|
});
|
|
2647
2653
|
}
|
|
2648
2654
|
adoptOwned(repoRoot, entry) {
|
|
@@ -73,6 +73,130 @@ export function configuredBase(preferences, role) {
|
|
|
73
73
|
: null;
|
|
74
74
|
}
|
|
75
75
|
export function taskStartDefinition(facts) {
|
|
76
|
+
const { previous, ...legacy } = legacyTaskStartDefinition(facts);
|
|
77
|
+
const tr = facts.language === 'tr';
|
|
78
|
+
const binding = { ...legacy.binding };
|
|
79
|
+
const keepOnly = facts.keepCurrent === true || !facts.currentCommit;
|
|
80
|
+
if (!keepOnly) {
|
|
81
|
+
binding.name = facts.name ?? facts.suggestedName;
|
|
82
|
+
delete binding.suggestedName;
|
|
83
|
+
}
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
const questions = legacy
|
|
86
|
+
.questions.filter((question) => question.id !== 'name')
|
|
87
|
+
.map((question) => {
|
|
88
|
+
if (question.id === 'base')
|
|
89
|
+
return {
|
|
90
|
+
...question,
|
|
91
|
+
message: tr ? 'Hangi daldan başlayalım?' : 'Which starting branch?',
|
|
92
|
+
options: question.options
|
|
93
|
+
.filter((option) => {
|
|
94
|
+
const base = binding.bases?.[option.id];
|
|
95
|
+
const identity = base ? stableStringify(base) : option.id;
|
|
96
|
+
if (seen.has(identity))
|
|
97
|
+
return false;
|
|
98
|
+
seen.add(identity);
|
|
99
|
+
return true;
|
|
100
|
+
})
|
|
101
|
+
.map((option) => {
|
|
102
|
+
const base = binding.bases?.[option.id];
|
|
103
|
+
return {
|
|
104
|
+
...option,
|
|
105
|
+
label: base?.kind === 'remote'
|
|
106
|
+
? sourceLabel(base, facts.currentBranch)
|
|
107
|
+
: option.id === 'current'
|
|
108
|
+
? (tr ? 'Yerel: ' : 'Local: ') + (facts.currentBranch ?? 'HEAD')
|
|
109
|
+
: tr
|
|
110
|
+
? 'origin üzerinde başka dal'
|
|
111
|
+
: 'Another branch on origin',
|
|
112
|
+
description: option.id === 'current'
|
|
113
|
+
? tr
|
|
114
|
+
? 'Çekmeden, mevcut commit’ten.'
|
|
115
|
+
: 'Current commit; no fetch.'
|
|
116
|
+
: option.id === 'other'
|
|
117
|
+
? tr
|
|
118
|
+
? 'Alttaki alana yaz.'
|
|
119
|
+
: 'Enter it below.'
|
|
120
|
+
: tr
|
|
121
|
+
? 'Güncel hali çekilir.'
|
|
122
|
+
: 'Fetch the latest commit.',
|
|
123
|
+
};
|
|
124
|
+
}),
|
|
125
|
+
textField: {
|
|
126
|
+
...question.textField,
|
|
127
|
+
title: tr
|
|
128
|
+
? 'origin dalı (yalnızca başka dal için)'
|
|
129
|
+
: 'Branch on origin (only for another branch)',
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
const holder = binding.holder?.externalTaskId;
|
|
133
|
+
return {
|
|
134
|
+
...question,
|
|
135
|
+
message: tr ? 'Nerede çalışalım?' : 'Where should we work?',
|
|
136
|
+
...(holder
|
|
137
|
+
? {
|
|
138
|
+
context: tr
|
|
139
|
+
? `Bu klasördeki ${holder} görevi burada başlarsan ayrı worktree’ye geçer; işi korunur.`
|
|
140
|
+
: `Starting here moves ${holder} to a separate worktree; its work is kept.`,
|
|
141
|
+
}
|
|
142
|
+
: {}),
|
|
143
|
+
options: question.options.map((option) => ({
|
|
144
|
+
...option,
|
|
145
|
+
label: option.id === 'worktree'
|
|
146
|
+
? tr
|
|
147
|
+
? 'Ayrı worktree'
|
|
148
|
+
: 'Separate worktree'
|
|
149
|
+
: option.id === 'here'
|
|
150
|
+
? tr
|
|
151
|
+
? 'Bu klasörde yeni dal'
|
|
152
|
+
: 'New branch in this folder'
|
|
153
|
+
: option.id === 'keep_current'
|
|
154
|
+
? tr
|
|
155
|
+
? 'Mevcut dalda kal'
|
|
156
|
+
: 'Stay on the current branch'
|
|
157
|
+
: tr
|
|
158
|
+
? 'Şimdilik başlatma'
|
|
159
|
+
: 'Not now',
|
|
160
|
+
description: option.id === 'worktree'
|
|
161
|
+
? tr
|
|
162
|
+
? 'Bu klasör değişmez.'
|
|
163
|
+
: 'Leave this folder unchanged.'
|
|
164
|
+
: option.id === 'here'
|
|
165
|
+
? tr
|
|
166
|
+
? 'Klasör yeni dala geçer.'
|
|
167
|
+
: 'Switch this folder to the new branch.'
|
|
168
|
+
: option.id === 'keep_current'
|
|
169
|
+
? tr
|
|
170
|
+
? `${facts.currentBranch ?? 'HEAD'} üzerinde devam et.`
|
|
171
|
+
: `Continue on ${facts.currentBranch ?? 'HEAD'}.`
|
|
172
|
+
: tr
|
|
173
|
+
? 'Hiçbir şey oluşturulmaz.'
|
|
174
|
+
: 'Create nothing.',
|
|
175
|
+
})),
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
questions.sort((left, right) => Number(right.id === 'base') - Number(left.id === 'base'));
|
|
179
|
+
return {
|
|
180
|
+
...legacy,
|
|
181
|
+
message: tr ? 'Göreve başla' : 'Start task',
|
|
182
|
+
context: keepOnly
|
|
183
|
+
? tr
|
|
184
|
+
? 'Mevcut dalda devam edilecek.'
|
|
185
|
+
: 'Continue on the current branch.'
|
|
186
|
+
: (tr ? 'Yeni dal: ' : 'New branch: ') +
|
|
187
|
+
binding.name +
|
|
188
|
+
(binding.base ? ' · ' + sourceLabel(binding.base, facts.currentBranch) : ''),
|
|
189
|
+
example: keepOnly
|
|
190
|
+
? legacy.example
|
|
191
|
+
: tr
|
|
192
|
+
? 'Ayrı worktree, işi ayrı klasörde tutar.'
|
|
193
|
+
: 'A worktree keeps this task in its own folder.',
|
|
194
|
+
questions,
|
|
195
|
+
binding: JSON.parse(JSON.stringify(binding)),
|
|
196
|
+
previous: [previous, legacy],
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function legacyTaskStartDefinition(facts) {
|
|
76
200
|
const tr = facts.language === 'tr';
|
|
77
201
|
const copy = startCopy[tr ? 'tr' : 'en'];
|
|
78
202
|
const folder = facts.folder;
|