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
|
@@ -4,6 +4,7 @@ import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
|
4
4
|
import { assertManagedPath, canonicalPath } from '../utilities/files.js';
|
|
5
5
|
import { sha256, stableStringify } from '../utilities/hash.js';
|
|
6
6
|
import { NativeCommandRunner } from '../utilities/process.js';
|
|
7
|
+
import { restrictedValueKind } from '../runtime/privacy-detector.js';
|
|
7
8
|
import { TaskBranchStore } from '../runtime/task-branch-store.js';
|
|
8
9
|
export const temporaryScaffoldingMarker = 'ENGINEERING-MEMORY-TEMPORARY';
|
|
9
10
|
export const temporaryScaffoldingPattern = `${temporaryScaffoldingMarker}:`;
|
|
@@ -35,6 +36,18 @@ export class GitInspector {
|
|
|
35
36
|
}
|
|
36
37
|
return sha256(identity);
|
|
37
38
|
}
|
|
39
|
+
async remoteIdentity(repoRoot) {
|
|
40
|
+
const remote = await this.gitValue(repoRoot, ['config', '--get', 'remote.origin.url']);
|
|
41
|
+
if (!remote)
|
|
42
|
+
return null;
|
|
43
|
+
const identity = canonicalRemoteIdentity(remote);
|
|
44
|
+
return !identity.startsWith('local/') &&
|
|
45
|
+
identity.length <= 500 &&
|
|
46
|
+
/^[a-z0-9][a-z0-9.-]*(:\d{1,5})?\/[^\s@\\]+$/.test(identity) &&
|
|
47
|
+
!restrictedValueKind(identity, 'remoteIdentity')
|
|
48
|
+
? identity
|
|
49
|
+
: null;
|
|
50
|
+
}
|
|
38
51
|
branchStore(repoRoot) {
|
|
39
52
|
return new TaskBranchStore(repoRoot, this.runner);
|
|
40
53
|
}
|
|
@@ -72,11 +85,13 @@ export class GitInspector {
|
|
|
72
85
|
throw new Error('The worktree directory already belongs to another repository or branch. Choose a different directory');
|
|
73
86
|
}
|
|
74
87
|
const exists = await this.branchExists(repoRoot, name);
|
|
88
|
+
const lost = (await this.lostRegistration(repoRoot, path, name)) ? ['-f'] : [];
|
|
75
89
|
const args = exists
|
|
76
|
-
? ['worktree', 'add', '--', path, name]
|
|
90
|
+
? ['worktree', 'add', ...lost, '--', path, name]
|
|
77
91
|
: [
|
|
78
92
|
'worktree',
|
|
79
93
|
'add',
|
|
94
|
+
...lost,
|
|
80
95
|
'--no-track',
|
|
81
96
|
'-b',
|
|
82
97
|
name,
|
|
@@ -89,6 +104,22 @@ export class GitInspector {
|
|
|
89
104
|
throw new Error(`Git could not create the worktree: ${result.stderr.trim()}`);
|
|
90
105
|
return await this.findRoot(path);
|
|
91
106
|
}
|
|
107
|
+
async lostRegistration(repoRoot, path, name) {
|
|
108
|
+
const listed = await this.runner.run('git', ['worktree', 'list', '--porcelain'], {
|
|
109
|
+
cwd: repoRoot,
|
|
110
|
+
});
|
|
111
|
+
if (listed.exitCode !== 0)
|
|
112
|
+
return false;
|
|
113
|
+
const registered = listed.stdout.split(/\r?\n\r?\n/).map((block) => ({
|
|
114
|
+
path: /^worktree (.+)$/m.exec(block)?.[1] ?? '',
|
|
115
|
+
branch: /^branch refs\/heads\/(.+)$/m.exec(block)?.[1] ?? null,
|
|
116
|
+
}));
|
|
117
|
+
const same = (left, right) => process.platform === 'win32'
|
|
118
|
+
? resolve(left).toLowerCase() === resolve(right).toLowerCase()
|
|
119
|
+
: resolve(left) === resolve(right);
|
|
120
|
+
return (registered.some((entry) => entry.path && same(entry.path, path)) &&
|
|
121
|
+
!registered.some((entry) => entry.branch === name && !same(entry.path, path)));
|
|
122
|
+
}
|
|
92
123
|
async fetchBranchCommit(repoRoot, remote, branch) {
|
|
93
124
|
await this.validateBranch(repoRoot, branch);
|
|
94
125
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/.test(remote))
|
|
@@ -242,6 +273,39 @@ export class GitInspector {
|
|
|
242
273
|
]);
|
|
243
274
|
return ref ? ref.replace(/^origin\//, '') : null;
|
|
244
275
|
}
|
|
276
|
+
async pushDestination(repoRoot) {
|
|
277
|
+
const branch = await this.currentBranch(repoRoot);
|
|
278
|
+
const upstream = branch
|
|
279
|
+
? await this.gitValue(repoRoot, ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'])
|
|
280
|
+
: null;
|
|
281
|
+
const listed = await this.runner.run('git', ['remote'], { cwd: repoRoot });
|
|
282
|
+
const remotes = listed.exitCode === 0 ? listed.stdout.split(/\r?\n/).filter(Boolean) : [];
|
|
283
|
+
const configured = (branch &&
|
|
284
|
+
(await this.gitValue(repoRoot, ['config', '--get', `branch.${branch}.pushRemote`]))) ||
|
|
285
|
+
(await this.gitValue(repoRoot, ['config', '--get', 'remote.pushDefault'])) ||
|
|
286
|
+
(branch && (await this.gitValue(repoRoot, ['config', '--get', `branch.${branch}.remote`]))) ||
|
|
287
|
+
'origin';
|
|
288
|
+
const remote = remotes.includes(configured) ? configured : null;
|
|
289
|
+
const pushUrl = remote
|
|
290
|
+
? await this.gitValue(repoRoot, ['remote', 'get-url', '--push', remote])
|
|
291
|
+
: null;
|
|
292
|
+
const remoteHead = remote
|
|
293
|
+
? await this.gitValue(repoRoot, [
|
|
294
|
+
'symbolic-ref',
|
|
295
|
+
'--short',
|
|
296
|
+
'-q',
|
|
297
|
+
`refs/remotes/${remote}/HEAD`,
|
|
298
|
+
])
|
|
299
|
+
: null;
|
|
300
|
+
return {
|
|
301
|
+
branch,
|
|
302
|
+
upstream,
|
|
303
|
+
remote,
|
|
304
|
+
pushUrl: pushUrl ? withoutCredentials(pushUrl) : null,
|
|
305
|
+
remoteDefaultBranch: remoteHead ? remoteHead.slice(`${remote}/`.length) : null,
|
|
306
|
+
headCommit: await this.gitValue(repoRoot, ['rev-parse', '--verify', '-q', 'HEAD']),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
245
309
|
async protectedBranchNames(repoRoot) {
|
|
246
310
|
const names = new Set(['main', 'master', 'develop', 'prod']);
|
|
247
311
|
const fallback = await this.defaultBranch(repoRoot);
|
|
@@ -286,6 +350,14 @@ export class GitInspector {
|
|
|
286
350
|
}
|
|
287
351
|
return await canonicalPath(resolve(commonDirectory, '..'));
|
|
288
352
|
}
|
|
353
|
+
async identity(repoRoot) {
|
|
354
|
+
const root = await this.findRoot(repoRoot);
|
|
355
|
+
const [head, branch] = await Promise.all([
|
|
356
|
+
this.gitValue(root, ['rev-parse', 'HEAD']),
|
|
357
|
+
this.currentBranch(root),
|
|
358
|
+
]);
|
|
359
|
+
return { repoRoot: root, head, branch };
|
|
360
|
+
}
|
|
289
361
|
async manifest(repoRoot) {
|
|
290
362
|
const root = await this.findRoot(repoRoot);
|
|
291
363
|
const head = await this.gitValue(root, ['rev-parse', 'HEAD']);
|
|
@@ -301,6 +373,30 @@ export class GitInspector {
|
|
|
301
373
|
diffHash: sha256(`${head ?? '<unborn>'}\n${stableStringify(hashManifest)}\n`),
|
|
302
374
|
};
|
|
303
375
|
}
|
|
376
|
+
async manifestAgainst(repoRoot, baseCommit) {
|
|
377
|
+
const root = await this.findRoot(repoRoot);
|
|
378
|
+
const head = await this.gitValue(root, ['rev-parse', 'HEAD']);
|
|
379
|
+
const changedPaths = await this.changedPathsAgainstHead(root, baseCommit);
|
|
380
|
+
const hashManifest = canonicalHashManifest(changedPaths);
|
|
381
|
+
return {
|
|
382
|
+
repoRoot: root,
|
|
383
|
+
head,
|
|
384
|
+
baseCommit,
|
|
385
|
+
changedPaths,
|
|
386
|
+
worktreeHash: sha256(`${stableStringify(hashManifest)}\n`),
|
|
387
|
+
diffHash: sha256(`${baseCommit}\n${head ?? '<unborn>'}\n${stableStringify(hashManifest)}\n`),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
async isAncestor(repoRoot, ancestor, descendant = 'HEAD') {
|
|
391
|
+
const result = await this.runner.run('git', ['merge-base', '--is-ancestor', ancestor, descendant], { cwd: repoRoot });
|
|
392
|
+
return result.exitCode === 0;
|
|
393
|
+
}
|
|
394
|
+
async committedPaths(repoRoot, baseCommit) {
|
|
395
|
+
const result = await this.runner.run('git', ['diff', '--name-only', '-z', '--find-renames', baseCommit, 'HEAD', '--'], { cwd: repoRoot });
|
|
396
|
+
if (result.exitCode !== 0)
|
|
397
|
+
throw new Error(`Git committed diff failed: ${result.stderr.trim()}`);
|
|
398
|
+
return result.stdout.split('\0').filter(Boolean).map(normalizeGitPath).sort();
|
|
399
|
+
}
|
|
304
400
|
async stagedManifest(repoRoot) {
|
|
305
401
|
const root = await this.findRoot(repoRoot);
|
|
306
402
|
let result = await this.runner.run('git', ['diff', '--cached', '--name-status', '-z', '--find-renames', '--find-copies', 'HEAD', '--'], { cwd: root });
|
|
@@ -357,10 +453,10 @@ export class GitInspector {
|
|
|
357
453
|
}
|
|
358
454
|
return result.sort((left, right) => left.path.localeCompare(right.path));
|
|
359
455
|
}
|
|
360
|
-
async changedPathsAgainstHead(repoRoot) {
|
|
456
|
+
async changedPathsAgainstHead(repoRoot, ref = 'HEAD') {
|
|
361
457
|
const [tracked, raw, untracked] = await Promise.all([
|
|
362
|
-
this.runner.run('git', ['diff',
|
|
363
|
-
this.runner.run('git', ['diff',
|
|
458
|
+
this.runner.run('git', ['diff', ref, '--name-status', '-z', '--find-renames', '--find-copies', '--'], { cwd: repoRoot }),
|
|
459
|
+
this.runner.run('git', ['diff', ref, '--raw', '-z', '--no-abbrev', '--'], {
|
|
364
460
|
cwd: repoRoot,
|
|
365
461
|
}),
|
|
366
462
|
this.runner.run('git', ['ls-files', '--others', '--exclude-standard', '-z'], {
|
|
@@ -699,6 +795,20 @@ function canonicalRemoteIdentity(remote) {
|
|
|
699
795
|
return `local/${normalizeRemotePath(trimmed.replace(/[?#].*$/, ''))}`;
|
|
700
796
|
}
|
|
701
797
|
}
|
|
798
|
+
function withoutCredentials(remote) {
|
|
799
|
+
if (!/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(remote))
|
|
800
|
+
return remote;
|
|
801
|
+
try {
|
|
802
|
+
const url = new URL(remote);
|
|
803
|
+
if (url.protocol === 'http:' || url.protocol === 'https:')
|
|
804
|
+
url.username = ''; // a pasted token sits here
|
|
805
|
+
url.password = '';
|
|
806
|
+
return url.toString();
|
|
807
|
+
}
|
|
808
|
+
catch {
|
|
809
|
+
return remote.replace(/\/\/[^/@]*@/, '//');
|
|
810
|
+
}
|
|
811
|
+
}
|
|
702
812
|
function normalizeRemotePath(path) {
|
|
703
813
|
const normalized = path
|
|
704
814
|
.replace(/\\/g, '/')
|
|
@@ -151,23 +151,34 @@ export class VerificationGate {
|
|
|
151
151
|
};
|
|
152
152
|
}
|
|
153
153
|
const actualSource = receipt.source ? await this.git.sourceIdentity(repoRoot) : null;
|
|
154
|
+
const ahead = receipt.branch &&
|
|
155
|
+
receipt.source &&
|
|
156
|
+
actualSource &&
|
|
157
|
+
actualSource.sourceCommit !== receipt.source.sourceCommit &&
|
|
158
|
+
(await this.git.isAncestor(repository.repoRoot, receipt.source.sourceCommit)) &&
|
|
159
|
+
(await this.git.committedPaths(repository.repoRoot, receipt.source.sourceCommit)).every((path) => receipt.taskChanges.some((change) => change.path === path))
|
|
160
|
+
? await this.git.manifestAgainst(repository.repoRoot, receipt.source.sourceCommit)
|
|
161
|
+
: null;
|
|
162
|
+
const committedAhead = ahead?.diffHash === receipt.diffHash;
|
|
163
|
+
const current = ahead && committedAhead ? ahead : manifest;
|
|
154
164
|
if (receipt.source &&
|
|
165
|
+
!committedAhead &&
|
|
155
166
|
(!actualSource ||
|
|
156
167
|
actualSource.sourceCommit !== receipt.source.sourceCommit ||
|
|
157
168
|
actualSource.treeHash !== receipt.source.treeHash)) {
|
|
158
169
|
return {
|
|
159
170
|
allowed: false,
|
|
160
171
|
reason: 'source_commit_mismatch',
|
|
161
|
-
diffHash:
|
|
172
|
+
diffHash: current.diffHash,
|
|
162
173
|
taskId: receipt.taskId,
|
|
163
174
|
details: ['Return to the task source commit or start a task for the current source.'],
|
|
164
175
|
};
|
|
165
176
|
}
|
|
166
|
-
if (receipt.diffHash !==
|
|
177
|
+
if (receipt.diffHash !== current.diffHash) {
|
|
167
178
|
return {
|
|
168
179
|
allowed: false,
|
|
169
180
|
reason: 'git_diff_changed_after_verify',
|
|
170
|
-
diffHash:
|
|
181
|
+
diffHash: current.diffHash,
|
|
171
182
|
taskId: receipt.taskId,
|
|
172
183
|
};
|
|
173
184
|
}
|
|
@@ -175,7 +186,7 @@ export class VerificationGate {
|
|
|
175
186
|
return {
|
|
176
187
|
allowed: false,
|
|
177
188
|
reason: 'no_task_changes_staged',
|
|
178
|
-
diffHash:
|
|
189
|
+
diffHash: current.diffHash,
|
|
179
190
|
taskId: receipt.taskId,
|
|
180
191
|
};
|
|
181
192
|
}
|
|
@@ -183,7 +194,7 @@ export class VerificationGate {
|
|
|
183
194
|
return {
|
|
184
195
|
allowed: false,
|
|
185
196
|
reason: 'staged_change_outside_task',
|
|
186
|
-
diffHash:
|
|
197
|
+
diffHash: current.diffHash,
|
|
187
198
|
taskId: receipt.taskId,
|
|
188
199
|
};
|
|
189
200
|
}
|
|
@@ -191,22 +202,24 @@ export class VerificationGate {
|
|
|
191
202
|
return {
|
|
192
203
|
allowed: false,
|
|
193
204
|
reason: 'online_commit_gate_unavailable',
|
|
194
|
-
diffHash:
|
|
205
|
+
diffHash: current.diffHash,
|
|
195
206
|
taskId: receipt.taskId,
|
|
196
207
|
};
|
|
197
208
|
}
|
|
198
209
|
try {
|
|
199
210
|
const attestation = await this.commitGateAttestor({
|
|
200
|
-
source:
|
|
201
|
-
?
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
211
|
+
source: committedAhead
|
|
212
|
+
? receipt.source
|
|
213
|
+
: actualSource
|
|
214
|
+
? {
|
|
215
|
+
...actualSource,
|
|
216
|
+
...(receipt.source?.linked ? { linked: receipt.source.linked } : {}),
|
|
217
|
+
}
|
|
218
|
+
: undefined,
|
|
206
219
|
taskId: receipt.taskId,
|
|
207
220
|
projectId: receipt.projectId,
|
|
208
221
|
repoFingerprint: receipt.repoFingerprint,
|
|
209
|
-
diffHash:
|
|
222
|
+
diffHash: current.diffHash,
|
|
210
223
|
expectedTaskVersion: receipt.taskVersion,
|
|
211
224
|
branch: branch ?? undefined,
|
|
212
225
|
});
|
|
@@ -214,11 +227,11 @@ export class VerificationGate {
|
|
|
214
227
|
if (result?.allowed !== true ||
|
|
215
228
|
result.taskId !== receipt.taskId ||
|
|
216
229
|
result.taskVersion !== receipt.taskVersion ||
|
|
217
|
-
result.diffHash !==
|
|
230
|
+
result.diffHash !== current.diffHash) {
|
|
218
231
|
return {
|
|
219
232
|
allowed: false,
|
|
220
233
|
reason: 'online_commit_gate_mismatch',
|
|
221
|
-
diffHash:
|
|
234
|
+
diffHash: current.diffHash,
|
|
222
235
|
taskId: receipt.taskId,
|
|
223
236
|
};
|
|
224
237
|
}
|
|
@@ -227,7 +240,7 @@ export class VerificationGate {
|
|
|
227
240
|
return {
|
|
228
241
|
allowed: false,
|
|
229
242
|
reason: 'online_commit_gate_rejected',
|
|
230
|
-
diffHash:
|
|
243
|
+
diffHash: current.diffHash,
|
|
231
244
|
taskId: receipt.taskId,
|
|
232
245
|
details: [error instanceof Error ? error.message : String(error)],
|
|
233
246
|
};
|
|
@@ -235,7 +248,7 @@ export class VerificationGate {
|
|
|
235
248
|
return {
|
|
236
249
|
allowed: true,
|
|
237
250
|
reason: 'verified',
|
|
238
|
-
diffHash:
|
|
251
|
+
diffHash: current.diffHash,
|
|
239
252
|
taskId: receipt.taskId,
|
|
240
253
|
};
|
|
241
254
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as z from 'zod/v4';
|
|
2
2
|
import { sha256, stableStringify } from '../utilities/hash.js';
|
|
3
|
-
import { askQuestionnaire } from './questionnaire-tools.js';
|
|
3
|
+
import { answerChoice, answerData, askQuestionnaire } from './questionnaire-tools.js';
|
|
4
4
|
const uuid = z.string().uuid();
|
|
5
5
|
const hash = z.string().regex(/^[a-f0-9]{64}$/);
|
|
6
6
|
const project = { projectId: uuid };
|
|
@@ -133,7 +133,114 @@ const onboardingInput = z.strictObject({
|
|
|
133
133
|
projectId: uuid.optional(),
|
|
134
134
|
reworkBehavior: z.enum(['preserve', 'redesign']).optional(),
|
|
135
135
|
});
|
|
136
|
+
const moveCopy = {
|
|
137
|
+
tr: {
|
|
138
|
+
ask: (name) => `"${name}" projesini bu depoya taşıyalım mı?`,
|
|
139
|
+
from: 'Projenin şu anki deposu',
|
|
140
|
+
to: 'Bu klasörün deposu',
|
|
141
|
+
unknown: 'adresi kayıtlı değil',
|
|
142
|
+
keeps: (tasks, records, items) => `Proje olduğu gibi devam eder: ${tasks} görev, ${records} hafıza kaydı ve ${items} iş kalemi projede kalır, hiçbir şey silinmez ve yeniden öğrenilmez.`,
|
|
143
|
+
after: 'Eski depo projenin geçmişinde saklanır. Hâlâ eski depoda çalışan ekip arkadaşları açık görevlerini bitirebilir, yeni görevi yeni depodan açar. Proje ileride yeniden taşınabilir.',
|
|
144
|
+
approve: 'Projeyi bu depoya taşı',
|
|
145
|
+
decline: 'Taşıma',
|
|
146
|
+
},
|
|
147
|
+
en: {
|
|
148
|
+
ask: (name) => `Move the project "${name}" to this repository?`,
|
|
149
|
+
from: 'The project lives in',
|
|
150
|
+
to: 'This folder points at',
|
|
151
|
+
unknown: 'address not recorded',
|
|
152
|
+
keeps: (tasks, records, items) => `The project continues as it is: ${tasks} tasks, ${records} memory records and ${items} work items stay with it; nothing is deleted or learned again.`,
|
|
153
|
+
after: 'The old repository is kept in the project history. Teammates still working in it can finish their open tasks and start new ones from the new repository. The project can be moved again later.',
|
|
154
|
+
approve: 'Move the project here',
|
|
155
|
+
decline: 'Do not move',
|
|
156
|
+
},
|
|
157
|
+
};
|
|
136
158
|
export function registerOnboardingTools(server, service) {
|
|
159
|
+
server.registerTool('project.move_repository', {
|
|
160
|
+
description: 'Move an existing project to the repository this checkout points at, after its code moved (another host, a new remote, a history restart). Project owners only. Opens ONE durable native form naming the old and the new repository; on approval the project keeps every task, memory record, work item and open session, the old repository is kept in its history, and this checkout is bound. Use it when project.resolve with bind refuses with recovery project.move_repository; never create a second project for moved code. Reconsider a declined move with a new decisionAttempt; retries keep the same attempt.',
|
|
161
|
+
inputSchema: z.strictObject({
|
|
162
|
+
repoRoot,
|
|
163
|
+
projectId: uuid,
|
|
164
|
+
decisionAttempt: z.number().int().min(1).max(10000).optional(),
|
|
165
|
+
language: z.enum(['tr', 'en']).default('en'),
|
|
166
|
+
presentation: z.enum(['host_native']).optional(),
|
|
167
|
+
}),
|
|
168
|
+
}, async (input, context) => {
|
|
169
|
+
const preflight = await service.projectRepositoryMovePreflight(input);
|
|
170
|
+
if (!preflight.ok)
|
|
171
|
+
return output(preflight);
|
|
172
|
+
const flight = preflight.data;
|
|
173
|
+
if (flight.authentication)
|
|
174
|
+
return output(preflight);
|
|
175
|
+
if (flight.alreadyHere || !flight.history.current)
|
|
176
|
+
return output(await service.projectResolve({
|
|
177
|
+
repoRoot: input.repoRoot,
|
|
178
|
+
bind: true,
|
|
179
|
+
projectId: input.projectId,
|
|
180
|
+
}));
|
|
181
|
+
if (flight.history.role !== 'owner')
|
|
182
|
+
return output({
|
|
183
|
+
ok: false,
|
|
184
|
+
error: {
|
|
185
|
+
kind: 'bridge_error',
|
|
186
|
+
message: 'Only a project owner can move a project to another repository. List the members with project.member_list and ask an owner to run project.move_repository from a checkout of the new repository. Nothing was changed.',
|
|
187
|
+
retryable: false,
|
|
188
|
+
recovery: 'project.member_list',
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
const copy = moveCopy[input.language];
|
|
192
|
+
const questionnaireId = 'project-move-' +
|
|
193
|
+
sha256(stableStringify({
|
|
194
|
+
projectId: input.projectId,
|
|
195
|
+
from: flight.history.current.repoFingerprint,
|
|
196
|
+
to: flight.checkout.repoFingerprint,
|
|
197
|
+
version: flight.history.lockVersion,
|
|
198
|
+
...(input.decisionAttempt ? { decisionAttempt: input.decisionAttempt } : {}),
|
|
199
|
+
}));
|
|
200
|
+
const form = await askQuestionnaire(server, service, {
|
|
201
|
+
repoRoot: input.repoRoot,
|
|
202
|
+
questionnaireId,
|
|
203
|
+
presentation: input.presentation,
|
|
204
|
+
language: input.language,
|
|
205
|
+
message: [
|
|
206
|
+
copy.ask(flight.history.name),
|
|
207
|
+
`${copy.from}: ${flight.history.current.remoteIdentity ?? copy.unknown}`,
|
|
208
|
+
`${copy.to}: ${flight.checkout.remoteIdentity ?? copy.unknown}`,
|
|
209
|
+
copy.keeps(flight.history.records.tasks, flight.history.records.memoryRecords, flight.history.records.workItems),
|
|
210
|
+
copy.after,
|
|
211
|
+
].join('\n'),
|
|
212
|
+
options: [
|
|
213
|
+
{ id: 'approve', label: copy.approve },
|
|
214
|
+
{ id: 'decline', label: copy.decline },
|
|
215
|
+
],
|
|
216
|
+
}, context, [], {
|
|
217
|
+
tool: 'project.move_repository',
|
|
218
|
+
...(input.decisionAttempt ? { decisionAttempt: input.decisionAttempt } : {}),
|
|
219
|
+
retryArguments: { projectId: input.projectId, language: input.language },
|
|
220
|
+
});
|
|
221
|
+
const record = await service.questionnaireResume({
|
|
222
|
+
repoRoot: input.repoRoot,
|
|
223
|
+
questionnaireId,
|
|
224
|
+
});
|
|
225
|
+
if (record.status !== 'answered')
|
|
226
|
+
return form;
|
|
227
|
+
if (record.answer?.choice !== 'approve')
|
|
228
|
+
return output({
|
|
229
|
+
ok: true,
|
|
230
|
+
data: {
|
|
231
|
+
moved: false,
|
|
232
|
+
status: 'declined',
|
|
233
|
+
nextAction: 'The project was not moved and nothing changed. Tell the user in one line. If they later want the move, call project.move_repository with decisionAttempt ' +
|
|
234
|
+
((input.decisionAttempt ?? 0) + 1) +
|
|
235
|
+
'.',
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
return output(await service.projectMoveRepository({
|
|
239
|
+
repoRoot: input.repoRoot,
|
|
240
|
+
projectId: input.projectId,
|
|
241
|
+
expectedVersion: flight.history.lockVersion,
|
|
242
|
+
}));
|
|
243
|
+
});
|
|
137
244
|
server.registerTool('memory.source_targets', {
|
|
138
245
|
description: 'Read bounded Git ref candidates without assuming environment roles from their names. Confirm only the chosen committed sources with native memory.sync_start questionnaires; an explicit ref can select a source outside this preview.',
|
|
139
246
|
inputSchema: z.object({ repoRoot: z.string().optional() }),
|
|
@@ -468,7 +575,7 @@ export function registerOnboardingTools(server, service) {
|
|
|
468
575
|
inputSchema: z.object(run),
|
|
469
576
|
}, async (input) => output(await service.onboardingRequest('verify', body(input))));
|
|
470
577
|
server.registerTool('memory.catalog', {
|
|
471
|
-
description: 'Page accessible memory resource headers by id
|
|
578
|
+
description: 'Page accessible memory resource headers by id: each entry names its current revision and its size in characters instead of carrying its content. Pass the response nextAfterId as afterId to read the next page; nextAfterId is null once nothing more remains. includeArchived exposes retired identities for refresh and rename preservation.',
|
|
472
579
|
inputSchema: z.object({
|
|
473
580
|
...project,
|
|
474
581
|
...page,
|
|
@@ -479,7 +586,7 @@ export function registerOnboardingTools(server, service) {
|
|
|
479
586
|
}),
|
|
480
587
|
}, async (input) => output(await service.onboardingRequest('catalog', body(input))));
|
|
481
588
|
server.registerTool('memory.read_revisions', {
|
|
482
|
-
description: 'Read selected immutable revisions in an authorized context session and record receipts. Bounds keep large project learning independent of normal task context limits.',
|
|
589
|
+
description: 'Read selected immutable revisions in an authorized context session and record receipts. One response carries what fits its size budget, in the order requested, as items; the revisions it did not carry are listed in remainingRevisionIds, to pass to the next call. Bounds keep large project learning independent of normal task context limits.',
|
|
483
590
|
inputSchema: z.object({
|
|
484
591
|
...project,
|
|
485
592
|
sessionId: uuid,
|
|
@@ -533,19 +640,6 @@ export function registerOnboardingTools(server, service) {
|
|
|
533
640
|
})));
|
|
534
641
|
});
|
|
535
642
|
}
|
|
536
|
-
function answerChoice(result) {
|
|
537
|
-
return answerData(result)?.choice;
|
|
538
|
-
}
|
|
539
|
-
function answerData(result) {
|
|
540
|
-
const response = result;
|
|
541
|
-
const text = response.content?.find((item) => item.type === 'text')?.text;
|
|
542
|
-
if (!text)
|
|
543
|
-
return undefined;
|
|
544
|
-
const value = JSON.parse(text);
|
|
545
|
-
return value.ok && value.data?.status === 'answered' && value.data.answerAvailable
|
|
546
|
-
? value.data.answer
|
|
547
|
-
: undefined;
|
|
548
|
-
}
|
|
549
643
|
async function approveOnboarding(server, service, input, context, initialize) {
|
|
550
644
|
const preparation = !input.projectId;
|
|
551
645
|
const scope = { repoRoot: input.repoRoot, preparation };
|