engineering-memory 1.11.9 → 1.11.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dispatcher/sections.mjs +6 -2
- package/install/codex-approval.mjs +457 -0
- package/install/files.mjs +5 -1
- package/install/installer.mjs +24 -1
- package/package.json +1 -1
- package/runtime/build.json +1 -1
- package/runtime/dist/src/config.js +1 -0
- package/runtime/dist/src/git/git-inspector.js +82 -3
- package/runtime/dist/src/git/verification-gate.js +30 -17
- package/runtime/dist/src/mcp/onboarding-tools.js +3 -16
- package/runtime/dist/src/mcp/questionnaire-tools.js +99 -15
- package/runtime/dist/src/mcp/tool-annotations.js +111 -0
- package/runtime/dist/src/mcp/tool-definitions.js +148 -28
- package/runtime/dist/src/mcp/worktree-tools.js +323 -236
- package/runtime/dist/src/project/repository.js +11 -5
- package/runtime/dist/src/runtime/active-context-store.js +3 -0
- package/runtime/dist/src/runtime/branch-preferences.js +0 -19
- package/runtime/dist/src/runtime/bridge-service.js +1131 -220
- package/runtime/dist/src/runtime/offline-outbox.js +33 -158
- package/runtime/dist/src/runtime/phase-timer.js +26 -0
- package/runtime/dist/src/runtime/privacy-detector.js +263 -0
- package/runtime/dist/src/runtime/questionnaire-store.js +210 -49
- package/runtime/dist/src/runtime/recovery-error.js +3 -1
- package/runtime/dist/src/runtime/runtime-entry.js +8 -0
- package/runtime/dist/src/runtime/runtime-host.js +1 -3
- package/runtime/dist/src/runtime/task-branch-store.js +17 -1
- package/runtime/dist/src/runtime/task-start.js +281 -0
- package/runtime/dist/src/runtime/worktree-pool.js +334 -24
- package/runtime/dist/src/utilities/process.js +1 -0
- package/skill/SKILL.md +3 -3
- package/skill/references/lifecycle.md +44 -19
- package/skill/references/memory-updates.md +9 -3
- package/skill/references/project-onboarding.md +19 -2
- package/skill/references/questionnaires.md +77 -15
|
@@ -1,10 +1,37 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
1
2
|
import * as z from 'zod/v4';
|
|
2
|
-
import {
|
|
3
|
+
import { cancelledDeliveryQuestionId, } from '../runtime/bridge-service.js';
|
|
4
|
+
import { configuredBase, sourceLabel, taskStartChoice, taskStartDefinition, } from '../runtime/task-start.js';
|
|
3
5
|
import { sha256, stableStringify } from '../utilities/hash.js';
|
|
4
6
|
import { askQuestionnaire } from './questionnaire-tools.js';
|
|
5
7
|
const repo = z.string().min(1);
|
|
6
8
|
const branch = z.string().min(1).max(255);
|
|
7
9
|
const remote = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/);
|
|
10
|
+
const presentation = z.enum(['host_native']).optional();
|
|
11
|
+
function deferredStart(input, questionnaireId) {
|
|
12
|
+
return {
|
|
13
|
+
ok: true,
|
|
14
|
+
data: json({
|
|
15
|
+
status: 'deferred',
|
|
16
|
+
allocated: false,
|
|
17
|
+
externalTaskId: input.externalTaskId,
|
|
18
|
+
questionnaireId,
|
|
19
|
+
reconsider: {
|
|
20
|
+
tool: 'task.branch',
|
|
21
|
+
arguments: {
|
|
22
|
+
repoRoot: input.repoRoot,
|
|
23
|
+
externalTaskId: input.externalTaskId,
|
|
24
|
+
decisionAttempt: (input.decisionAttempt ?? 0) + 1,
|
|
25
|
+
language: input.language,
|
|
26
|
+
name: input.name,
|
|
27
|
+
base: input.base,
|
|
28
|
+
keepCurrent: input.keepCurrent,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
nextAction: `Nothing was created, reserved or saved. Tell the user in one line that starting ${input.externalTaskId} is on hold. Continue only read-only work. When the user wants to start, call reconsider exactly as given; it asks the start form again.`,
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
8
35
|
const output = (r) => ({
|
|
9
36
|
content: [{ type: 'text', text: JSON.stringify(r) }],
|
|
10
37
|
...(!r.ok ? { isError: true } : {}),
|
|
@@ -42,7 +69,7 @@ export function registerWorktreeTools(server, service) {
|
|
|
42
69
|
}),
|
|
43
70
|
}, async (input) => output(await service.projectGitPreferences(input)));
|
|
44
71
|
server.registerTool('project.set_git_preferences', {
|
|
45
|
-
description: 'Confirm
|
|
72
|
+
description: 'Confirm every supplied shared branch choice in one durable native questionnaire (one question per role), then save the approved roles against expectedVersion in a single PUT. A role answered "leave unchanged" stays as it was and unanswered. Reconsider with a new decisionAttempt; retries keep the same attempt. Requires project owner/maintainer or organization administrator. Branches are creation preferences, never memory environment labels.',
|
|
46
73
|
inputSchema: z.strictObject({
|
|
47
74
|
repoRoot: repo,
|
|
48
75
|
projectId: z.string().uuid(),
|
|
@@ -52,6 +79,7 @@ export function registerWorktreeTools(server, service) {
|
|
|
52
79
|
production: preference.optional(),
|
|
53
80
|
test: preference.optional(),
|
|
54
81
|
language: z.enum(['tr', 'en']).default('en'),
|
|
82
|
+
presentation,
|
|
55
83
|
}),
|
|
56
84
|
}, async (input, context) => {
|
|
57
85
|
const current = await service.projectGitPreferences(input);
|
|
@@ -68,33 +96,41 @@ export function registerWorktreeTools(server, service) {
|
|
|
68
96
|
recovery: 'task.branch',
|
|
69
97
|
},
|
|
70
98
|
});
|
|
71
|
-
const
|
|
72
|
-
|
|
99
|
+
const roles = ['development', 'production', 'test'].filter((role) => Boolean(input[role]));
|
|
100
|
+
if (roles.length === 0)
|
|
101
|
+
return output({ ok: true, data: { unchanged: true } });
|
|
102
|
+
const roleLabel = (role) => {
|
|
73
103
|
const value = input[role];
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
return input.language === 'tr'
|
|
105
|
+
? role +
|
|
106
|
+
' dal tercihi: ' +
|
|
107
|
+
(value.branch ? value.remote + '/' + value.branch : 'henüz yok') +
|
|
108
|
+
'. Ortak proje ayarını kaydedelim mi?'
|
|
109
|
+
: 'Save the shared ' +
|
|
110
|
+
role +
|
|
111
|
+
' branch preference: ' +
|
|
112
|
+
(value.branch ? value.remote + '/' + value.branch : 'none yet') +
|
|
113
|
+
'?';
|
|
114
|
+
};
|
|
115
|
+
const digest = sha256(stableStringify({
|
|
116
|
+
projectId: input.projectId,
|
|
117
|
+
version: input.expectedVersion,
|
|
118
|
+
roles,
|
|
119
|
+
values: roles.map((role) => input[role]),
|
|
120
|
+
...(input.decisionAttempt ? { decisionAttempt: input.decisionAttempt } : {}),
|
|
121
|
+
}));
|
|
122
|
+
const questionnaireId = 'project-branches-' + digest;
|
|
123
|
+
const definition = {
|
|
124
|
+
repoRoot: input.repoRoot,
|
|
125
|
+
questionnaireId,
|
|
126
|
+
presentation: input.presentation,
|
|
127
|
+
language: input.language,
|
|
128
|
+
message: input.language === 'tr'
|
|
129
|
+
? 'Ortak dal tercihlerini kaydedelim mi? Hiçbir şey kaydedilmeden önce onayını bekliyor.'
|
|
130
|
+
: 'Save the shared branch preferences? Nothing is saved until you answer.',
|
|
131
|
+
questions: roles.map((role) => ({
|
|
132
|
+
id: role,
|
|
133
|
+
message: roleLabel(role),
|
|
98
134
|
options: [
|
|
99
135
|
{
|
|
100
136
|
id: 'approve',
|
|
@@ -105,16 +141,31 @@ export function registerWorktreeTools(server, service) {
|
|
|
105
141
|
label: input.language === 'tr' ? 'Şimdilik değiştirme' : 'Leave unchanged',
|
|
106
142
|
},
|
|
107
143
|
],
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
144
|
+
})),
|
|
145
|
+
};
|
|
146
|
+
const owner = {
|
|
147
|
+
tool: 'project.set_git_preferences',
|
|
148
|
+
...(input.decisionAttempt ? { decisionAttempt: input.decisionAttempt } : {}),
|
|
149
|
+
retryArguments: json({
|
|
150
|
+
projectId: input.projectId,
|
|
151
|
+
expectedVersion: input.expectedVersion,
|
|
152
|
+
development: input.development,
|
|
153
|
+
production: input.production,
|
|
154
|
+
test: input.test,
|
|
155
|
+
language: input.language,
|
|
156
|
+
}),
|
|
157
|
+
};
|
|
158
|
+
const form = await askQuestionnaire(server, service, definition, context, [], owner);
|
|
159
|
+
const record = await service.questionnaireResume({
|
|
160
|
+
repoRoot: input.repoRoot,
|
|
161
|
+
questionnaireId,
|
|
162
|
+
});
|
|
163
|
+
if (record.status !== 'answered')
|
|
164
|
+
return form;
|
|
165
|
+
const changes = { expectedVersion: input.expectedVersion };
|
|
166
|
+
for (const role of roles)
|
|
167
|
+
if (record.answers?.[role]?.choice === 'approve')
|
|
168
|
+
changes[role] = json(input[role]);
|
|
118
169
|
if (Object.keys(changes).length === 1)
|
|
119
170
|
return output({ ok: true, data: { unchanged: true } });
|
|
120
171
|
return output(await service.setProjectGitPreferences(input.projectId, changes));
|
|
@@ -128,7 +179,7 @@ export function registerWorktreeTools(server, service) {
|
|
|
128
179
|
inputSchema: z.strictObject(policy),
|
|
129
180
|
}, async (input) => output(await service.worktreePolicy(json(input))));
|
|
130
181
|
server.registerTool('task.branch', {
|
|
131
|
-
description: '
|
|
182
|
+
description: 'Start a write task with ONE durable native form: where the branch starts (unless base is given), the new branch name with a suggested default (unless name is given), and the folder: 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.',
|
|
132
183
|
inputSchema: z.strictObject({
|
|
133
184
|
repoRoot: repo,
|
|
134
185
|
externalTaskId: z.string().min(2).max(160),
|
|
@@ -137,177 +188,129 @@ export function registerWorktreeTools(server, service) {
|
|
|
137
188
|
base: baseSchema.optional(),
|
|
138
189
|
keepCurrent: z.boolean().optional(),
|
|
139
190
|
language: z.enum(['tr', 'en']).default('en'),
|
|
191
|
+
presentation,
|
|
140
192
|
}),
|
|
141
193
|
}, async (input, context) => {
|
|
142
|
-
const
|
|
143
|
-
if (
|
|
194
|
+
const preflight = await service.taskBranchPreflight(input);
|
|
195
|
+
if (!preflight.ok)
|
|
196
|
+
return output(preflight);
|
|
197
|
+
const flight = preflight.data;
|
|
198
|
+
if (flight.existingAllocation)
|
|
144
199
|
return output(await service.taskBranch(input));
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const definition = {
|
|
157
|
-
repoRoot: input.repoRoot,
|
|
158
|
-
questionnaireId: 'task-base-select-' + taskKey + '-' + prefs.version,
|
|
159
|
-
message: input.language === 'tr'
|
|
160
|
-
? 'Bu görev için başlangıç dalını seç.'
|
|
161
|
-
: 'Choose the starting branch for this task.',
|
|
162
|
-
language: input.language,
|
|
163
|
-
allowFreeText: false,
|
|
164
|
-
options: branchChoices(prefs, prefs.currentBranch),
|
|
165
|
-
};
|
|
166
|
-
const form = await askQuestionnaire(server, service, definition, context);
|
|
167
|
-
const record = await service.questionnaireResume({
|
|
168
|
-
repoRoot: input.repoRoot,
|
|
169
|
-
questionnaireId: definition.questionnaireId,
|
|
200
|
+
const role = input.base?.kind;
|
|
201
|
+
if ((role === 'development' || role === 'production' || role === 'test') &&
|
|
202
|
+
!configuredBase(flight.preferences, role))
|
|
203
|
+
return output({
|
|
204
|
+
ok: false,
|
|
205
|
+
error: {
|
|
206
|
+
kind: 'bridge_error',
|
|
207
|
+
retryable: false,
|
|
208
|
+
message: 'Choose a configured or task-specific base branch.',
|
|
209
|
+
recovery: 'task.branch',
|
|
210
|
+
},
|
|
170
211
|
});
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (choice === 'keep') {
|
|
175
|
-
keepCurrent = true;
|
|
176
|
-
base = { kind: 'current' };
|
|
177
|
-
}
|
|
178
|
-
else if (choice === 'other') {
|
|
179
|
-
const next = await askText(server, service, input, context, 'other-base', input.language === 'tr'
|
|
180
|
-
? 'Başlangıç için uzak dalın adını seç. origin remote kullanılacak.'
|
|
181
|
-
: 'Choose the remote base branch name. The origin remote will be used.');
|
|
182
|
-
if (!next.value)
|
|
183
|
-
return next.form;
|
|
184
|
-
base = { kind: 'remote', remote: 'origin', branch: next.value };
|
|
185
|
-
}
|
|
186
|
-
else if (choice === 'current' ||
|
|
187
|
-
choice === 'development' ||
|
|
188
|
-
choice === 'production' ||
|
|
189
|
-
choice === 'test')
|
|
190
|
-
base = { kind: choice };
|
|
191
|
-
else
|
|
192
|
-
return form;
|
|
193
|
-
}
|
|
194
|
-
if (keepCurrent) {
|
|
195
|
-
base = { kind: 'current' };
|
|
196
|
-
name = undefined;
|
|
197
|
-
}
|
|
198
|
-
base ??= { kind: 'current' };
|
|
199
|
-
if (base.kind === 'current')
|
|
200
|
-
base = { kind: 'current', commit: base.commit ?? prefs.currentCommit ?? undefined };
|
|
201
|
-
if (base.kind === 'development' || base.kind === 'production' || base.kind === 'test') {
|
|
202
|
-
const selected = prefs.branches[base.kind];
|
|
203
|
-
if (!selected.answered || !selected.remote || !selected.branch)
|
|
204
|
-
return output({
|
|
205
|
-
ok: false,
|
|
206
|
-
error: {
|
|
207
|
-
kind: 'bridge_error',
|
|
208
|
-
retryable: false,
|
|
209
|
-
message: 'Choose a configured or task-specific base branch.',
|
|
210
|
-
recovery: 'task.branch',
|
|
211
|
-
},
|
|
212
|
-
});
|
|
213
|
-
base = { kind: 'remote', remote: selected.remote, branch: selected.branch };
|
|
214
|
-
}
|
|
215
|
-
if (!keepCurrent && !name) {
|
|
216
|
-
const next = await askText(server, service, input, context, 'branch-name', input.language === 'tr'
|
|
217
|
-
? 'Yeni görev branch’inin adı ne olsun?'
|
|
218
|
-
: 'Name the new task branch.');
|
|
219
|
-
if (!next.value)
|
|
220
|
-
return next.form;
|
|
221
|
-
name = next.value;
|
|
222
|
-
}
|
|
223
|
-
const signature = sha256(stableStringify({
|
|
212
|
+
const definition = taskStartDefinition({ ...input, ...flight });
|
|
213
|
+
const owner = {
|
|
214
|
+
tool: 'task.branch',
|
|
224
215
|
externalTaskId: input.externalTaskId,
|
|
225
|
-
base,
|
|
226
|
-
name: name ?? null,
|
|
227
|
-
keepCurrent,
|
|
228
216
|
...(input.decisionAttempt ? { decisionAttempt: input.decisionAttempt } : {}),
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
? (prefs.currentBranch ?? 'HEAD')
|
|
236
|
-
: prefs.branches[base.kind].remote + '/' + prefs.branches[base.kind].branch;
|
|
237
|
-
const definition = {
|
|
238
|
-
repoRoot: input.repoRoot,
|
|
239
|
-
questionnaireId: 'task-branch-confirm-' + signature,
|
|
240
|
-
language: input.language,
|
|
241
|
-
allowFreeText: false,
|
|
242
|
-
message: input.language === 'tr'
|
|
243
|
-
? keepCurrent
|
|
244
|
-
? 'Bu klasörde mevcut branch üzerinde devam edilsin mi?'
|
|
245
|
-
: sourceLabel + ' kaynağından ' + name + ' branch’i ayrı worktree’de açılsın mı?'
|
|
246
|
-
: keepCurrent
|
|
247
|
-
? 'Continue in this checkout on its current branch?'
|
|
248
|
-
: 'Create ' + name + ' from ' + sourceLabel + ' in a separate worktree?',
|
|
249
|
-
options: [
|
|
250
|
-
{ id: 'approve', label: input.language === 'tr' ? 'Devam et' : 'Continue' },
|
|
251
|
-
{ id: 'defer', label: input.language === 'tr' ? 'Şimdilik başlatma' : 'Not now' },
|
|
252
|
-
],
|
|
217
|
+
retryArguments: json({
|
|
218
|
+
language: input.language,
|
|
219
|
+
name: input.name,
|
|
220
|
+
base: input.base,
|
|
221
|
+
keepCurrent: input.keepCurrent,
|
|
222
|
+
}),
|
|
253
223
|
};
|
|
254
|
-
const form = await askQuestionnaire(server, service, definition, context);
|
|
224
|
+
const form = await askQuestionnaire(server, service, { ...definition, repoRoot: input.repoRoot, presentation: input.presentation }, context, [], owner);
|
|
255
225
|
const record = await service.questionnaireResume({
|
|
256
226
|
repoRoot: input.repoRoot,
|
|
257
227
|
questionnaireId: definition.questionnaireId,
|
|
258
228
|
});
|
|
259
|
-
|
|
229
|
+
const choice = taskStartChoice(record);
|
|
230
|
+
if (!choice)
|
|
260
231
|
return form;
|
|
261
|
-
|
|
262
|
-
|
|
232
|
+
if (choice.status === 'deferred')
|
|
233
|
+
return output(deferredStart(input, definition.questionnaireId));
|
|
234
|
+
const reconsider = (reason) => output({
|
|
235
|
+
ok: false,
|
|
236
|
+
error: {
|
|
237
|
+
kind: 'bridge_error',
|
|
238
|
+
retryable: false,
|
|
239
|
+
message: `${reason} Call task.branch with decisionAttempt ${(input.decisionAttempt ?? 0) + 1} to ask the start form again.`,
|
|
240
|
+
recovery: 'task.branch',
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
if (choice.name && choice.name !== input.name && choice.name !== flight.suggestedName) {
|
|
244
|
+
const unusable = await service.unusableBranchName(input.repoRoot, choice.name);
|
|
245
|
+
if (unusable)
|
|
246
|
+
return reconsider(unusable);
|
|
247
|
+
}
|
|
248
|
+
const request = {
|
|
263
249
|
externalTaskId: input.externalTaskId,
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
keepCurrent,
|
|
250
|
+
name: choice.name ?? null,
|
|
251
|
+
keepCurrent: choice.keepCurrent,
|
|
267
252
|
...(input.decisionAttempt ? { decisionAttempt: input.decisionAttempt } : {}),
|
|
268
|
-
}));
|
|
269
|
-
const fallbackDefinition = {
|
|
270
|
-
...definition,
|
|
271
|
-
questionnaireId: 'task-branch-fallback-' + fallbackSignature,
|
|
272
|
-
message: input.language === 'tr'
|
|
273
|
-
? sourceLabel +
|
|
274
|
-
' uzak dalına ulaşılamadı. Yereldeki ' +
|
|
275
|
-
(prefs.currentBranch ?? 'HEAD') +
|
|
276
|
-
' commit’i ' +
|
|
277
|
-
(prefs.currentCommit?.slice(0, 12) ?? 'unborn HEAD') +
|
|
278
|
-
' üzerinden ayrı worktree ile devam edilsin mi?'
|
|
279
|
-
: 'The remote ' +
|
|
280
|
-
sourceLabel +
|
|
281
|
-
' is unavailable. Continue from local ' +
|
|
282
|
-
(prefs.currentBranch ?? 'HEAD') +
|
|
283
|
-
' commit ' +
|
|
284
|
-
(prefs.currentCommit?.slice(0, 12) ?? 'unborn HEAD') +
|
|
285
|
-
' in a separate worktree?',
|
|
286
253
|
};
|
|
254
|
+
const allocate = (base, decisionId) => service.taskBranch({
|
|
255
|
+
repoRoot: input.repoRoot,
|
|
256
|
+
externalTaskId: input.externalTaskId,
|
|
257
|
+
name: choice.name ?? undefined,
|
|
258
|
+
keepCurrent: choice.keepCurrent,
|
|
259
|
+
inPlace: choice.inPlace,
|
|
260
|
+
base,
|
|
261
|
+
decisionId,
|
|
262
|
+
startId: definition.questionnaireId,
|
|
263
|
+
decisionAttempt: input.decisionAttempt,
|
|
264
|
+
});
|
|
265
|
+
const fallbackBase = {
|
|
266
|
+
kind: 'current',
|
|
267
|
+
...(flight.currentCommit ? { commit: flight.currentCommit } : {}),
|
|
268
|
+
};
|
|
269
|
+
const fallbackId = 'task-branch-fallback-' +
|
|
270
|
+
sha256(stableStringify({ ...request, base: fallbackBase, inPlace: choice.inPlace }));
|
|
287
271
|
const fallback = async () => {
|
|
288
|
-
const
|
|
289
|
-
const
|
|
272
|
+
const tr = input.language === 'tr';
|
|
273
|
+
const from = sourceLabel(choice.base, flight.currentBranch);
|
|
274
|
+
const local = sourceLabel(fallbackBase, flight.currentBranch);
|
|
275
|
+
const fallbackForm = await askQuestionnaire(server, service, {
|
|
290
276
|
repoRoot: input.repoRoot,
|
|
291
|
-
questionnaireId:
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
277
|
+
questionnaireId: fallbackId,
|
|
278
|
+
language: input.language,
|
|
279
|
+
presentation: input.presentation,
|
|
280
|
+
allowFreeText: false,
|
|
281
|
+
message: tr
|
|
282
|
+
? from +
|
|
283
|
+
' uzak dalına ulaşılamadı. Yereldeki ' +
|
|
284
|
+
local +
|
|
285
|
+
' üzerinden ' +
|
|
286
|
+
(choice.inPlace ? 'bu klasörde' : 'ayrı worktree ile') +
|
|
287
|
+
' devam edilsin mi?'
|
|
288
|
+
: 'The remote ' +
|
|
289
|
+
from +
|
|
290
|
+
' is unavailable. Continue from local ' +
|
|
291
|
+
local +
|
|
292
|
+
(choice.inPlace ? ' in this folder?' : ' in a separate worktree?'),
|
|
293
|
+
options: [
|
|
294
|
+
{ id: 'approve', label: tr ? 'Devam et' : 'Continue' },
|
|
295
|
+
{ id: 'defer', label: tr ? 'Şimdilik başlatma' : 'Not now' },
|
|
296
|
+
],
|
|
297
|
+
}, context, [], owner);
|
|
298
|
+
const answer = await service.questionnaireResume({
|
|
296
299
|
repoRoot: input.repoRoot,
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
300
|
+
questionnaireId: fallbackId,
|
|
301
|
+
});
|
|
302
|
+
if (answer.status === 'answered' && answer.answer?.choice === 'defer')
|
|
303
|
+
return output(deferredStart(input, fallbackId));
|
|
304
|
+
if (answer.status !== 'answered' || answer.answer?.choice !== 'approve')
|
|
305
|
+
return fallbackForm;
|
|
306
|
+
return output(await allocate(fallbackBase, fallbackId));
|
|
304
307
|
};
|
|
305
|
-
if (base.kind === 'remote') {
|
|
308
|
+
if (choice.base.kind === 'remote') {
|
|
306
309
|
let recorded = false;
|
|
307
310
|
try {
|
|
308
311
|
await service.questionnaireResume({
|
|
309
312
|
repoRoot: input.repoRoot,
|
|
310
|
-
questionnaireId:
|
|
313
|
+
questionnaireId: fallbackId,
|
|
311
314
|
});
|
|
312
315
|
recorded = true;
|
|
313
316
|
}
|
|
@@ -315,16 +318,8 @@ export function registerWorktreeTools(server, service) {
|
|
|
315
318
|
if (recorded)
|
|
316
319
|
return fallback();
|
|
317
320
|
}
|
|
318
|
-
const attempt = await
|
|
319
|
-
|
|
320
|
-
externalTaskId: input.externalTaskId,
|
|
321
|
-
name,
|
|
322
|
-
keepCurrent,
|
|
323
|
-
base,
|
|
324
|
-
decisionId: definition.questionnaireId,
|
|
325
|
-
decisionAttempt: input.decisionAttempt,
|
|
326
|
-
});
|
|
327
|
-
if (base.kind === 'remote' &&
|
|
321
|
+
const attempt = await allocate(choice.base, definition.questionnaireId);
|
|
322
|
+
if (choice.base.kind === 'remote' &&
|
|
328
323
|
!attempt.ok &&
|
|
329
324
|
attempt.error?.message.startsWith('The selected remote branch could not be fetched.'))
|
|
330
325
|
return fallback();
|
|
@@ -335,24 +330,82 @@ export function registerWorktreeTools(server, service) {
|
|
|
335
330
|
repoRoot: repo,
|
|
336
331
|
externalTaskId: z.string().min(2).max(160),
|
|
337
332
|
generation: z.string().uuid(),
|
|
338
|
-
delivered: z.boolean().optional(),
|
|
339
333
|
});
|
|
340
334
|
server.registerTool('worktree.list', {
|
|
341
|
-
description: 'Discover existing managed Git checkouts and list activity and protection reasons before allocating another folder. Unregistered legacy checkouts remain protected until explicitly reconciled.',
|
|
335
|
+
description: 'Discover existing managed Git checkouts and list activity and protection reasons before allocating another folder. Unregistered legacy checkouts remain protected until explicitly reconciled. Slots outside the managed root are retired: never reused and not counted toward the limit.',
|
|
342
336
|
inputSchema: locationSchema,
|
|
343
337
|
}, async (input) => output(await service.worktreeOperation('list', input)));
|
|
344
338
|
server.registerTool('worktree.reconcile', {
|
|
345
|
-
description: 'Reconcile Git with the local registry, including unregistered legacy checkouts. Use releaseUnowned only on a selected unreserved recovered checkout after prior work has finished; a native source-bound confirmation and fresh safety checks precede reuse.
|
|
339
|
+
description: 'Reconcile Git with the local registry, including unregistered legacy checkouts. Use releaseUnowned only on a selected unreserved recovered checkout after prior work has finished; a native source-bound confirmation and fresh safety checks precede reuse. Use retireLegacy only when the user wants old-location slots gone: each retired folder is removed with git worktree remove after its own native approval, and the branch stays. Otherwise never delete files or reset branches.',
|
|
346
340
|
inputSchema: z.strictObject({
|
|
347
341
|
repoRoot: repo,
|
|
348
342
|
releaseUnowned: z.boolean().optional(),
|
|
343
|
+
retireLegacy: z.boolean().optional(),
|
|
349
344
|
decisionAttempt: z.number().int().min(0).default(0),
|
|
350
345
|
language: z.enum(['tr', 'en']).default('en'),
|
|
351
346
|
}),
|
|
352
347
|
}, async (input, context) => {
|
|
353
348
|
const inspection = await service.worktreeOperation('reconcile', input);
|
|
354
|
-
if (!inspection.ok || !input.releaseUnowned)
|
|
349
|
+
if (!inspection.ok || (!input.releaseUnowned && !input.retireLegacy))
|
|
355
350
|
return output(inspection);
|
|
351
|
+
if (input.retireLegacy) {
|
|
352
|
+
const items = inspection.data.items.filter((item) => item.activity === 'retired');
|
|
353
|
+
const retired = [];
|
|
354
|
+
const kept = [];
|
|
355
|
+
const refused = [];
|
|
356
|
+
for (const item of items) {
|
|
357
|
+
const questionnaireId = 'worktree-retire-' + sha256(item.repoRoot + item.generation);
|
|
358
|
+
const missing = item.reasons.includes('worktree directory missing');
|
|
359
|
+
const folder = basename(item.repoRoot);
|
|
360
|
+
const form = await askQuestionnaire(server, service, {
|
|
361
|
+
repoRoot: input.repoRoot,
|
|
362
|
+
questionnaireId,
|
|
363
|
+
language: input.language,
|
|
364
|
+
allowFreeText: false,
|
|
365
|
+
message: input.language === 'tr'
|
|
366
|
+
? folder +
|
|
367
|
+
(missing
|
|
368
|
+
? ' eski konumdaki bir worktree kaydı; klasör artık yok. Kaydı silinsin mi?'
|
|
369
|
+
: ' eski konumda kalan bir worktree (branch ' +
|
|
370
|
+
(item.branch ?? 'yok') +
|
|
371
|
+
'); artık yeniden kullanılmıyor. Silinsin mi? Branch kalır. Git yalnızca temiz bir klasörü kaldırır ve içindeki .gitignore kapsamındaki dosyaları da siler.')
|
|
372
|
+
: folder +
|
|
373
|
+
(missing
|
|
374
|
+
? ' is a worktree record in the old location whose folder is gone. Remove the record?'
|
|
375
|
+
: ' is a worktree left in the old location (branch ' +
|
|
376
|
+
(item.branch ?? 'none') +
|
|
377
|
+
'); it is no longer reused. Remove it? The branch stays. Git removes only a clean folder, and its ignored files go with it.'),
|
|
378
|
+
options: [
|
|
379
|
+
{
|
|
380
|
+
id: 'retire',
|
|
381
|
+
label: input.language === 'tr' ? 'Klasörü kaldır' : 'Remove the folder',
|
|
382
|
+
},
|
|
383
|
+
{ id: 'keep', label: input.language === 'tr' ? 'Kalsın' : 'Keep it' },
|
|
384
|
+
],
|
|
385
|
+
}, context);
|
|
386
|
+
const decision = await service.questionnaireResume({
|
|
387
|
+
repoRoot: input.repoRoot,
|
|
388
|
+
questionnaireId,
|
|
389
|
+
});
|
|
390
|
+
if (decision.status !== 'answered')
|
|
391
|
+
return form;
|
|
392
|
+
if (decision.answer?.choice !== 'retire') {
|
|
393
|
+
kept.push(item.repoRoot);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const result = await service.worktreeOperation('retire', {
|
|
397
|
+
repoRoot: input.repoRoot,
|
|
398
|
+
worktreePath: item.repoRoot,
|
|
399
|
+
generation: item.generation,
|
|
400
|
+
decisionId: questionnaireId,
|
|
401
|
+
});
|
|
402
|
+
if (result.ok)
|
|
403
|
+
retired.push(item.repoRoot);
|
|
404
|
+
else
|
|
405
|
+
refused.push({ repoRoot: item.repoRoot, message: result.error?.message ?? '' });
|
|
406
|
+
}
|
|
407
|
+
return output({ ok: true, data: json({ retired, kept, refused }) });
|
|
408
|
+
}
|
|
356
409
|
const data = inspection.data;
|
|
357
410
|
const entry = data.selected;
|
|
358
411
|
if (!entry?.recoveredWithoutReservation || !entry.baseCommit)
|
|
@@ -413,43 +466,77 @@ export function registerWorktreeTools(server, service) {
|
|
|
413
466
|
inputSchema: ownershipSchema,
|
|
414
467
|
}, async (input) => output(await service.worktreeOperation('heartbeat', input)));
|
|
415
468
|
server.registerTool('task.pause', {
|
|
416
|
-
description: 'Pause without abandoning or deleting work. Stop writing until session.resume returns the new ownership generation.',
|
|
417
|
-
inputSchema: ownershipSchema,
|
|
469
|
+
description: 'Pause without abandoning or deleting work. Stop writing until session.resume returns the new ownership generation. A pause records no delivery; settle delivery with worktree.release.',
|
|
470
|
+
inputSchema: ownershipSchema.extend({ delivered: z.boolean().optional() }),
|
|
418
471
|
}, async (input) => output(await service.worktreeOperation('pause', input)));
|
|
419
472
|
server.registerTool('worktree.release', {
|
|
420
|
-
description:
|
|
421
|
-
inputSchema: ownershipSchema
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
'
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
473
|
+
description: "Release this generation once the user settled its delivery. deliveryOutcome 'delivered' states that authorized delivery happened; 'cancelled' means the user decided not to deliver, and the native question asks whether the folder is kept, released, or its changes saved to a preserved ref before release. A pending delivery needs an outcome. Never force cleanup.",
|
|
474
|
+
inputSchema: ownershipSchema.extend({
|
|
475
|
+
deliveryOutcome: z.enum(['delivered', 'cancelled']).optional(),
|
|
476
|
+
delivered: z.boolean().optional(),
|
|
477
|
+
language: z.enum(['tr', 'en']).default('en'),
|
|
478
|
+
}),
|
|
479
|
+
}, async (input, context) => {
|
|
480
|
+
if (input.deliveryOutcome !== 'cancelled')
|
|
481
|
+
return output(await service.worktreeOperation('release', input));
|
|
482
|
+
const state = await service.worktreeFolder(input);
|
|
483
|
+
if (!state.ok)
|
|
484
|
+
return output(state);
|
|
485
|
+
const folder = state.data;
|
|
486
|
+
const questionnaireId = cancelledDeliveryQuestionId({ generation: input.generation, managed: folder.managed }, folder.clean);
|
|
487
|
+
if (!folder.deliveryPending)
|
|
488
|
+
return output(await service.worktreeOperation('cancel_delivery', {
|
|
489
|
+
...input,
|
|
490
|
+
decisionId: questionnaireId,
|
|
491
|
+
}));
|
|
492
|
+
const release = {
|
|
493
|
+
id: 'release',
|
|
494
|
+
label: input.language === 'tr'
|
|
495
|
+
? 'Klasörü boşalt; branch kalsın'
|
|
496
|
+
: 'Free the folder; the branch stays',
|
|
497
|
+
};
|
|
498
|
+
const save = {
|
|
499
|
+
id: 'save_and_release',
|
|
500
|
+
label: input.language === 'tr'
|
|
501
|
+
? 'Değişiklikleri kaydedip klasörü boşalt'
|
|
502
|
+
: 'Save the changes and free the folder',
|
|
503
|
+
};
|
|
504
|
+
const keep = {
|
|
505
|
+
id: 'keep',
|
|
506
|
+
label: input.language === 'tr' ? 'Klasör olduğu gibi kalsın' : 'Keep the folder as it is',
|
|
507
|
+
};
|
|
508
|
+
const form = await askQuestionnaire(server, service, {
|
|
509
|
+
repoRoot: input.repoRoot,
|
|
510
|
+
questionnaireId,
|
|
511
|
+
language: input.language,
|
|
512
|
+
allowFreeText: false,
|
|
513
|
+
message: input.language === 'tr'
|
|
514
|
+
? input.externalTaskId +
|
|
515
|
+
' teslim edilmeyecek. Bu klasör ne olsun?' +
|
|
516
|
+
(folder.clean
|
|
517
|
+
? ''
|
|
518
|
+
: folder.managed
|
|
519
|
+
? ' Klasörde kaydedilmemiş değişiklikler var; kaydedilirse bir Git ref’inde saklanır ve geri yüklenebilir.'
|
|
520
|
+
: ' Klasörde kaydedilmemiş değişiklikler var; bu senin klasörün olduğu için boşaltılsa da dosyalara dokunulmaz.')
|
|
521
|
+
: input.externalTaskId +
|
|
522
|
+
' will not be delivered. What should happen to this folder?' +
|
|
523
|
+
(folder.clean
|
|
524
|
+
? ''
|
|
525
|
+
: folder.managed
|
|
526
|
+
? ' It has uncommitted changes; saving keeps them in a Git ref they can be restored from.'
|
|
527
|
+
: ' It has uncommitted changes; this is your own folder, so its files are never touched when it is freed.'),
|
|
528
|
+
options: folder.clean || !folder.managed ? [release, keep] : [save, keep],
|
|
529
|
+
}, context);
|
|
530
|
+
const decision = await service.questionnaireResume({
|
|
531
|
+
repoRoot: input.repoRoot,
|
|
532
|
+
questionnaireId,
|
|
533
|
+
});
|
|
534
|
+
if (decision.status !== 'answered')
|
|
535
|
+
return form;
|
|
536
|
+
return output(await service.worktreeOperation('cancel_delivery', {
|
|
537
|
+
...input,
|
|
538
|
+
decisionId: questionnaireId,
|
|
539
|
+
}));
|
|
447
540
|
});
|
|
448
|
-
return {
|
|
449
|
-
form,
|
|
450
|
-
value: record.status === 'answered' && record.answer?.choice === 'set'
|
|
451
|
-
? record.answer.text
|
|
452
|
-
: undefined,
|
|
453
|
-
};
|
|
454
541
|
}
|
|
455
542
|
//# sourceMappingURL=worktree-tools.js.map
|