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.7",
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",
@@ -0,0 +1,3 @@
1
+ {
2
+ "gitHead": "eefcf3aac9c83ba9d0126d05309ca100947317d3"
3
+ }
@@ -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
- if (pointer?.worktreeGeneration && this.dependencies.worktreePool) {
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
- pointer.branch === undefined)
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: pointer.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 managedRoot = key(join(await this.documentsDirectory(), 'engineeringmemory'));
83
- const orphan = (await this.gitPaths(input.repoRoot)).find((p) => key(p.path).startsWith(managedRoot + (process.platform === 'win32' ? '\\' : '/')));
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.documentsDirectory(), entry.repoRoot, true);
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 root = join(await this.documentsDirectory(), 'engineeringmemory', project.folder, 'worktrees');
183
- await ensureManagedDirectory(await this.documentsDirectory(), root);
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 + '-worktree' + slot))))
214
+ (await pathExists(join(root, worktreeName(project.folder, slot, 2)))))
186
215
  slot++;
187
- const path = join(root, project.folder + '-worktree' + slot);
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 managedRoot = join(await this.documentsDirectory(), 'engineeringmemory');
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
- if (key(parent) !== key(join(managedRoot, folder, 'worktrees')))
339
- continue;
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(join(root, folder))))
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.documents = result.stdout.trim();
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.documents =
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.documents = join(homedir(), 'Documents');
723
- await mkdir(this.documents, { recursive: true });
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.documentsDirectory();
768
- const expected = join(documents, 'engineeringmemory', project.folder, 'worktrees', project.folder + '-worktree' + entry.slot);
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 OS Documents folder under `engineeringmemory/<stable-project-folder>/worktrees/<folder>-worktreeN`; 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.
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