engineering-memory 1.11.8 → 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 +7 -3
- package/install/codex-approval.mjs +457 -0
- package/install/files.mjs +119 -9
- package/install/installer.mjs +44 -5
- package/install/launcher.mjs +95 -0
- package/package.json +1 -1
- package/runtime/build.json +1 -1
- package/runtime/dist/src/config.js +4 -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/index.js +25 -81
- 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/server.js +14 -7
- 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/tool-host.js +12 -0
- 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 +1163 -231
- package/runtime/dist/src/runtime/create-bridge-service.js +82 -0
- 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 +24 -0
- package/runtime/dist/src/runtime/runtime-host.js +263 -0
- 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 +340 -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
|
@@ -89,10 +89,26 @@ export class TaskBranchStore {
|
|
|
89
89
|
throw new BridgeRecoveryError('The task has finished but its worktree reservation could not be released. Resume the task to retry cleanup.', 'session.resume');
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
+
async detach(expected, oid) {
|
|
93
|
+
const current = await this.read();
|
|
94
|
+
const changed = () => new BridgeRecoveryError(`The reservation of this folder changed after the question about ${expected.externalTaskId} was asked. Call task.branch again; its question shows which task holds the folder now.`, 'task.branch');
|
|
95
|
+
if (!current ||
|
|
96
|
+
current.oid !== oid ||
|
|
97
|
+
current.decision.projectId !== expected.projectId ||
|
|
98
|
+
current.decision.externalTaskId !== expected.externalTaskId ||
|
|
99
|
+
current.decision.taskId !== expected.taskId)
|
|
100
|
+
throw changed();
|
|
101
|
+
const result = await this.runner.run('git', ['update-ref', '--no-deref', '-d', decisionRef, oid], { cwd: this.repoRoot });
|
|
102
|
+
if (result.exitCode !== 0) {
|
|
103
|
+
if ((await this.read())?.oid !== oid)
|
|
104
|
+
throw changed();
|
|
105
|
+
throw new BridgeRecoveryError('Git could not move the task out of this folder. Retry task.branch; nothing was changed.', 'task.branch');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
92
108
|
assertSame(current, requested) {
|
|
93
109
|
if (current.projectId !== requested.projectId ||
|
|
94
110
|
current.externalTaskId !== requested.externalTaskId) {
|
|
95
|
-
throw new BridgeRecoveryError(`This
|
|
111
|
+
throw new BridgeRecoveryError(`This folder is held by task ${current.externalTaskId}. Continue that task, or call task.branch for the new task: its question offers a separate worktree, or a new branch in this folder after asking whether ${current.externalTaskId} should move out.`, 'task.branch');
|
|
96
112
|
}
|
|
97
113
|
if (current.branch !== requested.branch) {
|
|
98
114
|
throw new BridgeRecoveryError(`This task selected ${current.branch ?? 'detached HEAD'}. Return to that branch before continuing.`, 'task.branch');
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { sha256, stableStringify } from '../utilities/hash.js';
|
|
2
|
+
export function suggestedBranchNames(externalTaskId) {
|
|
3
|
+
const slug = externalTaskId
|
|
4
|
+
.replace(/[^A-Za-z0-9._-]+/g, '-')
|
|
5
|
+
.replace(/\.{2,}/g, '.')
|
|
6
|
+
.replace(/-{2,}/g, '-')
|
|
7
|
+
.replace(/^[-.]+|[-.]+$/g, '')
|
|
8
|
+
.slice(0, 120)
|
|
9
|
+
.replace(/\.lock$/i, '') || 'task';
|
|
10
|
+
const name = 'feature/' + slug;
|
|
11
|
+
return [name, ...[2, 3, 4, 5, 6, 7, 8, 9].map((suffix) => name + '-' + suffix)];
|
|
12
|
+
}
|
|
13
|
+
export function sameHolder(left, right) {
|
|
14
|
+
if (!left || !right)
|
|
15
|
+
return left === right;
|
|
16
|
+
return ((left.taskId ?? left.externalTaskId) === (right.taskId ?? right.externalTaskId) &&
|
|
17
|
+
left.oid === right.oid &&
|
|
18
|
+
(left.generation ?? null) === (right.generation ?? null));
|
|
19
|
+
}
|
|
20
|
+
export function sourceLabel(base, currentBranch) {
|
|
21
|
+
if (base.kind === 'remote')
|
|
22
|
+
return base.remote + '/' + base.branch;
|
|
23
|
+
if (base.kind === 'local')
|
|
24
|
+
return base.ref;
|
|
25
|
+
if (base.kind === 'current')
|
|
26
|
+
return (currentBranch ?? 'HEAD') + (base.commit ? ' (' + base.commit.slice(0, 7) + ')' : '');
|
|
27
|
+
return base.kind;
|
|
28
|
+
}
|
|
29
|
+
export function configuredBase(preferences, role) {
|
|
30
|
+
const selected = preferences.branches[role];
|
|
31
|
+
return selected.answered && selected.remote && selected.branch
|
|
32
|
+
? { kind: 'remote', remote: selected.remote, branch: selected.branch }
|
|
33
|
+
: null;
|
|
34
|
+
}
|
|
35
|
+
export function taskStartDefinition(facts) {
|
|
36
|
+
const tr = facts.language === 'tr';
|
|
37
|
+
const folder = facts.folder;
|
|
38
|
+
const keepOnly = facts.keepCurrent === true || !facts.currentCommit;
|
|
39
|
+
const here = !keepOnly && !folder.managed && !folder.heldBy && folder.clean;
|
|
40
|
+
const keep = keepOnly || (!folder.managed && !folder.heldBy && !folder.holder);
|
|
41
|
+
const holder = here && folder.holder ? holderIdentity(folder.holder) : null;
|
|
42
|
+
const currentLabel = sourceLabel({ kind: 'current', ...(facts.currentCommit ? { commit: facts.currentCommit } : {}) }, facts.currentBranch);
|
|
43
|
+
const binding = {
|
|
44
|
+
externalTaskId: facts.externalTaskId,
|
|
45
|
+
...(facts.decisionAttempt ? { decisionAttempt: facts.decisionAttempt } : {}),
|
|
46
|
+
currentCommit: facts.currentCommit,
|
|
47
|
+
holder,
|
|
48
|
+
};
|
|
49
|
+
const questions = [
|
|
50
|
+
{
|
|
51
|
+
id: 'location',
|
|
52
|
+
message: tr ? 'Nerede başlasın?' : 'Where should it start?',
|
|
53
|
+
options: [
|
|
54
|
+
...(keepOnly
|
|
55
|
+
? []
|
|
56
|
+
: [{ id: 'worktree', label: tr ? 'Ayrı bir worktree’de' : 'In a separate worktree' }]),
|
|
57
|
+
...(here
|
|
58
|
+
? [
|
|
59
|
+
{
|
|
60
|
+
id: 'here',
|
|
61
|
+
label: holder
|
|
62
|
+
? tr
|
|
63
|
+
? holder.externalTaskId + ' bu klasörden çıkarılsın, yeni branch burada açılsın'
|
|
64
|
+
: 'Move ' + holder.externalTaskId + ' out and start the new branch here'
|
|
65
|
+
: tr
|
|
66
|
+
? 'Bu klasörde yeni branch olarak'
|
|
67
|
+
: 'In this folder, as a new branch',
|
|
68
|
+
},
|
|
69
|
+
]
|
|
70
|
+
: []),
|
|
71
|
+
...(keep
|
|
72
|
+
? [
|
|
73
|
+
{
|
|
74
|
+
id: 'keep_current',
|
|
75
|
+
label: tr
|
|
76
|
+
? 'Burada, ' + currentLabel + ' üzerinde, yeni branch açmadan'
|
|
77
|
+
: 'Here on ' + currentLabel + ', without a new branch',
|
|
78
|
+
},
|
|
79
|
+
]
|
|
80
|
+
: []),
|
|
81
|
+
{ id: 'defer', label: tr ? 'Şimdilik başlatma' : 'Not now' },
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
if (!keepOnly && !facts.base) {
|
|
86
|
+
const roles = ['development', 'production', 'test']
|
|
87
|
+
.flatMap((role) => {
|
|
88
|
+
const base = configuredBase(facts.preferences, role);
|
|
89
|
+
return base ? [{ role, base }] : [];
|
|
90
|
+
})
|
|
91
|
+
.slice(0, 2);
|
|
92
|
+
const roleOption = ({ role, base }) => ({
|
|
93
|
+
id: role,
|
|
94
|
+
label: base.remote + '/' + base.branch + ' (' + role + ')',
|
|
95
|
+
});
|
|
96
|
+
binding.bases = {
|
|
97
|
+
current: {
|
|
98
|
+
kind: 'current',
|
|
99
|
+
...(facts.currentCommit ? { commit: facts.currentCommit } : {}),
|
|
100
|
+
},
|
|
101
|
+
...Object.fromEntries(roles.map(({ role, base }) => [role, base])),
|
|
102
|
+
};
|
|
103
|
+
questions.push({
|
|
104
|
+
id: 'base',
|
|
105
|
+
message: tr ? 'Hangi daldan başlasın?' : 'Which branch should it start from?',
|
|
106
|
+
options: [
|
|
107
|
+
...roles.slice(0, 1).map(roleOption),
|
|
108
|
+
{
|
|
109
|
+
id: 'current',
|
|
110
|
+
label: tr ? 'Bu klasörün dalı: ' + currentLabel : 'This folder’s branch: ' + currentLabel,
|
|
111
|
+
},
|
|
112
|
+
...roles.slice(1).map(roleOption),
|
|
113
|
+
{ id: 'other', label: tr ? 'Başka bir uzak dal' : 'Another remote branch' },
|
|
114
|
+
],
|
|
115
|
+
otherAsText: true,
|
|
116
|
+
textField: {
|
|
117
|
+
title: tr ? 'origin üzerindeki dal adı' : 'Branch name on origin',
|
|
118
|
+
maxLength: 255,
|
|
119
|
+
requiredForChoice: 'other',
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
else if (!keepOnly)
|
|
124
|
+
binding.base = resolvedBase(facts.base, facts);
|
|
125
|
+
if (!keepOnly && !facts.name) {
|
|
126
|
+
binding.suggestedName = facts.suggestedName;
|
|
127
|
+
questions.push({
|
|
128
|
+
id: 'name',
|
|
129
|
+
message: tr ? 'Yeni branch’in adı ne olsun?' : 'What should the new branch be called?',
|
|
130
|
+
options: [
|
|
131
|
+
{ id: 'suggested', label: facts.suggestedName },
|
|
132
|
+
{ id: 'custom', label: tr ? 'Başka bir ad' : 'Another name' },
|
|
133
|
+
],
|
|
134
|
+
textField: {
|
|
135
|
+
title: tr ? 'Branch adı' : 'Branch name',
|
|
136
|
+
maxLength: 255,
|
|
137
|
+
requiredForChoice: 'custom',
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
else if (!keepOnly)
|
|
142
|
+
binding.name = facts.name;
|
|
143
|
+
const fixed = [
|
|
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
|
+
: []),
|
|
161
|
+
...(binding.base
|
|
162
|
+
? [
|
|
163
|
+
tr
|
|
164
|
+
? sourceLabel(binding.base, facts.currentBranch) + ' dalından başlar.'
|
|
165
|
+
: 'It starts from ' + sourceLabel(binding.base, facts.currentBranch) + '.',
|
|
166
|
+
]
|
|
167
|
+
: []),
|
|
168
|
+
...(binding.name
|
|
169
|
+
? [tr ? 'Yeni branch: ' + binding.name + '.' : 'New branch: ' + binding.name + '.']
|
|
170
|
+
: []),
|
|
171
|
+
...(withheld(folder, keepOnly, tr) ? [withheld(folder, keepOnly, tr)] : []),
|
|
172
|
+
];
|
|
173
|
+
const content = {
|
|
174
|
+
message: [
|
|
175
|
+
tr
|
|
176
|
+
? facts.externalTaskId + ' nasıl başlasın? Sen cevaplayana kadar hiçbir şey oluşturulmaz.'
|
|
177
|
+
: facts.externalTaskId +
|
|
178
|
+
': how should this task start? Nothing is created until you answer.',
|
|
179
|
+
...fixed,
|
|
180
|
+
].join(' '),
|
|
181
|
+
language: facts.language,
|
|
182
|
+
questions,
|
|
183
|
+
terminalChoices: [
|
|
184
|
+
{ questionId: 'location', choice: 'defer' },
|
|
185
|
+
...(keep ? [{ questionId: 'location', choice: 'keep_current' }] : []),
|
|
186
|
+
],
|
|
187
|
+
binding: JSON.parse(JSON.stringify(binding)),
|
|
188
|
+
};
|
|
189
|
+
return { questionnaireId: 'task-start-' + sha256(stableStringify(content)), ...content };
|
|
190
|
+
}
|
|
191
|
+
export function taskStartChoice(record) {
|
|
192
|
+
if (record.status !== 'answered' ||
|
|
193
|
+
!record.questionnaireId.startsWith('task-start-') ||
|
|
194
|
+
!record.answers ||
|
|
195
|
+
!record.binding)
|
|
196
|
+
return null;
|
|
197
|
+
const binding = record.binding;
|
|
198
|
+
const task = {
|
|
199
|
+
externalTaskId: binding.externalTaskId,
|
|
200
|
+
...(binding.decisionAttempt ? { decisionAttempt: binding.decisionAttempt } : {}),
|
|
201
|
+
};
|
|
202
|
+
const location = record.answers.location?.choice;
|
|
203
|
+
if (location === 'defer')
|
|
204
|
+
return { status: 'deferred', ...task };
|
|
205
|
+
if (location === 'keep_current')
|
|
206
|
+
return {
|
|
207
|
+
status: 'start',
|
|
208
|
+
...task,
|
|
209
|
+
base: {
|
|
210
|
+
kind: 'current',
|
|
211
|
+
...(binding.currentCommit ? { commit: binding.currentCommit } : {}),
|
|
212
|
+
},
|
|
213
|
+
name: null,
|
|
214
|
+
keepCurrent: true,
|
|
215
|
+
inPlace: false,
|
|
216
|
+
holder: null,
|
|
217
|
+
};
|
|
218
|
+
if (location !== 'worktree' && location !== 'here')
|
|
219
|
+
return null;
|
|
220
|
+
const baseAnswer = record.answers.base;
|
|
221
|
+
const other = baseAnswer?.choice === 'other' ? baseAnswer.text?.replace(/^origin\//, '') : null;
|
|
222
|
+
const base = binding.base ??
|
|
223
|
+
(other
|
|
224
|
+
? { kind: 'remote', remote: 'origin', branch: other }
|
|
225
|
+
: baseAnswer
|
|
226
|
+
? binding.bases?.[baseAnswer.choice]
|
|
227
|
+
: undefined);
|
|
228
|
+
const nameAnswer = record.answers.name;
|
|
229
|
+
const name = binding.name ??
|
|
230
|
+
(nameAnswer?.choice === 'custom'
|
|
231
|
+
? nameAnswer.text
|
|
232
|
+
: nameAnswer?.choice === 'suggested'
|
|
233
|
+
? binding.suggestedName
|
|
234
|
+
: undefined);
|
|
235
|
+
if (!base || !name)
|
|
236
|
+
return null;
|
|
237
|
+
return {
|
|
238
|
+
status: 'start',
|
|
239
|
+
...task,
|
|
240
|
+
base,
|
|
241
|
+
name,
|
|
242
|
+
keepCurrent: false,
|
|
243
|
+
inPlace: location === 'here',
|
|
244
|
+
holder: location === 'here' ? binding.holder : null,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function resolvedBase(base, facts) {
|
|
248
|
+
if (base.kind === 'current')
|
|
249
|
+
return {
|
|
250
|
+
kind: 'current',
|
|
251
|
+
...((base.commit ?? facts.currentCommit)
|
|
252
|
+
? { commit: base.commit ?? facts.currentCommit }
|
|
253
|
+
: {}),
|
|
254
|
+
};
|
|
255
|
+
if (base.kind === 'development' || base.kind === 'production' || base.kind === 'test')
|
|
256
|
+
return configuredBase(facts.preferences, base.kind) ?? base;
|
|
257
|
+
return base;
|
|
258
|
+
}
|
|
259
|
+
function holderIdentity(holder) {
|
|
260
|
+
return {
|
|
261
|
+
externalTaskId: holder.externalTaskId,
|
|
262
|
+
taskId: holder.taskId,
|
|
263
|
+
oid: holder.oid,
|
|
264
|
+
generation: holder.generation,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function withheld(folder, keepOnly, tr) {
|
|
268
|
+
if (keepOnly || folder.managed || (!folder.heldBy && folder.clean))
|
|
269
|
+
return null;
|
|
270
|
+
if (folder.heldBy)
|
|
271
|
+
return tr
|
|
272
|
+
? folder.heldBy +
|
|
273
|
+
' bu klasörde başka bir oturumda hâlâ çalışıyor; yeni branch burada açılamaz. O oturumda duraklatılırsa bu klasör de seçilebilir.'
|
|
274
|
+
: 'Task ' +
|
|
275
|
+
folder.heldBy +
|
|
276
|
+
' is still running in another client in this folder, so the new branch cannot start here. Once it is paused there, this folder can be chosen too.';
|
|
277
|
+
return tr
|
|
278
|
+
? 'Bu klasörde kaydedilmemiş değişiklikler var; yeni branch burada açılamaz.'
|
|
279
|
+
: 'This folder has uncommitted changes, so the new branch cannot start here.';
|
|
280
|
+
}
|
|
281
|
+
//# sourceMappingURL=task-start.js.map
|