engineering-memory 1.11.3 → 1.11.5
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 -0
- package/package.json +1 -1
- package/runtime/dist/src/config.js +2 -0
- package/runtime/dist/src/git/git-inspector.js +41 -2
- package/runtime/dist/src/git/verification-gate.js +40 -1
- package/runtime/dist/src/index.js +15 -1
- package/runtime/dist/src/mcp/onboarding-tools.js +39 -1
- package/runtime/dist/src/mcp/server.js +2 -0
- package/runtime/dist/src/mcp/tool-definitions.js +15 -12
- package/runtime/dist/src/mcp/worktree-tools.js +392 -0
- package/runtime/dist/src/project/project-intake.js +9 -2
- package/runtime/dist/src/runtime/api-client.js +4 -0
- package/runtime/dist/src/runtime/branch-preferences.js +53 -0
- package/runtime/dist/src/runtime/bridge-service.js +407 -17
- package/runtime/dist/src/runtime/task-branch-store.js +1 -1
- package/runtime/dist/src/runtime/worktree-policy.js +37 -0
- package/runtime/dist/src/runtime/worktree-pool.js +847 -0
- package/runtime/dist/src/utilities/local-mutex.js +37 -0
- package/skill/SKILL.md +1 -1
- package/skill/references/lifecycle.md +14 -6
- package/skill/references/project-onboarding.md +4 -0
- package/skill/references/questionnaires.md +11 -1
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import * as z from 'zod/v4';
|
|
2
|
+
import { branchChoices } from '../runtime/branch-preferences.js';
|
|
3
|
+
import { sha256, stableStringify } from '../utilities/hash.js';
|
|
4
|
+
import { askQuestionnaire } from './questionnaire-tools.js';
|
|
5
|
+
const repo = z.string().min(1);
|
|
6
|
+
const branch = z.string().min(1).max(255);
|
|
7
|
+
const remote = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/);
|
|
8
|
+
const output = (r) => ({
|
|
9
|
+
content: [{ type: 'text', text: JSON.stringify(r) }],
|
|
10
|
+
...(!r.ok ? { isError: true } : {}),
|
|
11
|
+
});
|
|
12
|
+
const json = (value) => JSON.parse(JSON.stringify(value));
|
|
13
|
+
const preference = z.strictObject({
|
|
14
|
+
answered: z.boolean(),
|
|
15
|
+
branch: branch.nullable(),
|
|
16
|
+
remote: remote.nullable(),
|
|
17
|
+
});
|
|
18
|
+
const policy = {
|
|
19
|
+
expectedVersion: z.number().int().min(1),
|
|
20
|
+
maxWorktrees: z.number().int().min(1).max(500),
|
|
21
|
+
heartbeatSeconds: z.number().int().min(5).max(300),
|
|
22
|
+
inactivitySeconds: z.number().int().min(60).max(86400),
|
|
23
|
+
};
|
|
24
|
+
const baseSchema = z.discriminatedUnion('kind', [
|
|
25
|
+
z.strictObject({
|
|
26
|
+
kind: z.literal('current'),
|
|
27
|
+
commit: z
|
|
28
|
+
.string()
|
|
29
|
+
.regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/)
|
|
30
|
+
.optional(),
|
|
31
|
+
}),
|
|
32
|
+
z.strictObject({ kind: z.enum(['development', 'production', 'test']) }),
|
|
33
|
+
z.strictObject({ kind: z.literal('remote'), remote, branch }),
|
|
34
|
+
z.strictObject({ kind: z.literal('local'), ref: branch }),
|
|
35
|
+
]);
|
|
36
|
+
export function registerWorktreeTools(server, service) {
|
|
37
|
+
server.registerTool('project.git_preferences', {
|
|
38
|
+
description: 'Read the project development/production/test branch preferences and which questions remain unanswered. Only canManage actors may change shared choices; other members may select a task-specific base.',
|
|
39
|
+
inputSchema: z.strictObject({
|
|
40
|
+
repoRoot: repo.optional(),
|
|
41
|
+
projectId: z.string().uuid().optional(),
|
|
42
|
+
}),
|
|
43
|
+
}, async (input) => output(await service.projectGitPreferences(input)));
|
|
44
|
+
server.registerTool('project.set_git_preferences', {
|
|
45
|
+
description: 'Confirm each supplied shared branch choice with a separate durable native questionnaire, then save against expectedVersion. Explicitly absent production/test branches stay answered. Reconsider a deferred choice 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
|
+
inputSchema: z.strictObject({
|
|
47
|
+
repoRoot: repo,
|
|
48
|
+
projectId: z.string().uuid(),
|
|
49
|
+
expectedVersion: z.number().int().min(0),
|
|
50
|
+
decisionAttempt: z.number().int().min(1).max(10000).optional(),
|
|
51
|
+
development: preference.optional(),
|
|
52
|
+
production: preference.optional(),
|
|
53
|
+
test: preference.optional(),
|
|
54
|
+
language: z.enum(['tr', 'en']).default('en'),
|
|
55
|
+
}),
|
|
56
|
+
}, async (input, context) => {
|
|
57
|
+
const current = await service.projectGitPreferences(input);
|
|
58
|
+
if (!current.ok)
|
|
59
|
+
return output(current);
|
|
60
|
+
const prefs = current.data;
|
|
61
|
+
if (!prefs.canManage)
|
|
62
|
+
return output({
|
|
63
|
+
ok: false,
|
|
64
|
+
error: {
|
|
65
|
+
kind: 'bridge_error',
|
|
66
|
+
message: 'Choose a base for your own task with task.branch. Shared preferences require a project or organization administrator.',
|
|
67
|
+
retryable: false,
|
|
68
|
+
recovery: 'task.branch',
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
const changes = { expectedVersion: input.expectedVersion };
|
|
72
|
+
for (const role of ['development', 'production', 'test']) {
|
|
73
|
+
const value = input[role];
|
|
74
|
+
if (!value)
|
|
75
|
+
continue;
|
|
76
|
+
const digest = sha256(stableStringify({
|
|
77
|
+
projectId: input.projectId,
|
|
78
|
+
version: input.expectedVersion,
|
|
79
|
+
role,
|
|
80
|
+
value,
|
|
81
|
+
...(input.decisionAttempt ? { decisionAttempt: input.decisionAttempt } : {}),
|
|
82
|
+
}));
|
|
83
|
+
const question = {
|
|
84
|
+
repoRoot: input.repoRoot,
|
|
85
|
+
questionnaireId: 'project-branch-' + digest,
|
|
86
|
+
message: input.language === 'tr'
|
|
87
|
+
? role +
|
|
88
|
+
' dal tercihi: ' +
|
|
89
|
+
(value.branch ? value.remote + '/' + value.branch : 'henüz yok') +
|
|
90
|
+
'. Ortak proje ayarını kaydedelim mi?'
|
|
91
|
+
: 'Save the shared ' +
|
|
92
|
+
role +
|
|
93
|
+
' branch preference: ' +
|
|
94
|
+
(value.branch ? value.remote + '/' + value.branch : 'none yet') +
|
|
95
|
+
'?',
|
|
96
|
+
language: input.language,
|
|
97
|
+
allowFreeText: false,
|
|
98
|
+
options: [
|
|
99
|
+
{
|
|
100
|
+
id: 'approve',
|
|
101
|
+
label: input.language === 'tr' ? 'Bu tercihi kaydet' : 'Save this choice',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'defer',
|
|
105
|
+
label: input.language === 'tr' ? 'Şimdilik değiştirme' : 'Leave unchanged',
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
const form = await askQuestionnaire(server, service, question, context);
|
|
110
|
+
const record = await service.questionnaireResume({
|
|
111
|
+
repoRoot: input.repoRoot,
|
|
112
|
+
questionnaireId: question.questionnaireId,
|
|
113
|
+
});
|
|
114
|
+
if (record.status !== 'answered' || record.answer?.choice !== 'approve')
|
|
115
|
+
return form;
|
|
116
|
+
changes[role] = json(value);
|
|
117
|
+
}
|
|
118
|
+
if (Object.keys(changes).length === 1)
|
|
119
|
+
return output({ ok: true, data: { unchanged: true } });
|
|
120
|
+
return output(await service.setProjectGitPreferences(input.projectId, changes));
|
|
121
|
+
});
|
|
122
|
+
server.registerTool('worktree.policy', {
|
|
123
|
+
description: 'Read and cache the backend worktree policy for this client. No environment variable controls the pool limit.',
|
|
124
|
+
inputSchema: z.strictObject({}),
|
|
125
|
+
}, async () => output(await service.worktreePolicy()));
|
|
126
|
+
server.registerTool('worktree.set_policy', {
|
|
127
|
+
description: 'Update the versioned product worktree policy. Only a global administrator may change these values.',
|
|
128
|
+
inputSchema: z.strictObject(policy),
|
|
129
|
+
}, async (input) => output(await service.worktreePolicy(json(input))));
|
|
130
|
+
server.registerTool('task.branch', {
|
|
131
|
+
description: 'Choose the task base with durable native forms and allocate a separate managed worktree for every new branch. Configured remote branches are fetched and the exact commit is pinned. Retry/resume preserves the allocation. For an explicitly reconsidered deferred answer supply a new decisionAttempt; retries keep the same attempt. Use the returned repoRoot for ALL commands; keepCurrent is allowed only in a free checkout. A read-only task allocates only when transitioning to write.',
|
|
132
|
+
inputSchema: z.strictObject({
|
|
133
|
+
repoRoot: repo,
|
|
134
|
+
externalTaskId: z.string().min(2).max(160),
|
|
135
|
+
decisionAttempt: z.number().int().min(1).max(10000).optional(),
|
|
136
|
+
name: branch.optional(),
|
|
137
|
+
base: baseSchema.optional(),
|
|
138
|
+
keepCurrent: z.boolean().optional(),
|
|
139
|
+
language: z.enum(['tr', 'en']).default('en'),
|
|
140
|
+
}),
|
|
141
|
+
}, async (input, context) => {
|
|
142
|
+
const existing = await service.worktreeTaskAllocation(input);
|
|
143
|
+
if (existing.ok && existing.data.allocation)
|
|
144
|
+
return output(await service.taskBranch(input));
|
|
145
|
+
if (!existing.ok)
|
|
146
|
+
return output(existing);
|
|
147
|
+
const current = await service.projectGitPreferences(input);
|
|
148
|
+
if (!current.ok)
|
|
149
|
+
return output(current);
|
|
150
|
+
const prefs = current.data;
|
|
151
|
+
let base = input.base;
|
|
152
|
+
let keepCurrent = input.keepCurrent === true;
|
|
153
|
+
let name = input.name;
|
|
154
|
+
const taskKey = sha256(input.externalTaskId + (input.decisionAttempt ? ':' + input.decisionAttempt : ''));
|
|
155
|
+
if (!base && !keepCurrent) {
|
|
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,
|
|
170
|
+
});
|
|
171
|
+
if (record.status !== 'answered')
|
|
172
|
+
return form;
|
|
173
|
+
const choice = record.answer?.choice;
|
|
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({
|
|
224
|
+
externalTaskId: input.externalTaskId,
|
|
225
|
+
base,
|
|
226
|
+
name: name ?? null,
|
|
227
|
+
keepCurrent,
|
|
228
|
+
...(input.decisionAttempt ? { decisionAttempt: input.decisionAttempt } : {}),
|
|
229
|
+
}));
|
|
230
|
+
const sourceLabel = base.kind === 'remote'
|
|
231
|
+
? base.remote + '/' + base.branch
|
|
232
|
+
: base.kind === 'local'
|
|
233
|
+
? base.ref
|
|
234
|
+
: base.kind === 'current'
|
|
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
|
+
],
|
|
253
|
+
};
|
|
254
|
+
const form = await askQuestionnaire(server, service, definition, context);
|
|
255
|
+
const record = await service.questionnaireResume({
|
|
256
|
+
repoRoot: input.repoRoot,
|
|
257
|
+
questionnaireId: definition.questionnaireId,
|
|
258
|
+
});
|
|
259
|
+
if (record.status !== 'answered' || record.answer?.choice !== 'approve')
|
|
260
|
+
return form;
|
|
261
|
+
const fallbackBase = { kind: 'current', commit: prefs.currentCommit ?? undefined };
|
|
262
|
+
const fallbackSignature = sha256(stableStringify({
|
|
263
|
+
externalTaskId: input.externalTaskId,
|
|
264
|
+
base: fallbackBase,
|
|
265
|
+
name: name ?? null,
|
|
266
|
+
keepCurrent,
|
|
267
|
+
...(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
|
+
};
|
|
287
|
+
const fallback = async () => {
|
|
288
|
+
const form = await askQuestionnaire(server, service, fallbackDefinition, context);
|
|
289
|
+
const record = await service.questionnaireResume({
|
|
290
|
+
repoRoot: input.repoRoot,
|
|
291
|
+
questionnaireId: fallbackDefinition.questionnaireId,
|
|
292
|
+
});
|
|
293
|
+
if (record.status !== 'answered' || record.answer?.choice !== 'approve')
|
|
294
|
+
return form;
|
|
295
|
+
return output(await service.taskBranch({
|
|
296
|
+
repoRoot: input.repoRoot,
|
|
297
|
+
externalTaskId: input.externalTaskId,
|
|
298
|
+
name,
|
|
299
|
+
keepCurrent,
|
|
300
|
+
base: fallbackBase,
|
|
301
|
+
decisionId: fallbackDefinition.questionnaireId,
|
|
302
|
+
decisionAttempt: input.decisionAttempt,
|
|
303
|
+
}));
|
|
304
|
+
};
|
|
305
|
+
if (base.kind === 'remote') {
|
|
306
|
+
let recorded = false;
|
|
307
|
+
try {
|
|
308
|
+
await service.questionnaireResume({
|
|
309
|
+
repoRoot: input.repoRoot,
|
|
310
|
+
questionnaireId: fallbackDefinition.questionnaireId,
|
|
311
|
+
});
|
|
312
|
+
recorded = true;
|
|
313
|
+
}
|
|
314
|
+
catch { }
|
|
315
|
+
if (recorded)
|
|
316
|
+
return fallback();
|
|
317
|
+
}
|
|
318
|
+
const attempt = await service.taskBranch({
|
|
319
|
+
repoRoot: input.repoRoot,
|
|
320
|
+
externalTaskId: input.externalTaskId,
|
|
321
|
+
name,
|
|
322
|
+
keepCurrent,
|
|
323
|
+
base,
|
|
324
|
+
decisionId: definition.questionnaireId,
|
|
325
|
+
decisionAttempt: input.decisionAttempt,
|
|
326
|
+
});
|
|
327
|
+
if (base.kind === 'remote' &&
|
|
328
|
+
!attempt.ok &&
|
|
329
|
+
attempt.error?.message.startsWith('The selected remote branch could not be fetched.'))
|
|
330
|
+
return fallback();
|
|
331
|
+
return output(attempt);
|
|
332
|
+
});
|
|
333
|
+
const locationSchema = z.strictObject({ repoRoot: repo });
|
|
334
|
+
const ownershipSchema = z.strictObject({
|
|
335
|
+
repoRoot: repo,
|
|
336
|
+
externalTaskId: z.string().min(2).max(160),
|
|
337
|
+
generation: z.string().uuid(),
|
|
338
|
+
delivered: z.boolean().optional(),
|
|
339
|
+
});
|
|
340
|
+
server.registerTool('worktree.list', {
|
|
341
|
+
description: 'List task checkouts, activity and reasons each directory is protected.',
|
|
342
|
+
inputSchema: locationSchema,
|
|
343
|
+
}, async (input) => output(await service.worktreeOperation('list', input)));
|
|
344
|
+
server.registerTool('worktree.reconcile', {
|
|
345
|
+
description: 'Reconcile Git with the local registry and recover interrupted allocations or a validated backup, without deleting files or resetting branches.',
|
|
346
|
+
inputSchema: locationSchema,
|
|
347
|
+
}, async (input) => output(await service.worktreeOperation('reconcile', input)));
|
|
348
|
+
server.registerTool('task.heartbeat', {
|
|
349
|
+
description: 'Renew this exact task ownership during real activity. Never run an idle timer to keep an abandoned task active.',
|
|
350
|
+
inputSchema: ownershipSchema,
|
|
351
|
+
}, async (input) => output(await service.worktreeOperation('heartbeat', input)));
|
|
352
|
+
server.registerTool('task.pause', {
|
|
353
|
+
description: 'Pause without abandoning or deleting work. Stop writing until session.resume returns the new ownership generation.',
|
|
354
|
+
inputSchema: ownershipSchema,
|
|
355
|
+
}, async (input) => output(await service.worktreeOperation('pause', input)));
|
|
356
|
+
server.registerTool('worktree.release', {
|
|
357
|
+
description: 'Safely release this generation only when the checkout is clean and authorized delivery is complete. Never force cleanup.',
|
|
358
|
+
inputSchema: ownershipSchema,
|
|
359
|
+
}, async (input) => output(await service.worktreeOperation('release', input)));
|
|
360
|
+
}
|
|
361
|
+
async function askText(server, service, input, context, purpose, message) {
|
|
362
|
+
const definition = {
|
|
363
|
+
repoRoot: input.repoRoot,
|
|
364
|
+
questionnaireId: purpose +
|
|
365
|
+
'-' +
|
|
366
|
+
sha256(input.externalTaskId + (input.decisionAttempt ? ':' + input.decisionAttempt : '')),
|
|
367
|
+
language: input.language,
|
|
368
|
+
message,
|
|
369
|
+
allowFreeText: false,
|
|
370
|
+
options: [
|
|
371
|
+
{ id: 'set', label: input.language === 'tr' ? 'Bu adı kullan' : 'Use this name' },
|
|
372
|
+
{ id: 'defer', label: input.language === 'tr' ? 'Şimdilik başlatma' : 'Not now' },
|
|
373
|
+
],
|
|
374
|
+
textField: {
|
|
375
|
+
requiredForChoice: 'set',
|
|
376
|
+
title: input.language === 'tr' ? 'Branch adı' : 'Branch name',
|
|
377
|
+
maxLength: 255,
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
const form = await askQuestionnaire(server, service, definition, context);
|
|
381
|
+
const record = await service.questionnaireResume({
|
|
382
|
+
repoRoot: input.repoRoot,
|
|
383
|
+
questionnaireId: definition.questionnaireId,
|
|
384
|
+
});
|
|
385
|
+
return {
|
|
386
|
+
form,
|
|
387
|
+
value: record.status === 'answered' && record.answer?.choice === 'set'
|
|
388
|
+
? record.answer.text
|
|
389
|
+
: undefined,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
//# sourceMappingURL=worktree-tools.js.map
|
|
@@ -77,7 +77,7 @@ export class ProjectIntake {
|
|
|
77
77
|
];
|
|
78
78
|
return { repoPath: root, recommendation, reasons, stacks, evidence, inventory };
|
|
79
79
|
}
|
|
80
|
-
async initialize(repoPath) {
|
|
80
|
+
async initialize(repoPath, initialBranch) {
|
|
81
81
|
const requested = resolve(repoPath);
|
|
82
82
|
await assertNoRepositoryAncestor(requested, this.runner);
|
|
83
83
|
let exists = true;
|
|
@@ -112,7 +112,14 @@ export class ProjectIntake {
|
|
|
112
112
|
}, (error) => (error.code === 'ENOENT' ? false : Promise.reject(error)));
|
|
113
113
|
if (existingGit)
|
|
114
114
|
return { repoPath: root, initialized: false };
|
|
115
|
-
|
|
115
|
+
if (initialBranch) {
|
|
116
|
+
const valid = await this.runner.run('git', ['check-ref-format', '--branch', initialBranch], {
|
|
117
|
+
cwd: root,
|
|
118
|
+
});
|
|
119
|
+
if (valid.exitCode !== 0 || valid.stdout.trim() !== initialBranch)
|
|
120
|
+
throw new Error('Select a valid literal initial development branch with project.initialize');
|
|
121
|
+
}
|
|
122
|
+
const result = await this.runner.run('git', ['init', '--quiet', ...(initialBranch ? ['--initial-branch=' + initialBranch] : []), '--'], { cwd: root });
|
|
116
123
|
if (result.exitCode !== 0)
|
|
117
124
|
throw new Error(`Git initialization failed: ${result.stderr.trim()}`);
|
|
118
125
|
return { repoPath: root, initialized: true };
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { sha256, stableStringify } from '../utilities/hash.js';
|
|
2
2
|
export const backendRecoveryOperationNames = [
|
|
3
|
+
'project.git_preferences',
|
|
4
|
+
'project.set_git_preferences',
|
|
5
|
+
'worktree.policy',
|
|
6
|
+
'worktree.set_policy',
|
|
3
7
|
'auth.signin_browser',
|
|
4
8
|
'auth.web_csrf',
|
|
5
9
|
'auth.web_signin',
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { sha256, stableStringify } from '../utilities/hash.js';
|
|
2
|
+
import { BridgeRecoveryError } from './recovery-error.js';
|
|
3
|
+
export function branchDecisionId(externalTaskId, base, name, keepCurrent) {
|
|
4
|
+
return ('task-base-' +
|
|
5
|
+
sha256(stableStringify({ externalTaskId, base, name: name ?? null, keepCurrent })));
|
|
6
|
+
}
|
|
7
|
+
export function branchChoices(preferences, current) {
|
|
8
|
+
const options = [];
|
|
9
|
+
for (const role of ['development', 'production', 'test']) {
|
|
10
|
+
const choice = preferences.branches[role];
|
|
11
|
+
if (choice.answered && choice.branch)
|
|
12
|
+
options.push({ id: role, label: role + ': ' + choice.remote + '/' + choice.branch });
|
|
13
|
+
}
|
|
14
|
+
options.splice(Math.min(1, options.length), 0, {
|
|
15
|
+
id: 'current',
|
|
16
|
+
label: 'Current branch: ' + (current ?? 'detached HEAD'),
|
|
17
|
+
});
|
|
18
|
+
options.push({ id: 'other', label: 'Another branch' }, { id: 'keep', label: 'Continue on this branch without creating a new branch' });
|
|
19
|
+
return options;
|
|
20
|
+
}
|
|
21
|
+
export async function resolveTaskBase(git, repoRoot, base, preferences) {
|
|
22
|
+
if (base.kind === 'current' || base.kind === 'local') {
|
|
23
|
+
const source = await git.sourceIdentity(repoRoot, base.kind === 'current' ? (base.commit ?? 'HEAD') : base.ref);
|
|
24
|
+
if (!source && base.kind === 'current' && !base.commit)
|
|
25
|
+
return { sourceCommit: null, base };
|
|
26
|
+
if (!source)
|
|
27
|
+
throw new BridgeRecoveryError('The selected local source has no commit. Choose a valid base or finish initial project setup.', 'task.branch');
|
|
28
|
+
return { ...source, base };
|
|
29
|
+
}
|
|
30
|
+
let remote;
|
|
31
|
+
let branch;
|
|
32
|
+
if (base.kind === 'remote') {
|
|
33
|
+
remote = base.remote;
|
|
34
|
+
branch = base.branch;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const choice = preferences.branches[base.kind];
|
|
38
|
+
if (!choice.answered || !choice.branch || !choice.remote)
|
|
39
|
+
throw new BridgeRecoveryError('That project branch has not been configured. Select a task-specific base through task.branch.', 'task.branch');
|
|
40
|
+
remote = choice.remote;
|
|
41
|
+
branch = choice.branch;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
return { ...(await git.fetchBranchCommit(repoRoot, remote, branch)), base, remote, branch };
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new BridgeRecoveryError('The selected remote branch could not be fetched. Retry task.branch, or ask a new native question to select an explicit local ref. No cached commit was selected.', 'task.branch');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function preferencesJson(preferences) {
|
|
51
|
+
return JSON.parse(JSON.stringify(preferences));
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=branch-preferences.js.map
|