engineering-memory 1.11.11 → 1.11.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dispatcher/sections.mjs +2 -2
- package/package.json +1 -1
- package/runtime/build.json +1 -1
- package/runtime/dist/src/mcp/onboarding-tools.js +833 -154
- package/runtime/dist/src/mcp/questionnaire-tools.js +55 -16
- package/runtime/dist/src/mcp/tool-definitions.js +107 -24
- package/runtime/dist/src/mcp/worktree-tools.js +349 -91
- package/runtime/dist/src/runtime/bridge-service.js +127 -19
- package/runtime/dist/src/runtime/create-bridge-service.js +2 -0
- package/runtime/dist/src/runtime/language-store.js +25 -0
- package/runtime/dist/src/runtime/offline-outbox.js +4 -4
- package/runtime/dist/src/runtime/privacy-detector.js +14 -5
- package/runtime/dist/src/runtime/questionnaire-store.js +26 -7
- package/runtime/dist/src/runtime/task-start.js +115 -33
- package/skill/references/questionnaires.md +8 -3
|
@@ -1,4 +1,44 @@
|
|
|
1
1
|
import { sha256, stableStringify } from '../utilities/hash.js';
|
|
2
|
+
const startCopy = {
|
|
3
|
+
en: {
|
|
4
|
+
start: (task) => `${task}: how should this task start?`,
|
|
5
|
+
context: 'A task works on its own branch so that unfinished work never mixes with what is already open in this folder. Nothing is created, checked out or reserved until you answer. Your answer is kept, so asking for the same task again does not ask you twice.',
|
|
6
|
+
example: 'Choosing a separate worktree creates the branch in a folder of its own and leaves this folder on the branch and the files you have open right now.',
|
|
7
|
+
keepContext: 'This task continues on the branch this folder is already on. Nothing is created, checked out or reserved until you answer. Your answer is kept, so asking for the same task again does not ask you twice.',
|
|
8
|
+
roles: { development: 'development', production: 'production', test: 'test' },
|
|
9
|
+
holder: (task, branch, when) => `${task} holds this folder (branch ${branch}, last activity ${when}). If it moves out it stays open, keeps its own branch and continues in a separate worktree when it is resumed.`,
|
|
10
|
+
unknown: 'unknown',
|
|
11
|
+
separate: 'A managed folder of its own is prepared and the new branch is created there. This folder is left on its branch and its files are not touched.',
|
|
12
|
+
inPlace: 'The new branch is created here and this folder is switched to it.',
|
|
13
|
+
moveOut: (task) => `${task} keeps its branch and its work and continues in a separate worktree later. The new branch is checked out here.`,
|
|
14
|
+
stay: (source) => `Nothing new is created. The work continues on ${source} in this folder, so every commit lands on that branch.`,
|
|
15
|
+
later: 'Nothing is created or reserved. The task waits until you ask to start it.',
|
|
16
|
+
fetched: (role, source) => `The project's ${role} branch ${source} is fetched from the remote and the new branch starts from that exact commit.`,
|
|
17
|
+
pinned: (source) => `The new branch starts where this folder is right now, ${source}. Nothing is fetched.`,
|
|
18
|
+
typedBranch: 'Write a branch name on origin below. It is fetched and the new branch starts from it.',
|
|
19
|
+
suggested: 'The branch is created with this name.',
|
|
20
|
+
typedName: 'Write the name below. A name that already exists is refused and the form is asked again.',
|
|
21
|
+
},
|
|
22
|
+
tr: {
|
|
23
|
+
start: (task) => `${task} nasıl başlasın?`,
|
|
24
|
+
context: 'Her görev kendi dalında çalışır; böylece yarım kalan iş, bu klasörde açık olan işe karışmaz. Sen cevaplayana kadar hiçbir klasör açılmaz, hiçbir dal oluşturulmaz, hiçbir yer ayrılmaz. Verdiğin cevap saklanır; aynı görev için ikinci kez sorulmaz.',
|
|
25
|
+
example: 'Ayrı worktree’yi seçersen yeni dal kendi klasöründe açılır; bu klasör şu anki dalında ve açık dosyalarıyla olduğu gibi kalır.',
|
|
26
|
+
keepContext: 'Bu görev, klasörün zaten üzerinde olduğu dalda devam eder. Sen cevaplayana kadar hiçbir şey oluşturulmaz, hiçbir yer ayrılmaz. Verdiğin cevap saklanır; aynı görev için yeniden sorulmaz.',
|
|
27
|
+
roles: { development: 'geliştirme', production: 'canlı', test: 'test' },
|
|
28
|
+
holder: (task, branch, when) => `${task} görevi bu klasörü tutuyor (dal ${branch}, son etkinlik ${when}). Buradan çıkarılırsa görev kapanmaz; kendi dalını ve işini korur, devam ettiğinde ayrı bir worktree’de açılır.`,
|
|
29
|
+
unknown: 'bilinmiyor',
|
|
30
|
+
separate: 'Görev için ayrı bir klasör hazırlanır ve yeni dal orada açılır. Bu klasör kendi dalında kalır, dosyalarına dokunulmaz.',
|
|
31
|
+
inPlace: 'Yeni dal burada açılır ve bu klasör o dala geçer.',
|
|
32
|
+
moveOut: (task) => `${task} dalını ve işini korur, sonra ayrı bir worktree’de devam eder. Yeni dal burada açılır.`,
|
|
33
|
+
stay: (source) => `Yeni bir şey oluşturulmaz. İş bu klasörde ${source} üzerinde sürer; her commit o dala gider.`,
|
|
34
|
+
later: 'Hiçbir şey oluşturulmaz, hiçbir yer ayrılmaz. Görev sen isteyene kadar beklemede kalır.',
|
|
35
|
+
fetched: (role, source) => `Projenin ${role} dalı ${source} uzak sunucudan çekilir ve yeni dal tam o commit’ten başlar.`,
|
|
36
|
+
pinned: (source) => `Yeni dal, bu klasörün şu an üzerinde olduğu ${source} noktasından başlar. Hiçbir şey çekilmez.`,
|
|
37
|
+
typedBranch: 'origin üzerindeki dal adını aşağıya yaz. O dal çekilir ve yeni dal ondan başlar.',
|
|
38
|
+
suggested: 'Dal bu adla açılır.',
|
|
39
|
+
typedName: 'Adı aşağıya yaz. Zaten var olan bir ad kabul edilmez, form yeniden sorulur.',
|
|
40
|
+
},
|
|
41
|
+
};
|
|
2
42
|
export function suggestedBranchNames(externalTaskId) {
|
|
3
43
|
const slug = externalTaskId
|
|
4
44
|
.replace(/[^A-Za-z0-9._-]+/g, '-')
|
|
@@ -34,6 +74,7 @@ export function configuredBase(preferences, role) {
|
|
|
34
74
|
}
|
|
35
75
|
export function taskStartDefinition(facts) {
|
|
36
76
|
const tr = facts.language === 'tr';
|
|
77
|
+
const copy = startCopy[tr ? 'tr' : 'en'];
|
|
37
78
|
const folder = facts.folder;
|
|
38
79
|
const keepOnly = facts.keepCurrent === true || !facts.currentCommit;
|
|
39
80
|
const here = !keepOnly && !folder.managed && !folder.heldBy && folder.clean;
|
|
@@ -46,14 +87,26 @@ export function taskStartDefinition(facts) {
|
|
|
46
87
|
currentCommit: facts.currentCommit,
|
|
47
88
|
holder,
|
|
48
89
|
};
|
|
90
|
+
const heldNotice = here && folder.holder
|
|
91
|
+
? copy.holder(folder.holder.externalTaskId, folder.holder.branch ?? 'detached HEAD', readableTime(folder.holder.lastCheckpointAt, tr) ?? copy.unknown)
|
|
92
|
+
: null;
|
|
93
|
+
const withheldNotice = withheld(folder, keepOnly, tr);
|
|
94
|
+
const locationContext = [heldNotice, withheldNotice].filter(Boolean).join(' ');
|
|
49
95
|
const questions = [
|
|
50
96
|
{
|
|
51
97
|
id: 'location',
|
|
52
98
|
message: tr ? 'Nerede başlasın?' : 'Where should it start?',
|
|
99
|
+
...(locationContext ? { context: locationContext } : {}),
|
|
53
100
|
options: [
|
|
54
101
|
...(keepOnly
|
|
55
102
|
? []
|
|
56
|
-
: [
|
|
103
|
+
: [
|
|
104
|
+
{
|
|
105
|
+
id: 'worktree',
|
|
106
|
+
label: tr ? 'Ayrı bir worktree’de' : 'In a separate worktree',
|
|
107
|
+
description: copy.separate,
|
|
108
|
+
},
|
|
109
|
+
]),
|
|
57
110
|
...(here
|
|
58
111
|
? [
|
|
59
112
|
{
|
|
@@ -65,6 +118,7 @@ export function taskStartDefinition(facts) {
|
|
|
65
118
|
: tr
|
|
66
119
|
? 'Bu klasörde yeni branch olarak'
|
|
67
120
|
: 'In this folder, as a new branch',
|
|
121
|
+
description: holder ? copy.moveOut(holder.externalTaskId) : copy.inPlace,
|
|
68
122
|
},
|
|
69
123
|
]
|
|
70
124
|
: []),
|
|
@@ -75,10 +129,11 @@ export function taskStartDefinition(facts) {
|
|
|
75
129
|
label: tr
|
|
76
130
|
? 'Burada, ' + currentLabel + ' üzerinde, yeni branch açmadan'
|
|
77
131
|
: 'Here on ' + currentLabel + ', without a new branch',
|
|
132
|
+
description: copy.stay(currentLabel),
|
|
78
133
|
},
|
|
79
134
|
]
|
|
80
135
|
: []),
|
|
81
|
-
{ id: 'defer', label: tr ? 'Şimdilik başlatma' : 'Not now' },
|
|
136
|
+
{ id: 'defer', label: tr ? 'Şimdilik başlatma' : 'Not now', description: copy.later },
|
|
82
137
|
],
|
|
83
138
|
},
|
|
84
139
|
];
|
|
@@ -92,6 +147,7 @@ export function taskStartDefinition(facts) {
|
|
|
92
147
|
const roleOption = ({ role, base }) => ({
|
|
93
148
|
id: role,
|
|
94
149
|
label: base.remote + '/' + base.branch + ' (' + role + ')',
|
|
150
|
+
description: copy.fetched(copy.roles[role], base.remote + '/' + base.branch),
|
|
95
151
|
});
|
|
96
152
|
binding.bases = {
|
|
97
153
|
current: {
|
|
@@ -108,9 +164,14 @@ export function taskStartDefinition(facts) {
|
|
|
108
164
|
{
|
|
109
165
|
id: 'current',
|
|
110
166
|
label: tr ? 'Bu klasörün dalı: ' + currentLabel : 'This folder’s branch: ' + currentLabel,
|
|
167
|
+
description: copy.pinned(currentLabel),
|
|
111
168
|
},
|
|
112
169
|
...roles.slice(1).map(roleOption),
|
|
113
|
-
{
|
|
170
|
+
{
|
|
171
|
+
id: 'other',
|
|
172
|
+
label: tr ? 'Başka bir uzak dal' : 'Another remote branch',
|
|
173
|
+
description: copy.typedBranch,
|
|
174
|
+
},
|
|
114
175
|
],
|
|
115
176
|
otherAsText: true,
|
|
116
177
|
textField: {
|
|
@@ -128,8 +189,8 @@ export function taskStartDefinition(facts) {
|
|
|
128
189
|
id: 'name',
|
|
129
190
|
message: tr ? 'Yeni branch’in adı ne olsun?' : 'What should the new branch be called?',
|
|
130
191
|
options: [
|
|
131
|
-
{ id: 'suggested', label: facts.suggestedName },
|
|
132
|
-
{ id: 'custom', label: tr ? 'Başka bir ad' : 'Another name' },
|
|
192
|
+
{ id: 'suggested', label: facts.suggestedName, description: copy.suggested },
|
|
193
|
+
{ id: 'custom', label: tr ? 'Başka bir ad' : 'Another name', description: copy.typedName },
|
|
133
194
|
],
|
|
134
195
|
textField: {
|
|
135
196
|
title: tr ? 'Branch adı' : 'Branch name',
|
|
@@ -140,24 +201,7 @@ export function taskStartDefinition(facts) {
|
|
|
140
201
|
}
|
|
141
202
|
else if (!keepOnly)
|
|
142
203
|
binding.name = facts.name;
|
|
143
|
-
const
|
|
144
|
-
...(here && folder.holder
|
|
145
|
-
? [
|
|
146
|
-
tr
|
|
147
|
-
? folder.holder.externalTaskId +
|
|
148
|
-
' bu klasörü tutuyor (branch ' +
|
|
149
|
-
(folder.holder.branch ?? 'detached HEAD') +
|
|
150
|
-
', son etkinlik ' +
|
|
151
|
-
(folder.holder.lastCheckpointAt ?? 'bilinmiyor') +
|
|
152
|
-
'). Çıkarılırsa görev açık kalır ve devam ederken ayrı bir worktree’de açılır.'
|
|
153
|
-
: folder.holder.externalTaskId +
|
|
154
|
-
' holds this folder (branch ' +
|
|
155
|
-
(folder.holder.branch ?? 'detached HEAD') +
|
|
156
|
-
', last activity ' +
|
|
157
|
-
(folder.holder.lastCheckpointAt ?? 'unknown') +
|
|
158
|
-
'). If it moves out, it stays open and continues in a separate worktree when resumed.',
|
|
159
|
-
]
|
|
160
|
-
: []),
|
|
204
|
+
const chosen = [
|
|
161
205
|
...(binding.base
|
|
162
206
|
? [
|
|
163
207
|
tr
|
|
@@ -168,25 +212,57 @@ export function taskStartDefinition(facts) {
|
|
|
168
212
|
...(binding.name
|
|
169
213
|
? [tr ? 'Yeni branch: ' + binding.name + '.' : 'New branch: ' + binding.name + '.']
|
|
170
214
|
: []),
|
|
171
|
-
...(withheld(folder, keepOnly, tr) ? [withheld(folder, keepOnly, tr)] : []),
|
|
172
215
|
];
|
|
173
|
-
const
|
|
216
|
+
const shared = {
|
|
217
|
+
language: facts.language,
|
|
218
|
+
terminalChoices: [
|
|
219
|
+
{ questionId: 'location', choice: 'defer' },
|
|
220
|
+
...(keep ? [{ questionId: 'location', choice: 'keep_current' }] : []),
|
|
221
|
+
],
|
|
222
|
+
binding: JSON.parse(JSON.stringify(binding)),
|
|
223
|
+
};
|
|
224
|
+
const previous = {
|
|
174
225
|
message: [
|
|
175
226
|
tr
|
|
176
227
|
? facts.externalTaskId + ' nasıl başlasın? Sen cevaplayana kadar hiçbir şey oluşturulmaz.'
|
|
177
228
|
: facts.externalTaskId +
|
|
178
229
|
': how should this task start? Nothing is created until you answer.',
|
|
179
|
-
...
|
|
230
|
+
...(here && folder.holder
|
|
231
|
+
? [
|
|
232
|
+
tr
|
|
233
|
+
? folder.holder.externalTaskId +
|
|
234
|
+
' bu klasörü tutuyor (branch ' +
|
|
235
|
+
(folder.holder.branch ?? 'detached HEAD') +
|
|
236
|
+
', son etkinlik ' +
|
|
237
|
+
(folder.holder.lastCheckpointAt ?? 'bilinmiyor') +
|
|
238
|
+
'). Çıkarılırsa görev açık kalır ve devam ederken ayrı bir worktree’de açılır.'
|
|
239
|
+
: folder.holder.externalTaskId +
|
|
240
|
+
' holds this folder (branch ' +
|
|
241
|
+
(folder.holder.branch ?? 'detached HEAD') +
|
|
242
|
+
', last activity ' +
|
|
243
|
+
(folder.holder.lastCheckpointAt ?? 'unknown') +
|
|
244
|
+
'). If it moves out, it stays open and continues in a separate worktree when resumed.',
|
|
245
|
+
]
|
|
246
|
+
: []),
|
|
247
|
+
...chosen,
|
|
248
|
+
...(withheldNotice ? [withheldNotice] : []),
|
|
180
249
|
].join(' '),
|
|
181
|
-
|
|
250
|
+
...shared,
|
|
251
|
+
questions: questions.map(({ context: _why, options, ...question }) => ({
|
|
252
|
+
...question,
|
|
253
|
+
options: options.map(({ description: _next, ...option }) => option),
|
|
254
|
+
})),
|
|
255
|
+
};
|
|
256
|
+
const questionnaireId = 'task-start-' + sha256(stableStringify(previous));
|
|
257
|
+
return {
|
|
258
|
+
questionnaireId,
|
|
259
|
+
message: [copy.start(facts.externalTaskId), ...chosen].join(' '),
|
|
260
|
+
context: keepOnly ? copy.keepContext : copy.context,
|
|
261
|
+
example: keepOnly ? copy.stay(currentLabel) : copy.example,
|
|
262
|
+
...shared,
|
|
182
263
|
questions,
|
|
183
|
-
|
|
184
|
-
{ questionId: 'location', choice: 'defer' },
|
|
185
|
-
...(keep ? [{ questionId: 'location', choice: 'keep_current' }] : []),
|
|
186
|
-
],
|
|
187
|
-
binding: JSON.parse(JSON.stringify(binding)),
|
|
264
|
+
previous: { questionnaireId, ...previous },
|
|
188
265
|
};
|
|
189
|
-
return { questionnaireId: 'task-start-' + sha256(stableStringify(content)), ...content };
|
|
190
266
|
}
|
|
191
267
|
export function taskStartChoice(record) {
|
|
192
268
|
if (record.status !== 'answered' ||
|
|
@@ -264,6 +340,12 @@ function holderIdentity(holder) {
|
|
|
264
340
|
generation: holder.generation,
|
|
265
341
|
};
|
|
266
342
|
}
|
|
343
|
+
function readableTime(value, tr) {
|
|
344
|
+
const [, date, time] = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/.exec(value ?? '') ?? [];
|
|
345
|
+
if (!date || !time)
|
|
346
|
+
return null;
|
|
347
|
+
return (tr ? date.split('-').reverse().join('.') : date) + ' ' + time + ' UTC';
|
|
348
|
+
}
|
|
267
349
|
function withheld(folder, keepOnly, tr) {
|
|
268
350
|
if (keepOnly || folder.managed || (!folder.heldBy && folder.clean))
|
|
269
351
|
return null;
|
|
@@ -6,7 +6,11 @@ Use the host's native questionnaire for every question to the user, including im
|
|
|
6
6
|
|
|
7
7
|
Use `questionnaire.ask` for a required decision. It records the question before opening a native MCP form. Use a stable question identifier belonging to the current task and decision, so a retry returns to the same question instead of creating another one. A new decision needs its own identifier; an answer to an earlier proposal, branch or delivery does not approve a later one.
|
|
8
8
|
|
|
9
|
-
Pass `repoRoot`, `questionnaireId`, `message` and two to twelve `options`, each with an `id` and `label`. Identifiers use letters, digits, underscores or hyphens.
|
|
9
|
+
Pass `repoRoot`, `questionnaireId`, `message` and two to twelve `options`, each with an `id` and `label`. Identifiers use letters, digits, underscores or hyphens.
|
|
10
|
+
|
|
11
|
+
A question has to be answerable by someone who has not followed the work. Say in `context` why it is being asked, give in `example` one concrete thing the decision changes, and give every option a `description` of what happens next if it is chosen. Show a record, a branch or a project by its title or name; an identifier or a hash in the visible text is refused, and one that really has to be shown goes inside backticks. Write all of it in the language the user is writing in and pass that language as `language`, a BCP-47 tag such as `tr`, `en` or `pt-BR`. The tag is remembered for the account and the computer, so it is asked for once: `session.entry` and every tool that opens a form take the same argument, and `questionnaire.ask` is refused while no language is known.
|
|
12
|
+
|
|
13
|
+
Every form carries one more choice, "I did not understand; explain in more detail first". It is never recorded as an answer. When a call returns `needs_explanation`, explain in chat why the question is asked, what each option leads to and one example, then show the same question again with `questionnaire.resume`. A free-text reply that says the question was not understood is handled the same way and is not passed on as the decision. Set `allowFreeText` only when the user needs to give an answer outside those options. `questionnaire.resume` takes the same `repoRoot` and `questionnaireId`. These tools collect decisions; they do not commit, publish, bind a project or perform the selected action. Apply the accepted answer through the relevant lifecycle tool, checking the current target and version first.
|
|
10
14
|
|
|
11
15
|
Fixed-choice answers can be replayed from the local receipt. Free text is returned only in the accepting call and is never stored. If a later receipt says the answer is unavailable, recover it from the user's actual answer in the conversation; never reconstruct it from the options. If it cannot be recovered, explain that it must be requested again using a new question identifier.
|
|
12
16
|
|
|
@@ -324,8 +328,9 @@ Saving a profile does not authorize filesystem initialization.
|
|
|
324
328
|
## Inspection confirmation language
|
|
325
329
|
|
|
326
330
|
For memory.sync_start, pass language: tr in a Turkish conversation and language: en in an English
|
|
327
|
-
conversation. Use the conversation language, never the computer locale.
|
|
328
|
-
|
|
331
|
+
conversation. Use the conversation language, never the computer locale. A call without language uses
|
|
332
|
+
the language remembered from an earlier call, and English when none is known. The bridge's own forms
|
|
333
|
+
have Turkish and English text; any other language opens them in English. The form shows a short source reference, scope, the treatment of uncommitted changes and
|
|
329
334
|
the separate approval needed for permanent memory. Full commit and request hashes stay in the durable
|
|
330
335
|
approval binding, not in the visible question. Generic questionnaire.ask messages and option labels
|
|
331
336
|
are written by the agent in the same language; pass language for the native help and field labels.
|