engineering-memory 1.11.7 → 1.11.8
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engineering-memory",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.8",
|
|
4
4
|
"description": "Installs the Engineering Memory skill and its local MCP bridge. Sign in after installing; your organization and project are resolved from your account.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -399,9 +399,15 @@ export class BridgeService {
|
|
|
399
399
|
(repository.projectId && repository.projectId !== projectId)) {
|
|
400
400
|
throw refuse('The live task belongs to a different project than the one this repository is bound to.', 'project.resolve');
|
|
401
401
|
}
|
|
402
|
-
|
|
402
|
+
const adoptable = pointer &&
|
|
403
|
+
!pointer.worktreeGeneration &&
|
|
404
|
+
normalizeTaskMode(pointer.mode) !== 'read_only' &&
|
|
405
|
+
this.dependencies.worktreePool
|
|
406
|
+
? await this.dependencies.worktreePool.find(projectId, taskSlug, repository.repoRoot)
|
|
407
|
+
: null;
|
|
408
|
+
if (pointer && this.dependencies.worktreePool && (pointer.worktreeGeneration || adoptable)) {
|
|
403
409
|
const pool = this.dependencies.worktreePool;
|
|
404
|
-
const previous = await pool.find(projectId, taskSlug, repository.repoRoot);
|
|
410
|
+
const previous = adoptable ?? (await pool.find(projectId, taskSlug, repository.repoRoot));
|
|
405
411
|
if (previous && previous.repoRoot !== repository.repoRoot)
|
|
406
412
|
return asJsonValue({
|
|
407
413
|
worktreeResumeRequired: true,
|
|
@@ -411,8 +417,11 @@ export class BridgeService {
|
|
|
411
417
|
nextAction: 'Call session.resume from this returned repoRoot and use it for every subsequent command.',
|
|
412
418
|
});
|
|
413
419
|
const sourceCommit = previous?.baseCommit ?? pointer.source?.sourceCommit;
|
|
420
|
+
const branch = pointer.worktreeGeneration
|
|
421
|
+
? pointer.branch
|
|
422
|
+
: (previous?.branch ?? pointer.branch);
|
|
414
423
|
if ((!sourceCommit && !(previous && previous.baseCommit === null && !previous.managed)) ||
|
|
415
|
-
|
|
424
|
+
branch === undefined)
|
|
416
425
|
throw refuse('The task branch and base must be restored before resuming its worktree.', 'worktree.reconcile');
|
|
417
426
|
const allocation = await pool.allocate({
|
|
418
427
|
projectId,
|
|
@@ -420,7 +429,7 @@ export class BridgeService {
|
|
|
420
429
|
repoFingerprint: repository.repoFingerprint,
|
|
421
430
|
repoRoot: repository.repoRoot,
|
|
422
431
|
externalTaskId: taskSlug,
|
|
423
|
-
branch
|
|
432
|
+
branch,
|
|
424
433
|
baseCommit: sourceCommit ?? null,
|
|
425
434
|
keepCurrent: previous ? !previous.managed : false,
|
|
426
435
|
resume: true,
|
|
@@ -433,6 +442,7 @@ export class BridgeService {
|
|
|
433
442
|
...pointer,
|
|
434
443
|
worktreeId: sha256(allocation.repoRoot),
|
|
435
444
|
worktreeGeneration: allocation.generation,
|
|
445
|
+
...(branch !== undefined ? { branch } : {}),
|
|
436
446
|
};
|
|
437
447
|
await this.dependencies.activeContexts.save(pointer);
|
|
438
448
|
if (allocation.repoRoot !== repository.repoRoot)
|
|
@@ -1884,11 +1894,26 @@ export class BridgeService {
|
|
|
1884
1894
|
const pool = this.dependencies.worktreePool;
|
|
1885
1895
|
if (!projectId || !pool)
|
|
1886
1896
|
throw refuse('Resolve the project with a pool-capable client.', 'project.resolve');
|
|
1887
|
-
if (operation === 'list' || operation === 'reconcile')
|
|
1897
|
+
if (operation === 'list' || operation === 'reconcile') {
|
|
1898
|
+
const managedRoot = await pool.managedRoot();
|
|
1888
1899
|
return asJsonValue({
|
|
1889
1900
|
items: await pool.reconcile(projectId, repository.repoRoot, await this.poolPolicy()),
|
|
1890
1901
|
selected: await pool.forPath(projectId, repository.repoRoot),
|
|
1902
|
+
managedRoot,
|
|
1903
|
+
...(managedRoot.problem || managedRoot.synced
|
|
1904
|
+
? {
|
|
1905
|
+
notice: [
|
|
1906
|
+
...(managedRoot.problem ? [managedRoot.problem] : []),
|
|
1907
|
+
...(managedRoot.synced
|
|
1908
|
+
? [
|
|
1909
|
+
`Managed worktrees are created under ${managedRoot.root}, which a cloud sync client watches, so every Git operation there is re-scanned and uploaded. To create new worktrees elsewhere, write {"root": "<absolute directory>"} to ${managedRoot.override}; it takes effect for the next allocation, and existing worktrees keep working where they are.`,
|
|
1910
|
+
]
|
|
1911
|
+
: []),
|
|
1912
|
+
].join(' '),
|
|
1913
|
+
}
|
|
1914
|
+
: {}),
|
|
1891
1915
|
});
|
|
1916
|
+
}
|
|
1892
1917
|
if (operation === 'release_unowned') {
|
|
1893
1918
|
const entry = await pool.forPath(projectId, repository.repoRoot);
|
|
1894
1919
|
if (!entry || !input.sourceCommit || !input.generation || !input.decisionId)
|
|
@@ -1974,6 +1999,21 @@ export class BridgeService {
|
|
|
1974
1999
|
resume: Boolean(existing),
|
|
1975
2000
|
}, await this.poolPolicy(true));
|
|
1976
2001
|
this.poolAllocations.set(allocation.repoRoot, allocation);
|
|
2002
|
+
if (existing && allocation.repoRoot === repository.repoRoot) {
|
|
2003
|
+
const current = await this.dependencies.activeContexts.loadForSlug(repository.repoFingerprint, input.externalTaskId);
|
|
2004
|
+
if (current &&
|
|
2005
|
+
current.taskSlug === input.externalTaskId &&
|
|
2006
|
+
!current.worktreeGeneration &&
|
|
2007
|
+
normalizeTaskMode(current.mode) !== 'read_only')
|
|
2008
|
+
await this.dependencies.activeContexts.save({
|
|
2009
|
+
...current,
|
|
2010
|
+
worktreeId: sha256(allocation.repoRoot),
|
|
2011
|
+
worktreeGeneration: allocation.generation,
|
|
2012
|
+
...(current.branch === undefined && allocation.branch !== null
|
|
2013
|
+
? { branch: allocation.branch }
|
|
2014
|
+
: {}),
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
1977
2017
|
return asJsonValue({
|
|
1978
2018
|
...allocation,
|
|
1979
2019
|
kept: !allocation.managed,
|
|
@@ -1987,11 +2027,10 @@ export class BridgeService {
|
|
|
1987
2027
|
const entry = await pool.forPath(pointer.projectId, repository.repoRoot);
|
|
1988
2028
|
if (!entry && !pointer.worktreeGeneration)
|
|
1989
2029
|
return; // existing unmanaged tasks retain their legacy reservation
|
|
2030
|
+
if (entry && entry.externalTaskId !== pointer.taskSlug)
|
|
2031
|
+
throw refuse(`This directory is allocated to task ${entry.externalTaskId}. Resume this task from its own worktree, or reconcile the pool to release the directory.`, 'worktree.reconcile');
|
|
1990
2032
|
const owned = this.poolAllocations.get(repository.repoRoot);
|
|
1991
|
-
if (!entry ||
|
|
1992
|
-
!owned ||
|
|
1993
|
-
owned.generation !== pointer.worktreeGeneration ||
|
|
1994
|
-
entry.externalTaskId !== pointer.taskSlug)
|
|
2033
|
+
if (!entry || !owned || owned.generation !== pointer.worktreeGeneration)
|
|
1995
2034
|
throw refuse('Resume this task to acquire its worktree before writing or delivering.', 'session.resume');
|
|
1996
2035
|
await pool.assertOwnership(pointer.projectId, repository.repoRoot, owned.generation);
|
|
1997
2036
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { mkdir, readFile, readdir } from 'node:fs/promises';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
5
5
|
import * as z from 'zod/v4';
|
|
6
6
|
import { GitInspector } from '../git/git-inspector.js';
|
|
7
7
|
import { atomicWrite, assertManagedPath, canonicalPath, ensureManagedDirectory, pathExists, readJson, removeFile, safeSegment, } from '../utilities/files.js';
|
|
@@ -37,11 +37,29 @@ const allocationSchema = z.object({
|
|
|
37
37
|
pendingDelivery: z.boolean(),
|
|
38
38
|
recoveredWithoutReservation: z.boolean().optional(),
|
|
39
39
|
reason: z.string().optional(),
|
|
40
|
+
root: z.string().optional(),
|
|
41
|
+
layout: z.union([z.literal(1), z.literal(2)]).optional(),
|
|
40
42
|
});
|
|
41
43
|
const registrySchema = z.object({
|
|
42
44
|
schemaVersion: z.literal(1),
|
|
43
45
|
projects: z.record(z.string(), z.object({ folder: z.string().regex(/^[a-zA-Z0-9._-]+$/), entries: z.array(allocationSchema) })),
|
|
44
46
|
});
|
|
47
|
+
export function managedBase(root, layout) {
|
|
48
|
+
return layout === 1
|
|
49
|
+
? join(root, 'engineeringmemory')
|
|
50
|
+
: join(root, 'engineering_memory', 'worktrees');
|
|
51
|
+
}
|
|
52
|
+
function projectDirectory(root, folder, layout) {
|
|
53
|
+
return layout === 1
|
|
54
|
+
? join(root, 'engineeringmemory', folder, 'worktrees')
|
|
55
|
+
: join(root, 'engineering_memory', 'worktrees', folder);
|
|
56
|
+
}
|
|
57
|
+
function worktreeName(folder, slot, layout) {
|
|
58
|
+
return folder + (layout === 1 ? '-worktree' : '_worktree') + slot;
|
|
59
|
+
}
|
|
60
|
+
function managedPath(root, folder, slot, layout) {
|
|
61
|
+
return join(projectDirectory(root, folder, layout), worktreeName(folder, slot, layout));
|
|
62
|
+
}
|
|
45
63
|
export class WorktreePool {
|
|
46
64
|
stateRoot;
|
|
47
65
|
ownerId;
|
|
@@ -53,6 +71,9 @@ export class WorktreePool {
|
|
|
53
71
|
startTimes = new Map();
|
|
54
72
|
ownStart;
|
|
55
73
|
documents;
|
|
74
|
+
legacyDocuments;
|
|
75
|
+
overrideDocuments;
|
|
76
|
+
overrideProblem;
|
|
56
77
|
before = new Map();
|
|
57
78
|
pendingWork;
|
|
58
79
|
constructor(stateRoot, options = {}) {
|
|
@@ -63,6 +84,7 @@ export class WorktreePool {
|
|
|
63
84
|
this.now = options.now ?? Date.now;
|
|
64
85
|
this.alive = options.processAlive ?? processAlive;
|
|
65
86
|
this.documents = options.documentsRoot;
|
|
87
|
+
this.legacyDocuments = options.legacyDocumentsRoot ?? options.documentsRoot;
|
|
66
88
|
this.pendingWork = options.pendingWork ?? (async () => false);
|
|
67
89
|
this.registryPath = join(stateRoot, 'worktree-pool.json');
|
|
68
90
|
}
|
|
@@ -79,8 +101,12 @@ export class WorktreePool {
|
|
|
79
101
|
const commonDir = await this.commonDirectory(input.repoRoot);
|
|
80
102
|
return this.transaction(async (registry) => {
|
|
81
103
|
if (!registry.projects[input.projectId]) {
|
|
82
|
-
const
|
|
83
|
-
const
|
|
104
|
+
const separator = process.platform === 'win32' ? '\\' : '/';
|
|
105
|
+
const bases = [];
|
|
106
|
+
for (const root of await this.scanRoots())
|
|
107
|
+
for (const layout of [1, 2])
|
|
108
|
+
bases.push(key(managedBase(root, layout)) + separator);
|
|
109
|
+
const orphan = (await this.gitPaths(input.repoRoot)).find((p) => bases.some((base) => key(p.path).startsWith(base)));
|
|
84
110
|
if (orphan)
|
|
85
111
|
throw refuse('Existing managed Git worktrees have no project index. Run worktree.reconcile before allocating more directories.');
|
|
86
112
|
}
|
|
@@ -107,7 +133,7 @@ export class WorktreePool {
|
|
|
107
133
|
if (paths.some((p) => p.branch === entry.branch && key(p.path) !== key(entry.repoRoot)))
|
|
108
134
|
throw refuse('The recorded branch is checked out at another location. Reconcile the moved worktree before resuming.');
|
|
109
135
|
if (paths.some((p) => key(p.path) === key(entry.repoRoot))) {
|
|
110
|
-
await assertManagedPath(await this.
|
|
136
|
+
await assertManagedPath(entry.root ?? (await this.legacyDocumentsDirectory()), entry.repoRoot, true);
|
|
111
137
|
if (await pathExists(entry.repoRoot))
|
|
112
138
|
throw refuse('The missing worktree reappeared; reconcile before recovery.');
|
|
113
139
|
await this.command(input.repoRoot, ['worktree', 'remove', entry.repoRoot]);
|
|
@@ -151,8 +177,10 @@ export class WorktreePool {
|
|
|
151
177
|
if (checkout)
|
|
152
178
|
throw refuse('This branch already has a checkout. Resume its task instead of creating or resetting the branch.');
|
|
153
179
|
let reusable;
|
|
180
|
+
const activeRoot = key(await this.documentsDirectory());
|
|
181
|
+
const legacyRoot = await this.legacyDocumentsDirectory();
|
|
154
182
|
for (const candidate of project.entries
|
|
155
|
-
.filter((e) => e.managed && e.commonDir === commonDir)
|
|
183
|
+
.filter((e) => e.managed && e.commonDir === commonDir && key(e.root ?? legacyRoot) === activeRoot)
|
|
156
184
|
.sort((a, b) => a.slot - b.slot)) {
|
|
157
185
|
if (candidate.pendingDelivery ||
|
|
158
186
|
['creating', 'quarantined'].includes(candidate.phase) ||
|
|
@@ -168,7 +196,7 @@ export class WorktreePool {
|
|
|
168
196
|
if (entry) {
|
|
169
197
|
const old = structuredClone(entry);
|
|
170
198
|
await this.releaseReservation(old);
|
|
171
|
-
const next = await this.entry(input, commonDir, old.repoRoot, old.slot, true);
|
|
199
|
+
const next = await this.entry(input, commonDir, old.repoRoot, old.slot, true, old.root ?? (await this.legacyDocumentsDirectory()), old.layout ?? 1);
|
|
172
200
|
project.entries[project.entries.indexOf(entry)] = next;
|
|
173
201
|
entry = next;
|
|
174
202
|
await this.save(registry);
|
|
@@ -179,14 +207,15 @@ export class WorktreePool {
|
|
|
179
207
|
if (project.entries.filter((e) => e.managed).length >= policy.maxWorktrees)
|
|
180
208
|
throw refuse('The managed worktree limit is full. worktree.list explains protected directories; finish delivery or safely release a clean task. No files were deleted.');
|
|
181
209
|
let slot = 1;
|
|
182
|
-
const
|
|
183
|
-
|
|
210
|
+
const active = await this.documentsDirectory();
|
|
211
|
+
const root = projectDirectory(active, project.folder, 2);
|
|
212
|
+
await ensureManagedDirectory(active, root);
|
|
184
213
|
while (project.entries.some((e) => e.slot === slot) ||
|
|
185
|
-
(await pathExists(join(root, project.folder
|
|
214
|
+
(await pathExists(join(root, worktreeName(project.folder, slot, 2)))))
|
|
186
215
|
slot++;
|
|
187
|
-
const path = join(root, project.folder
|
|
216
|
+
const path = join(root, worktreeName(project.folder, slot, 2));
|
|
188
217
|
await assertManagedPath(root, path, true);
|
|
189
|
-
entry = await this.entry(input, commonDir, path, slot, true);
|
|
218
|
+
entry = await this.entry(input, commonDir, path, slot, true, await this.documentsDirectory(), 2);
|
|
190
219
|
project.entries.push(entry);
|
|
191
220
|
await this.save(registry);
|
|
192
221
|
await this.git.createWorktree(input.repoRoot, input.branch, path, input.baseCommit);
|
|
@@ -326,24 +355,19 @@ export class WorktreePool {
|
|
|
326
355
|
let project = registry.projects[projectId];
|
|
327
356
|
const locations = await this.gitPaths(repoRoot);
|
|
328
357
|
const candidates = [];
|
|
329
|
-
const
|
|
358
|
+
const managedRoots = await this.scanRoots();
|
|
330
359
|
for (const location of locations) {
|
|
331
360
|
if (project?.entries.some((entry) => key(entry.repoRoot) === key(location.path)))
|
|
332
361
|
continue;
|
|
333
362
|
if (!(await pathExists(location.path)))
|
|
334
363
|
continue;
|
|
335
364
|
const parent = dirname(location.path);
|
|
336
|
-
const folder = dirname(parent).split(/[\\/]/).pop();
|
|
337
365
|
const name = location.path.split(/[\\/]/).pop();
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const prefix = folder + '-worktree';
|
|
341
|
-
const slot = Number(name.slice(prefix.length));
|
|
342
|
-
if (!name.startsWith(prefix) ||
|
|
343
|
-
!Number.isSafeInteger(slot) ||
|
|
344
|
-
slot < 1 ||
|
|
345
|
-
name !== prefix + slot)
|
|
366
|
+
const placement = this.placement(managedRoots, parent, name);
|
|
367
|
+
if (!placement)
|
|
346
368
|
continue;
|
|
369
|
+
const { documents, folder, slot, layout } = placement;
|
|
370
|
+
const managedRoot = managedBase(documents, layout);
|
|
347
371
|
try {
|
|
348
372
|
await assertManagedPath(managedRoot, location.path, false);
|
|
349
373
|
if ((await this.commonDirectory(location.path)) !== commonDir)
|
|
@@ -357,7 +381,7 @@ export class WorktreePool {
|
|
|
357
381
|
continue;
|
|
358
382
|
if (Object.entries(registry.projects).some(([id, value]) => id !== projectId && key(value.folder) === key(folder)))
|
|
359
383
|
continue;
|
|
360
|
-
candidates.push({ path: location.path, folder, slot });
|
|
384
|
+
candidates.push({ path: location.path, folder, slot, root: documents, layout });
|
|
361
385
|
}
|
|
362
386
|
if (!project) {
|
|
363
387
|
const folders = [...new Set(candidates.map((candidate) => candidate.folder))];
|
|
@@ -405,6 +429,8 @@ export class WorktreePool {
|
|
|
405
429
|
repoRoot: await canonicalPath(location.path),
|
|
406
430
|
slot: location.slot,
|
|
407
431
|
managed: true,
|
|
432
|
+
root: resolve(location.root),
|
|
433
|
+
layout: location.layout,
|
|
408
434
|
externalTaskId: reservation?.decision.externalTaskId ??
|
|
409
435
|
'unregistered-' + sha256(key(location.path)).slice(0, 20),
|
|
410
436
|
taskId: reservation?.decision.taskId,
|
|
@@ -451,9 +477,11 @@ export class WorktreePool {
|
|
|
451
477
|
});
|
|
452
478
|
return discoverOnly ? [] : this.list(projectId, policy);
|
|
453
479
|
}
|
|
454
|
-
async entry(input, commonDir, repoRoot, slot, managed) {
|
|
480
|
+
async entry(input, commonDir, repoRoot, slot, managed, root, layout) {
|
|
455
481
|
const generation = randomUUID();
|
|
456
482
|
return {
|
|
483
|
+
...(root ? { root: resolve(root) } : {}),
|
|
484
|
+
...(layout ? { layout } : {}),
|
|
457
485
|
projectId: input.projectId,
|
|
458
486
|
repoFingerprint: input.repoFingerprint,
|
|
459
487
|
commonDir,
|
|
@@ -626,9 +654,9 @@ export class WorktreePool {
|
|
|
626
654
|
.slice(0, 70);
|
|
627
655
|
if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(folder))
|
|
628
656
|
folder = 'project-' + folder;
|
|
629
|
-
const root = join(await this.documentsDirectory(), 'engineeringmemory');
|
|
630
657
|
if (Object.values(registry.projects).some((p) => key(p.folder) === key(folder)) ||
|
|
631
|
-
(await pathExists(
|
|
658
|
+
(await pathExists(projectDirectory(await this.documentsDirectory(), folder, 2))) ||
|
|
659
|
+
(await pathExists(join(managedBase(await this.legacyDocumentsDirectory(), 1), folder))))
|
|
632
660
|
folder += '-' + sha256(projectId).slice(0, 8);
|
|
633
661
|
if (Object.values(registry.projects).some((p) => key(p.folder) === key(folder)))
|
|
634
662
|
throw refuse('The managed project folder identity conflicts with another project.');
|
|
@@ -697,9 +725,87 @@ export class WorktreePool {
|
|
|
697
725
|
return null;
|
|
698
726
|
}
|
|
699
727
|
}
|
|
728
|
+
async managedRoot() {
|
|
729
|
+
const root = managedBase(await this.documentsDirectory(), 2);
|
|
730
|
+
return {
|
|
731
|
+
root,
|
|
732
|
+
override: join(this.stateRoot, 'worktree-root.json'),
|
|
733
|
+
synced: /onedrive|dropbox|icloud|mobile documents|google\s?drive/i.test(root),
|
|
734
|
+
...(this.overrideProblem ? { problem: this.overrideProblem } : {}),
|
|
735
|
+
};
|
|
736
|
+
}
|
|
700
737
|
async documentsDirectory() {
|
|
738
|
+
if (this.overrideDocuments)
|
|
739
|
+
return this.overrideDocuments;
|
|
740
|
+
const overridePath = join(this.stateRoot, 'worktree-root.json');
|
|
741
|
+
this.overrideProblem = undefined;
|
|
742
|
+
let override = null;
|
|
743
|
+
try {
|
|
744
|
+
override = await readJson(overridePath);
|
|
745
|
+
}
|
|
746
|
+
catch (error) {
|
|
747
|
+
this.overrideProblem = `${overridePath} could not be read and is ignored: ${error instanceof Error ? error.message : String(error)}`;
|
|
748
|
+
}
|
|
749
|
+
if (override && typeof override.root === 'string' && isAbsolute(override.root)) {
|
|
750
|
+
try {
|
|
751
|
+
await mkdir(override.root, { recursive: true }).catch((error) => {
|
|
752
|
+
if (!['EEXIST', 'EPERM'].includes(error.code ?? ''))
|
|
753
|
+
throw error;
|
|
754
|
+
});
|
|
755
|
+
this.overrideDocuments = resolve(override.root);
|
|
756
|
+
return this.overrideDocuments;
|
|
757
|
+
}
|
|
758
|
+
catch (error) {
|
|
759
|
+
this.overrideProblem = `${overridePath} names ${override.root}, which could not be created and is ignored: ${error instanceof Error ? error.message : String(error)}`;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
else if (override) {
|
|
763
|
+
this.overrideProblem = `${overridePath} must contain {"root": "<absolute directory>"} and is ignored.`;
|
|
764
|
+
}
|
|
765
|
+
return this.defaultDocumentsDirectory();
|
|
766
|
+
}
|
|
767
|
+
async defaultDocumentsDirectory() {
|
|
701
768
|
if (this.documents)
|
|
702
769
|
return resolve(this.documents);
|
|
770
|
+
this.documents =
|
|
771
|
+
process.platform === 'win32' ? (process.env.SystemDrive ?? 'C:') + '\\' : homedir();
|
|
772
|
+
return resolve(this.documents);
|
|
773
|
+
}
|
|
774
|
+
async scanRoots() {
|
|
775
|
+
return [
|
|
776
|
+
...new Map([
|
|
777
|
+
await this.documentsDirectory(),
|
|
778
|
+
await this.defaultDocumentsDirectory(),
|
|
779
|
+
await this.legacyDocumentsDirectory(),
|
|
780
|
+
].map((root) => [key(root), root])).values(),
|
|
781
|
+
];
|
|
782
|
+
}
|
|
783
|
+
placement(roots, parent, name) {
|
|
784
|
+
for (const documents of roots) {
|
|
785
|
+
const parentName = parent.split(/[\\/]/).pop() ?? '';
|
|
786
|
+
const grandParent = dirname(parent);
|
|
787
|
+
const legacyFolder = grandParent.split(/[\\/]/).pop() ?? '';
|
|
788
|
+
const attempts = [
|
|
789
|
+
{ folder: parentName, layout: 2 },
|
|
790
|
+
{ folder: legacyFolder, layout: 1 },
|
|
791
|
+
];
|
|
792
|
+
for (const { folder, layout } of attempts) {
|
|
793
|
+
if (!folder || key(parent) !== key(projectDirectory(documents, folder, layout)))
|
|
794
|
+
continue;
|
|
795
|
+
const prefix = folder + (layout === 1 ? '-worktree' : '_worktree');
|
|
796
|
+
const slot = Number(name.slice(prefix.length));
|
|
797
|
+
if (name.startsWith(prefix) &&
|
|
798
|
+
Number.isSafeInteger(slot) &&
|
|
799
|
+
slot >= 1 &&
|
|
800
|
+
name === prefix + slot)
|
|
801
|
+
return { documents, folder, slot, layout };
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
async legacyDocumentsDirectory() {
|
|
807
|
+
if (this.legacyDocuments)
|
|
808
|
+
return resolve(this.legacyDocuments);
|
|
703
809
|
if (process.platform === 'win32') {
|
|
704
810
|
const result = await this.runner.run('powershell.exe', [
|
|
705
811
|
'-NoProfile',
|
|
@@ -709,19 +815,18 @@ export class WorktreePool {
|
|
|
709
815
|
], {});
|
|
710
816
|
if (result.exitCode !== 0 || !result.stdout.trim())
|
|
711
817
|
throw refuse('The Windows Documents folder could not be resolved.');
|
|
712
|
-
this.
|
|
818
|
+
this.legacyDocuments = result.stdout.trim();
|
|
713
819
|
}
|
|
714
820
|
else if (process.platform === 'linux') {
|
|
715
821
|
const result = await this.runner.run('xdg-user-dir', ['DOCUMENTS'], {}).catch(() => null);
|
|
716
|
-
this.
|
|
822
|
+
this.legacyDocuments =
|
|
717
823
|
result?.exitCode === 0 && result.stdout.trim()
|
|
718
824
|
? result.stdout.trim()
|
|
719
825
|
: join(homedir(), 'Documents');
|
|
720
826
|
}
|
|
721
827
|
else
|
|
722
|
-
this.
|
|
723
|
-
|
|
724
|
-
return resolve(this.documents);
|
|
828
|
+
this.legacyDocuments = join(homedir(), 'Documents');
|
|
829
|
+
return resolve(this.legacyDocuments);
|
|
725
830
|
}
|
|
726
831
|
async transaction(work) {
|
|
727
832
|
await mkdir(this.stateRoot, { recursive: true });
|
|
@@ -764,8 +869,10 @@ export class WorktreePool {
|
|
|
764
869
|
throw refuse('The registry contains conflicting task ownership. Restore a validated backup with worktree.reconcile.');
|
|
765
870
|
paths.add(key(entry.repoRoot));
|
|
766
871
|
if (entry.managed) {
|
|
767
|
-
const documents = await this.
|
|
768
|
-
const expected =
|
|
872
|
+
const documents = entry.root ?? (await this.legacyDocumentsDirectory());
|
|
873
|
+
const expected = entry.slot
|
|
874
|
+
? managedPath(documents, project.folder, entry.slot, entry.layout ?? 1)
|
|
875
|
+
: '';
|
|
769
876
|
if (!entry.slot || key(entry.repoRoot) !== key(expected))
|
|
770
877
|
throw refuse('A managed worktree path does not match its project Documents directory. Reconcile before continuing.');
|
|
771
878
|
await assertManagedPath(documents, entry.repoRoot, true);
|
|
@@ -118,7 +118,10 @@ export async function assertManagedPath(root, target, allowMissingTarget) {
|
|
|
118
118
|
export async function ensureManagedDirectory(root, directory) {
|
|
119
119
|
const resolvedRoot = resolve(root);
|
|
120
120
|
const resolvedDirectory = ensureWithinRoot(resolvedRoot, directory);
|
|
121
|
-
await mkdir(resolvedRoot, { recursive: true })
|
|
121
|
+
await mkdir(resolvedRoot, { recursive: true }).catch((error) => {
|
|
122
|
+
if (!isNodeError(error) || !['EEXIST', 'EPERM'].includes(error.code ?? ''))
|
|
123
|
+
throw error;
|
|
124
|
+
});
|
|
122
125
|
const rootStat = await lstat(resolvedRoot);
|
|
123
126
|
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
|
|
124
127
|
throw new Error(`Managed root must be a real directory: ${resolvedRoot}`);
|
|
@@ -14,7 +14,7 @@ Before a new write or scaffold task, read `session.entry` and the project Git pr
|
|
|
14
14
|
|
|
15
15
|
Call `task.branch` with the stable `externalTaskId` and `repoRoot`. Its native forms show configured branch names, the current commit, another branch, and the explicit keep-current option. New branches always use a separate managed worktree. Configured remote bases are fetched and their exact commits pinned; after a fetch failure select an explicit local source through a new native decision, never silently use a cached branch. Existing task decisions survive retries and restarts. Resume their recorded branch rather than recreating or resetting it.
|
|
16
16
|
|
|
17
|
-
Use the returned `repoRoot` for **every** file read/write, terminal, context, validation and Git/delivery operation. The user-local pool is shared by Codex and Claude. Managed directories live in the
|
|
17
|
+
Use the returned `repoRoot` for **every** file read/write, terminal, context, validation and Git/delivery operation. The user-local pool is shared by Codex and Claude. Managed directories live under `engineering_memory/worktrees/<stable-project-folder>/<folder>_worktreeN`, at the root of the system drive on Windows (`C:\engineering_memory\worktrees\...`) and in the home directory elsewhere, or under the absolute directory named by `worktree-root.json` in the user-level API state directory when that file exists; `worktree.list` reports the effective root, and worktrees created by earlier clients under the Documents folder keep working where they are. Do not supply arbitrary `worktreePath` values or create ad-hoc siblings. Independent clones cannot reuse each other's worktrees. `worktree.list` explains which slots are active, inactive but protected, or safely reusable. The versioned backend policy defaults to 50 directories per project/computer, 30-second heartbeats and 10-minute inactivity. Protected inactive directories still count toward the limit. Only a global admin changes `worktree.set_policy`; it is not an environment setting.
|
|
18
18
|
|
|
19
19
|
Renew `task.heartbeat` using the exact returned task, path and ownership generation during actual work and at approximately the cached heartbeat interval during long local commands. The bridge renews while its own task operation is running. Never run a perpetual heartbeat for an idle chat or MCP process. Inactivity never authorizes takeover of a live owner or deletion of files. Before writing after interruption, use `session.resume`; follow any returned worktree redirect and resume there. A stale generation cannot renew, release, verify or commit another owner's work.
|
|
20
20
|
|