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,10 +1,38 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
1
2
|
import * as z from 'zod/v4';
|
|
2
|
-
import {
|
|
3
|
+
import { cancelledDeliveryQuestionId, forgetUnreadableQuestionId, } 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';
|
|
6
|
+
import { unreadableWorktree } from '../runtime/worktree-pool.js';
|
|
4
7
|
import { askQuestionnaire } from './questionnaire-tools.js';
|
|
5
8
|
const repo = z.string().min(1);
|
|
6
9
|
const branch = z.string().min(1).max(255);
|
|
7
10
|
const remote = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/);
|
|
11
|
+
const presentation = z.enum(['host_native']).optional();
|
|
12
|
+
function deferredStart(input, questionnaireId) {
|
|
13
|
+
return {
|
|
14
|
+
ok: true,
|
|
15
|
+
data: json({
|
|
16
|
+
status: 'deferred',
|
|
17
|
+
allocated: false,
|
|
18
|
+
externalTaskId: input.externalTaskId,
|
|
19
|
+
questionnaireId,
|
|
20
|
+
reconsider: {
|
|
21
|
+
tool: 'task.branch',
|
|
22
|
+
arguments: {
|
|
23
|
+
repoRoot: input.repoRoot,
|
|
24
|
+
externalTaskId: input.externalTaskId,
|
|
25
|
+
decisionAttempt: (input.decisionAttempt ?? 0) + 1,
|
|
26
|
+
language: input.language,
|
|
27
|
+
name: input.name,
|
|
28
|
+
base: input.base,
|
|
29
|
+
keepCurrent: input.keepCurrent,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
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.`,
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
8
36
|
const output = (r) => ({
|
|
9
37
|
content: [{ type: 'text', text: JSON.stringify(r) }],
|
|
10
38
|
...(!r.ok ? { isError: true } : {}),
|
|
@@ -42,7 +70,7 @@ export function registerWorktreeTools(server, service) {
|
|
|
42
70
|
}),
|
|
43
71
|
}, async (input) => output(await service.projectGitPreferences(input)));
|
|
44
72
|
server.registerTool('project.set_git_preferences', {
|
|
45
|
-
description: 'Confirm
|
|
73
|
+
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
74
|
inputSchema: z.strictObject({
|
|
47
75
|
repoRoot: repo,
|
|
48
76
|
projectId: z.string().uuid(),
|
|
@@ -52,6 +80,7 @@ export function registerWorktreeTools(server, service) {
|
|
|
52
80
|
production: preference.optional(),
|
|
53
81
|
test: preference.optional(),
|
|
54
82
|
language: z.enum(['tr', 'en']).default('en'),
|
|
83
|
+
presentation,
|
|
55
84
|
}),
|
|
56
85
|
}, async (input, context) => {
|
|
57
86
|
const current = await service.projectGitPreferences(input);
|
|
@@ -68,33 +97,41 @@ export function registerWorktreeTools(server, service) {
|
|
|
68
97
|
recovery: 'task.branch',
|
|
69
98
|
},
|
|
70
99
|
});
|
|
71
|
-
const
|
|
72
|
-
|
|
100
|
+
const roles = ['development', 'production', 'test'].filter((role) => Boolean(input[role]));
|
|
101
|
+
if (roles.length === 0)
|
|
102
|
+
return output({ ok: true, data: { unchanged: true } });
|
|
103
|
+
const roleLabel = (role) => {
|
|
73
104
|
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
|
-
|
|
105
|
+
return input.language === 'tr'
|
|
106
|
+
? role +
|
|
107
|
+
' dal tercihi: ' +
|
|
108
|
+
(value.branch ? value.remote + '/' + value.branch : 'henüz yok') +
|
|
109
|
+
'. Ortak proje ayarını kaydedelim mi?'
|
|
110
|
+
: 'Save the shared ' +
|
|
111
|
+
role +
|
|
112
|
+
' branch preference: ' +
|
|
113
|
+
(value.branch ? value.remote + '/' + value.branch : 'none yet') +
|
|
114
|
+
'?';
|
|
115
|
+
};
|
|
116
|
+
const digest = sha256(stableStringify({
|
|
117
|
+
projectId: input.projectId,
|
|
118
|
+
version: input.expectedVersion,
|
|
119
|
+
roles,
|
|
120
|
+
values: roles.map((role) => input[role]),
|
|
121
|
+
...(input.decisionAttempt ? { decisionAttempt: input.decisionAttempt } : {}),
|
|
122
|
+
}));
|
|
123
|
+
const questionnaireId = 'project-branches-' + digest;
|
|
124
|
+
const definition = {
|
|
125
|
+
repoRoot: input.repoRoot,
|
|
126
|
+
questionnaireId,
|
|
127
|
+
presentation: input.presentation,
|
|
128
|
+
language: input.language,
|
|
129
|
+
message: input.language === 'tr'
|
|
130
|
+
? 'Ortak dal tercihlerini kaydedelim mi? Hiçbir şey kaydedilmeden önce onayını bekliyor.'
|
|
131
|
+
: 'Save the shared branch preferences? Nothing is saved until you answer.',
|
|
132
|
+
questions: roles.map((role) => ({
|
|
133
|
+
id: role,
|
|
134
|
+
message: roleLabel(role),
|
|
98
135
|
options: [
|
|
99
136
|
{
|
|
100
137
|
id: 'approve',
|
|
@@ -105,16 +142,31 @@ export function registerWorktreeTools(server, service) {
|
|
|
105
142
|
label: input.language === 'tr' ? 'Şimdilik değiştirme' : 'Leave unchanged',
|
|
106
143
|
},
|
|
107
144
|
],
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
145
|
+
})),
|
|
146
|
+
};
|
|
147
|
+
const owner = {
|
|
148
|
+
tool: 'project.set_git_preferences',
|
|
149
|
+
...(input.decisionAttempt ? { decisionAttempt: input.decisionAttempt } : {}),
|
|
150
|
+
retryArguments: json({
|
|
151
|
+
projectId: input.projectId,
|
|
152
|
+
expectedVersion: input.expectedVersion,
|
|
153
|
+
development: input.development,
|
|
154
|
+
production: input.production,
|
|
155
|
+
test: input.test,
|
|
156
|
+
language: input.language,
|
|
157
|
+
}),
|
|
158
|
+
};
|
|
159
|
+
const form = await askQuestionnaire(server, service, definition, context, [], owner);
|
|
160
|
+
const record = await service.questionnaireResume({
|
|
161
|
+
repoRoot: input.repoRoot,
|
|
162
|
+
questionnaireId,
|
|
163
|
+
});
|
|
164
|
+
if (record.status !== 'answered')
|
|
165
|
+
return form;
|
|
166
|
+
const changes = { expectedVersion: input.expectedVersion };
|
|
167
|
+
for (const role of roles)
|
|
168
|
+
if (record.answers?.[role]?.choice === 'approve')
|
|
169
|
+
changes[role] = json(input[role]);
|
|
118
170
|
if (Object.keys(changes).length === 1)
|
|
119
171
|
return output({ ok: true, data: { unchanged: true } });
|
|
120
172
|
return output(await service.setProjectGitPreferences(input.projectId, changes));
|
|
@@ -128,7 +180,7 @@ export function registerWorktreeTools(server, service) {
|
|
|
128
180
|
inputSchema: z.strictObject(policy),
|
|
129
181
|
}, async (input) => output(await service.worktreePolicy(json(input))));
|
|
130
182
|
server.registerTool('task.branch', {
|
|
131
|
-
description: '
|
|
183
|
+
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
184
|
inputSchema: z.strictObject({
|
|
133
185
|
repoRoot: repo,
|
|
134
186
|
externalTaskId: z.string().min(2).max(160),
|
|
@@ -137,177 +189,129 @@ export function registerWorktreeTools(server, service) {
|
|
|
137
189
|
base: baseSchema.optional(),
|
|
138
190
|
keepCurrent: z.boolean().optional(),
|
|
139
191
|
language: z.enum(['tr', 'en']).default('en'),
|
|
192
|
+
presentation,
|
|
140
193
|
}),
|
|
141
194
|
}, async (input, context) => {
|
|
142
|
-
const
|
|
143
|
-
if (
|
|
195
|
+
const preflight = await service.taskBranchPreflight(input);
|
|
196
|
+
if (!preflight.ok)
|
|
197
|
+
return output(preflight);
|
|
198
|
+
const flight = preflight.data;
|
|
199
|
+
if (flight.existingAllocation)
|
|
144
200
|
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,
|
|
201
|
+
const role = input.base?.kind;
|
|
202
|
+
if ((role === 'development' || role === 'production' || role === 'test') &&
|
|
203
|
+
!configuredBase(flight.preferences, role))
|
|
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
|
+
},
|
|
170
212
|
});
|
|
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({
|
|
213
|
+
const definition = taskStartDefinition({ ...input, ...flight });
|
|
214
|
+
const owner = {
|
|
215
|
+
tool: 'task.branch',
|
|
224
216
|
externalTaskId: input.externalTaskId,
|
|
225
|
-
base,
|
|
226
|
-
name: name ?? null,
|
|
227
|
-
keepCurrent,
|
|
228
217
|
...(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
|
-
],
|
|
218
|
+
retryArguments: json({
|
|
219
|
+
language: input.language,
|
|
220
|
+
name: input.name,
|
|
221
|
+
base: input.base,
|
|
222
|
+
keepCurrent: input.keepCurrent,
|
|
223
|
+
}),
|
|
253
224
|
};
|
|
254
|
-
const form = await askQuestionnaire(server, service, definition, context);
|
|
225
|
+
const form = await askQuestionnaire(server, service, { ...definition, repoRoot: input.repoRoot, presentation: input.presentation }, context, [], owner);
|
|
255
226
|
const record = await service.questionnaireResume({
|
|
256
227
|
repoRoot: input.repoRoot,
|
|
257
228
|
questionnaireId: definition.questionnaireId,
|
|
258
229
|
});
|
|
259
|
-
|
|
230
|
+
const choice = taskStartChoice(record);
|
|
231
|
+
if (!choice)
|
|
260
232
|
return form;
|
|
261
|
-
|
|
262
|
-
|
|
233
|
+
if (choice.status === 'deferred')
|
|
234
|
+
return output(deferredStart(input, definition.questionnaireId));
|
|
235
|
+
const reconsider = (reason) => output({
|
|
236
|
+
ok: false,
|
|
237
|
+
error: {
|
|
238
|
+
kind: 'bridge_error',
|
|
239
|
+
retryable: false,
|
|
240
|
+
message: `${reason} Call task.branch with decisionAttempt ${(input.decisionAttempt ?? 0) + 1} to ask the start form again.`,
|
|
241
|
+
recovery: 'task.branch',
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
if (choice.name && choice.name !== input.name && choice.name !== flight.suggestedName) {
|
|
245
|
+
const unusable = await service.unusableBranchName(input.repoRoot, choice.name);
|
|
246
|
+
if (unusable)
|
|
247
|
+
return reconsider(unusable);
|
|
248
|
+
}
|
|
249
|
+
const request = {
|
|
263
250
|
externalTaskId: input.externalTaskId,
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
keepCurrent,
|
|
251
|
+
name: choice.name ?? null,
|
|
252
|
+
keepCurrent: choice.keepCurrent,
|
|
267
253
|
...(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
254
|
};
|
|
255
|
+
const allocate = (base, decisionId) => service.taskBranch({
|
|
256
|
+
repoRoot: input.repoRoot,
|
|
257
|
+
externalTaskId: input.externalTaskId,
|
|
258
|
+
name: choice.name ?? undefined,
|
|
259
|
+
keepCurrent: choice.keepCurrent,
|
|
260
|
+
inPlace: choice.inPlace,
|
|
261
|
+
base,
|
|
262
|
+
decisionId,
|
|
263
|
+
startId: definition.questionnaireId,
|
|
264
|
+
decisionAttempt: input.decisionAttempt,
|
|
265
|
+
});
|
|
266
|
+
const fallbackBase = {
|
|
267
|
+
kind: 'current',
|
|
268
|
+
...(flight.currentCommit ? { commit: flight.currentCommit } : {}),
|
|
269
|
+
};
|
|
270
|
+
const fallbackId = 'task-branch-fallback-' +
|
|
271
|
+
sha256(stableStringify({ ...request, base: fallbackBase, inPlace: choice.inPlace }));
|
|
287
272
|
const fallback = async () => {
|
|
288
|
-
const
|
|
289
|
-
const
|
|
273
|
+
const tr = input.language === 'tr';
|
|
274
|
+
const from = sourceLabel(choice.base, flight.currentBranch);
|
|
275
|
+
const local = sourceLabel(fallbackBase, flight.currentBranch);
|
|
276
|
+
const fallbackForm = await askQuestionnaire(server, service, {
|
|
290
277
|
repoRoot: input.repoRoot,
|
|
291
|
-
questionnaireId:
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
278
|
+
questionnaireId: fallbackId,
|
|
279
|
+
language: input.language,
|
|
280
|
+
presentation: input.presentation,
|
|
281
|
+
allowFreeText: false,
|
|
282
|
+
message: tr
|
|
283
|
+
? from +
|
|
284
|
+
' uzak dalına ulaşılamadı. Yereldeki ' +
|
|
285
|
+
local +
|
|
286
|
+
' üzerinden ' +
|
|
287
|
+
(choice.inPlace ? 'bu klasörde' : 'ayrı worktree ile') +
|
|
288
|
+
' devam edilsin mi?'
|
|
289
|
+
: 'The remote ' +
|
|
290
|
+
from +
|
|
291
|
+
' is unavailable. Continue from local ' +
|
|
292
|
+
local +
|
|
293
|
+
(choice.inPlace ? ' in this folder?' : ' in a separate worktree?'),
|
|
294
|
+
options: [
|
|
295
|
+
{ id: 'approve', label: tr ? 'Devam et' : 'Continue' },
|
|
296
|
+
{ id: 'defer', label: tr ? 'Şimdilik başlatma' : 'Not now' },
|
|
297
|
+
],
|
|
298
|
+
}, context, [], owner);
|
|
299
|
+
const answer = await service.questionnaireResume({
|
|
296
300
|
repoRoot: input.repoRoot,
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
301
|
+
questionnaireId: fallbackId,
|
|
302
|
+
});
|
|
303
|
+
if (answer.status === 'answered' && answer.answer?.choice === 'defer')
|
|
304
|
+
return output(deferredStart(input, fallbackId));
|
|
305
|
+
if (answer.status !== 'answered' || answer.answer?.choice !== 'approve')
|
|
306
|
+
return fallbackForm;
|
|
307
|
+
return output(await allocate(fallbackBase, fallbackId));
|
|
304
308
|
};
|
|
305
|
-
if (base.kind === 'remote') {
|
|
309
|
+
if (choice.base.kind === 'remote') {
|
|
306
310
|
let recorded = false;
|
|
307
311
|
try {
|
|
308
312
|
await service.questionnaireResume({
|
|
309
313
|
repoRoot: input.repoRoot,
|
|
310
|
-
questionnaireId:
|
|
314
|
+
questionnaireId: fallbackId,
|
|
311
315
|
});
|
|
312
316
|
recorded = true;
|
|
313
317
|
}
|
|
@@ -315,16 +319,8 @@ export function registerWorktreeTools(server, service) {
|
|
|
315
319
|
if (recorded)
|
|
316
320
|
return fallback();
|
|
317
321
|
}
|
|
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' &&
|
|
322
|
+
const attempt = await allocate(choice.base, definition.questionnaireId);
|
|
323
|
+
if (choice.base.kind === 'remote' &&
|
|
328
324
|
!attempt.ok &&
|
|
329
325
|
attempt.error?.message.startsWith('The selected remote branch could not be fetched.'))
|
|
330
326
|
return fallback();
|
|
@@ -335,24 +331,153 @@ export function registerWorktreeTools(server, service) {
|
|
|
335
331
|
repoRoot: repo,
|
|
336
332
|
externalTaskId: z.string().min(2).max(160),
|
|
337
333
|
generation: z.string().uuid(),
|
|
338
|
-
delivered: z.boolean().optional(),
|
|
339
334
|
});
|
|
340
335
|
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.',
|
|
336
|
+
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
337
|
inputSchema: locationSchema,
|
|
343
338
|
}, async (input) => output(await service.worktreeOperation('list', input)));
|
|
344
339
|
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.
|
|
340
|
+
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. Use forgetUnreadable with the externalTaskId of a checkout that is gone or can no longer be read as a Git worktree, called from a working checkout of the same project: after its own native approval Engineering Memory stops tracking that entry, so it no longer counts toward the limit, and no file in the folder is read, moved or deleted. Otherwise never delete files or reset branches.',
|
|
346
341
|
inputSchema: z.strictObject({
|
|
347
342
|
repoRoot: repo,
|
|
348
343
|
releaseUnowned: z.boolean().optional(),
|
|
344
|
+
retireLegacy: z.boolean().optional(),
|
|
345
|
+
forgetUnreadable: z.strictObject({ externalTaskId: z.string().min(2).max(160) }).optional(),
|
|
349
346
|
decisionAttempt: z.number().int().min(0).default(0),
|
|
350
347
|
language: z.enum(['tr', 'en']).default('en'),
|
|
351
348
|
}),
|
|
352
349
|
}, async (input, context) => {
|
|
353
350
|
const inspection = await service.worktreeOperation('reconcile', input);
|
|
354
|
-
if (!inspection.ok ||
|
|
351
|
+
if (!inspection.ok ||
|
|
352
|
+
(!input.releaseUnowned && !input.retireLegacy && !input.forgetUnreadable))
|
|
355
353
|
return output(inspection);
|
|
354
|
+
if (input.forgetUnreadable) {
|
|
355
|
+
const item = inspection.data.items.find((entry) => entry.externalTaskId === input.forgetUnreadable.externalTaskId);
|
|
356
|
+
if (!item || item.activity === 'retired' || !unreadableWorktree(item))
|
|
357
|
+
return output({
|
|
358
|
+
ok: false,
|
|
359
|
+
error: {
|
|
360
|
+
kind: 'bridge_error',
|
|
361
|
+
retryable: false,
|
|
362
|
+
recovery: 'worktree.list',
|
|
363
|
+
message: 'Only a checkout that is missing or can no longer be read as a Git worktree is forgotten. Call worktree.list to see this task: a readable one is freed with worktree.release, and a retired slot is removed with worktree.reconcile and retireLegacy.',
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
const questionnaireId = forgetUnreadableQuestionId(item, input.decisionAttempt);
|
|
367
|
+
const folder = basename(item.repoRoot);
|
|
368
|
+
const tr = input.language === 'tr';
|
|
369
|
+
const form = await askQuestionnaire(server, service, {
|
|
370
|
+
repoRoot: input.repoRoot,
|
|
371
|
+
questionnaireId,
|
|
372
|
+
language: input.language,
|
|
373
|
+
allowFreeText: false,
|
|
374
|
+
message: tr
|
|
375
|
+
? item.externalTaskId +
|
|
376
|
+
' görevinin ' +
|
|
377
|
+
folder +
|
|
378
|
+
' klasörü artık bir Git çalışma kopyası olarak okunamıyor. Engineering Memory bu klasörü takip etmeyi bıraksın mı? Klasördeki hiçbir dosyaya dokunulmaz, hiçbiri silinmez.' +
|
|
379
|
+
(item.pendingDelivery
|
|
380
|
+
? ' Bu görev teslimi sonuçlanmadan kapandı; teslim edilmemiş iş varsa yalnızca bu klasörde duruyor.'
|
|
381
|
+
: '')
|
|
382
|
+
: 'The ' +
|
|
383
|
+
folder +
|
|
384
|
+
' folder of task ' +
|
|
385
|
+
item.externalTaskId +
|
|
386
|
+
' can no longer be read as a Git checkout. Should Engineering Memory stop tracking it? No file in the folder is touched or deleted.' +
|
|
387
|
+
(item.pendingDelivery
|
|
388
|
+
? ' That task closed without its delivery being settled, so any undelivered work exists only in that folder.'
|
|
389
|
+
: ''),
|
|
390
|
+
options: [
|
|
391
|
+
{
|
|
392
|
+
id: 'forget',
|
|
393
|
+
label: tr
|
|
394
|
+
? 'Takip etmeyi bırak; dosyalara dokunma'
|
|
395
|
+
: 'Stop tracking it; leave the files alone',
|
|
396
|
+
},
|
|
397
|
+
{ id: 'keep', label: tr ? 'Takip etmeye devam et' : 'Keep tracking it' },
|
|
398
|
+
],
|
|
399
|
+
}, context, [], {
|
|
400
|
+
tool: 'worktree.reconcile',
|
|
401
|
+
...(input.decisionAttempt ? { decisionAttempt: input.decisionAttempt } : {}),
|
|
402
|
+
retryArguments: json({
|
|
403
|
+
forgetUnreadable: input.forgetUnreadable,
|
|
404
|
+
language: input.language,
|
|
405
|
+
}),
|
|
406
|
+
});
|
|
407
|
+
const decision = await service.questionnaireResume({
|
|
408
|
+
repoRoot: input.repoRoot,
|
|
409
|
+
questionnaireId,
|
|
410
|
+
});
|
|
411
|
+
if (decision.status !== 'answered')
|
|
412
|
+
return form;
|
|
413
|
+
if (decision.answer?.choice !== 'forget')
|
|
414
|
+
return output({ ok: true, data: json({ kept: item.externalTaskId }) });
|
|
415
|
+
return output(await service.worktreeOperation('forget_unreadable', {
|
|
416
|
+
repoRoot: input.repoRoot,
|
|
417
|
+
externalTaskId: item.externalTaskId,
|
|
418
|
+
generation: item.generation,
|
|
419
|
+
decisionAttempt: input.decisionAttempt,
|
|
420
|
+
decisionId: questionnaireId,
|
|
421
|
+
}));
|
|
422
|
+
}
|
|
423
|
+
if (input.retireLegacy) {
|
|
424
|
+
const items = inspection.data.items.filter((item) => item.activity === 'retired');
|
|
425
|
+
const retired = [];
|
|
426
|
+
const kept = [];
|
|
427
|
+
const refused = [];
|
|
428
|
+
for (const item of items) {
|
|
429
|
+
const questionnaireId = 'worktree-retire-' + sha256(item.repoRoot + item.generation);
|
|
430
|
+
const missing = item.reasons.includes('worktree directory missing');
|
|
431
|
+
const folder = basename(item.repoRoot);
|
|
432
|
+
const form = await askQuestionnaire(server, service, {
|
|
433
|
+
repoRoot: input.repoRoot,
|
|
434
|
+
questionnaireId,
|
|
435
|
+
language: input.language,
|
|
436
|
+
allowFreeText: false,
|
|
437
|
+
message: input.language === 'tr'
|
|
438
|
+
? folder +
|
|
439
|
+
(missing
|
|
440
|
+
? ' eski konumdaki bir worktree kaydı; klasör artık yok. Kaydı silinsin mi?'
|
|
441
|
+
: ' eski konumda kalan bir worktree (branch ' +
|
|
442
|
+
(item.branch ?? 'yok') +
|
|
443
|
+
'); 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.')
|
|
444
|
+
: folder +
|
|
445
|
+
(missing
|
|
446
|
+
? ' is a worktree record in the old location whose folder is gone. Remove the record?'
|
|
447
|
+
: ' is a worktree left in the old location (branch ' +
|
|
448
|
+
(item.branch ?? 'none') +
|
|
449
|
+
'); it is no longer reused. Remove it? The branch stays. Git removes only a clean folder, and its ignored files go with it.'),
|
|
450
|
+
options: [
|
|
451
|
+
{
|
|
452
|
+
id: 'retire',
|
|
453
|
+
label: input.language === 'tr' ? 'Klasörü kaldır' : 'Remove the folder',
|
|
454
|
+
},
|
|
455
|
+
{ id: 'keep', label: input.language === 'tr' ? 'Kalsın' : 'Keep it' },
|
|
456
|
+
],
|
|
457
|
+
}, context);
|
|
458
|
+
const decision = await service.questionnaireResume({
|
|
459
|
+
repoRoot: input.repoRoot,
|
|
460
|
+
questionnaireId,
|
|
461
|
+
});
|
|
462
|
+
if (decision.status !== 'answered')
|
|
463
|
+
return form;
|
|
464
|
+
if (decision.answer?.choice !== 'retire') {
|
|
465
|
+
kept.push(item.repoRoot);
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
const result = await service.worktreeOperation('retire', {
|
|
469
|
+
repoRoot: input.repoRoot,
|
|
470
|
+
worktreePath: item.repoRoot,
|
|
471
|
+
generation: item.generation,
|
|
472
|
+
decisionId: questionnaireId,
|
|
473
|
+
});
|
|
474
|
+
if (result.ok)
|
|
475
|
+
retired.push(item.repoRoot);
|
|
476
|
+
else
|
|
477
|
+
refused.push({ repoRoot: item.repoRoot, message: result.error?.message ?? '' });
|
|
478
|
+
}
|
|
479
|
+
return output({ ok: true, data: json({ retired, kept, refused }) });
|
|
480
|
+
}
|
|
356
481
|
const data = inspection.data;
|
|
357
482
|
const entry = data.selected;
|
|
358
483
|
if (!entry?.recoveredWithoutReservation || !entry.baseCommit)
|
|
@@ -413,43 +538,77 @@ export function registerWorktreeTools(server, service) {
|
|
|
413
538
|
inputSchema: ownershipSchema,
|
|
414
539
|
}, async (input) => output(await service.worktreeOperation('heartbeat', input)));
|
|
415
540
|
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,
|
|
541
|
+
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.',
|
|
542
|
+
inputSchema: ownershipSchema.extend({ delivered: z.boolean().optional() }),
|
|
418
543
|
}, async (input) => output(await service.worktreeOperation('pause', input)));
|
|
419
544
|
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
|
-
|
|
545
|
+
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.",
|
|
546
|
+
inputSchema: ownershipSchema.extend({
|
|
547
|
+
deliveryOutcome: z.enum(['delivered', 'cancelled']).optional(),
|
|
548
|
+
delivered: z.boolean().optional(),
|
|
549
|
+
language: z.enum(['tr', 'en']).default('en'),
|
|
550
|
+
}),
|
|
551
|
+
}, async (input, context) => {
|
|
552
|
+
if (input.deliveryOutcome !== 'cancelled')
|
|
553
|
+
return output(await service.worktreeOperation('release', input));
|
|
554
|
+
const state = await service.worktreeFolder(input);
|
|
555
|
+
if (!state.ok)
|
|
556
|
+
return output(state);
|
|
557
|
+
const folder = state.data;
|
|
558
|
+
const questionnaireId = cancelledDeliveryQuestionId({ generation: input.generation, managed: folder.managed }, folder.clean);
|
|
559
|
+
if (!folder.deliveryPending)
|
|
560
|
+
return output(await service.worktreeOperation('cancel_delivery', {
|
|
561
|
+
...input,
|
|
562
|
+
decisionId: questionnaireId,
|
|
563
|
+
}));
|
|
564
|
+
const release = {
|
|
565
|
+
id: 'release',
|
|
566
|
+
label: input.language === 'tr'
|
|
567
|
+
? 'Klasörü boşalt; branch kalsın'
|
|
568
|
+
: 'Free the folder; the branch stays',
|
|
569
|
+
};
|
|
570
|
+
const save = {
|
|
571
|
+
id: 'save_and_release',
|
|
572
|
+
label: input.language === 'tr'
|
|
573
|
+
? 'Değişiklikleri kaydedip klasörü boşalt'
|
|
574
|
+
: 'Save the changes and free the folder',
|
|
575
|
+
};
|
|
576
|
+
const keep = {
|
|
577
|
+
id: 'keep',
|
|
578
|
+
label: input.language === 'tr' ? 'Klasör olduğu gibi kalsın' : 'Keep the folder as it is',
|
|
579
|
+
};
|
|
580
|
+
const form = await askQuestionnaire(server, service, {
|
|
581
|
+
repoRoot: input.repoRoot,
|
|
582
|
+
questionnaireId,
|
|
583
|
+
language: input.language,
|
|
584
|
+
allowFreeText: false,
|
|
585
|
+
message: input.language === 'tr'
|
|
586
|
+
? input.externalTaskId +
|
|
587
|
+
' teslim edilmeyecek. Bu klasör ne olsun?' +
|
|
588
|
+
(folder.clean
|
|
589
|
+
? ''
|
|
590
|
+
: folder.managed
|
|
591
|
+
? ' Klasörde kaydedilmemiş değişiklikler var; kaydedilirse bir Git ref’inde saklanır ve geri yüklenebilir.'
|
|
592
|
+
: ' Klasörde kaydedilmemiş değişiklikler var; bu senin klasörün olduğu için boşaltılsa da dosyalara dokunulmaz.')
|
|
593
|
+
: input.externalTaskId +
|
|
594
|
+
' will not be delivered. What should happen to this folder?' +
|
|
595
|
+
(folder.clean
|
|
596
|
+
? ''
|
|
597
|
+
: folder.managed
|
|
598
|
+
? ' It has uncommitted changes; saving keeps them in a Git ref they can be restored from.'
|
|
599
|
+
: ' It has uncommitted changes; this is your own folder, so its files are never touched when it is freed.'),
|
|
600
|
+
options: folder.clean || !folder.managed ? [release, keep] : [save, keep],
|
|
601
|
+
}, context);
|
|
602
|
+
const decision = await service.questionnaireResume({
|
|
603
|
+
repoRoot: input.repoRoot,
|
|
604
|
+
questionnaireId,
|
|
605
|
+
});
|
|
606
|
+
if (decision.status !== 'answered')
|
|
607
|
+
return form;
|
|
608
|
+
return output(await service.worktreeOperation('cancel_delivery', {
|
|
609
|
+
...input,
|
|
610
|
+
decisionId: questionnaireId,
|
|
611
|
+
}));
|
|
447
612
|
});
|
|
448
|
-
return {
|
|
449
|
-
form,
|
|
450
|
-
value: record.status === 'answered' && record.answer?.choice === 'set'
|
|
451
|
-
? record.answer.text
|
|
452
|
-
: undefined,
|
|
453
|
-
};
|
|
454
613
|
}
|
|
455
614
|
//# sourceMappingURL=worktree-tools.js.map
|