engineering-memory 1.11.14 → 1.11.16

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.
@@ -9,6 +9,9 @@ import { sha256 } from '../utilities/hash.js';
9
9
  import { localMutex } from '../utilities/local-mutex.js';
10
10
  import { NativeCommandRunner } from '../utilities/process.js';
11
11
  import { BridgeRecoveryError } from './recovery-error.js';
12
+ import { planWorktreeFiles, createWorktreeStage, stageWorktreeFiles, publishWorktreeFiles, discardStagedWorktreeFiles, } from './worktree-preparation.js';
13
+ import { WorktreeFileIssue, WorktreeFileStatus, } from './worktree-readiness-types.js';
14
+ import { openWorktreeInEditor, WorktreeEditorStatus } from './worktree-editor.js';
12
15
  const allocationSchema = z.object({
13
16
  projectId: z.string().min(1),
14
17
  repoFingerprint: z.string().regex(/^[a-f0-9]{64}$/),
@@ -42,6 +45,37 @@ const allocationSchema = z.object({
42
45
  deliveryOutcome: z.enum(['delivered', 'cancelled']).optional(),
43
46
  cancelledAt: z.string().optional(),
44
47
  preservedRef: z.string().optional(),
48
+ sourceRepoRoot: z.string().optional(),
49
+ sourceMainRoot: z.string().optional(),
50
+ preparedPaths: z.array(z.string()).optional(),
51
+ preparationSources: z.record(z.string(), z.string()).optional(),
52
+ preparationStage: z
53
+ .object({
54
+ directory: z.string(),
55
+ repoRoot: z.string(),
56
+ count: z.number().int().min(0).max(100),
57
+ identity: z.string(),
58
+ })
59
+ .optional(),
60
+ editorGeneration: z.string().optional(),
61
+ editorResult: z
62
+ .object({ status: z.nativeEnum(WorktreeEditorStatus), detail: z.string().optional() })
63
+ .optional(),
64
+ readiness: z
65
+ .object({
66
+ files: z.object({
67
+ status: z.nativeEnum(WorktreeFileStatus),
68
+ copied: z.number().int().nonnegative(),
69
+ unchanged: z.number().int().nonnegative(),
70
+ externalDependencies: z.number().int().nonnegative().optional(),
71
+ problems: z.array(z.object({ path: z.string(), reason: z.nativeEnum(WorktreeFileIssue) })),
72
+ }),
73
+ editor: z.object({
74
+ status: z.nativeEnum(WorktreeEditorStatus),
75
+ detail: z.string().optional(),
76
+ }),
77
+ })
78
+ .optional(),
45
79
  });
46
80
  const registrySchema = z.object({
47
81
  schemaVersion: z.literal(1),
@@ -230,6 +264,8 @@ export class WorktreePool {
230
264
  const old = structuredClone(entry);
231
265
  await this.releaseReservation(old);
232
266
  const next = await this.entry(input, commonDir, old.repoRoot, old.slot, true, old.root ?? (await this.legacyDocumentsDirectory()), old.layout ?? 1);
267
+ next.preparedPaths = old.preparedPaths;
268
+ next.preparationStage = old.preparationStage;
233
269
  project.entries[project.entries.indexOf(entry)] = next;
234
270
  entry = next;
235
271
  await this.save(registry);
@@ -260,6 +296,138 @@ export class WorktreePool {
260
296
  return structuredClone(entry);
261
297
  });
262
298
  }
299
+ async prepare(allocation) {
300
+ if (!allocation.managed || allocation.phase !== 'working')
301
+ return allocation;
302
+ return localMutex(key(this.stateRoot) + '/prepare/' + key(allocation.repoRoot), async () => {
303
+ const current = await this.transaction(async (registry) => {
304
+ const entry = this.owned(registry, allocation.projectId, allocation.repoRoot, allocation.generation);
305
+ await this.assertIdentity(entry);
306
+ if (!entry.sourceRepoRoot) {
307
+ entry.sourceRepoRoot = await this.git.mainWorktree(entry.repoRoot);
308
+ entry.sourceMainRoot = entry.sourceRepoRoot;
309
+ await this.save(registry);
310
+ }
311
+ return structuredClone(entry);
312
+ });
313
+ let files;
314
+ try {
315
+ if (current.preparationStage) {
316
+ await this.transaction(async (registry) => {
317
+ const entry = this.owned(registry, current.projectId, current.repoRoot, current.generation);
318
+ if (entry.preparationStage) {
319
+ if (entry.preparationStage.repoRoot !== entry.repoRoot)
320
+ throw new Error('Unknown staging owner');
321
+ await discardStagedWorktreeFiles(entry.preparationStage);
322
+ delete entry.preparationStage;
323
+ await this.save(registry);
324
+ }
325
+ });
326
+ }
327
+ const plan = await planWorktreeFiles(current.sourceRepoRoot, current.sourceMainRoot ?? current.sourceRepoRoot, current.repoRoot, current.preparedPaths ?? [], current.preparationSources);
328
+ await this.transaction(async (registry) => {
329
+ const entry = this.owned(registry, current.projectId, current.repoRoot, current.generation);
330
+ entry.preparationSources = plan.sources;
331
+ entry.readiness = {
332
+ files: { status: WorktreeFileStatus.Attention, copied: 0, unchanged: 0, problems: [] },
333
+ editor: entry.editorResult ?? { status: WorktreeEditorStatus.Skipped },
334
+ };
335
+ entry.preparedPaths = [
336
+ ...new Set([...(entry.preparedPaths ?? []), ...plan.files.map((file) => file.path)]),
337
+ ];
338
+ await this.save(registry);
339
+ });
340
+ if (plan.problems.length) {
341
+ files = {
342
+ status: WorktreeFileStatus.Attention,
343
+ copied: 0,
344
+ unchanged: 0,
345
+ problems: plan.problems,
346
+ };
347
+ }
348
+ else if (!plan.files.length) {
349
+ files = { status: WorktreeFileStatus.NotNeeded, copied: 0, unchanged: 0, problems: [] };
350
+ }
351
+ else {
352
+ const stage = await this.transaction(async (registry) => {
353
+ const entry = this.owned(registry, current.projectId, current.repoRoot, current.generation);
354
+ await this.assertIdentity(entry);
355
+ const created = await createWorktreeStage(plan);
356
+ entry.preparationStage = {
357
+ directory: created.directory,
358
+ repoRoot: created.repoRoot,
359
+ count: created.count,
360
+ identity: created.identity,
361
+ };
362
+ await this.save(registry);
363
+ return created;
364
+ });
365
+ try {
366
+ await stageWorktreeFiles(stage);
367
+ files = await this.transaction(async (registry) => {
368
+ const entry = this.owned(registry, current.projectId, current.repoRoot, current.generation);
369
+ await this.assertIdentity(entry);
370
+ const result = await publishWorktreeFiles(stage);
371
+ try {
372
+ await discardStagedWorktreeFiles(stage);
373
+ }
374
+ catch {
375
+ result.status = WorktreeFileStatus.Attention;
376
+ result.problems.push({ path: '.', reason: WorktreeFileIssue.Failed });
377
+ }
378
+ entry.readiness = {
379
+ files: result,
380
+ editor: entry.editorResult ?? { status: WorktreeEditorStatus.Skipped },
381
+ };
382
+ if (!result.problems.some((item) => item.path === '.' && item.reason === WorktreeFileIssue.Failed))
383
+ delete entry.preparationStage;
384
+ await this.save(registry);
385
+ return result;
386
+ });
387
+ }
388
+ finally {
389
+ await discardStagedWorktreeFiles(stage);
390
+ }
391
+ }
392
+ }
393
+ catch {
394
+ files = {
395
+ status: WorktreeFileStatus.Attention,
396
+ copied: 0,
397
+ unchanged: 0,
398
+ problems: [{ path: '.', reason: WorktreeFileIssue.Failed }],
399
+ };
400
+ }
401
+ const launch = await this.transaction(async (registry) => {
402
+ const entry = this.owned(registry, current.projectId, current.repoRoot, current.generation);
403
+ const needed = entry.editorGeneration !== entry.generation;
404
+ if (needed) {
405
+ entry.editorGeneration = entry.generation;
406
+ entry.editorResult = {
407
+ status: WorktreeEditorStatus.Skipped,
408
+ detail: 'An editor launch was started; its visual result is not verified.',
409
+ };
410
+ }
411
+ entry.readiness = {
412
+ files,
413
+ editor: entry.editorResult ?? { status: WorktreeEditorStatus.Skipped },
414
+ };
415
+ await this.save(registry);
416
+ return needed;
417
+ });
418
+ if (launch) {
419
+ await this.assertOwnership(current.projectId, current.repoRoot, current.generation);
420
+ const editor = await openWorktreeInEditor(current.repoRoot);
421
+ await this.transaction(async (registry) => {
422
+ const entry = this.owned(registry, current.projectId, current.repoRoot, current.generation);
423
+ entry.editorResult = editor;
424
+ entry.readiness = { files, editor };
425
+ await this.save(registry);
426
+ });
427
+ }
428
+ return this.transaction(async (registry) => structuredClone(this.owned(registry, current.projectId, current.repoRoot, current.generation)));
429
+ });
430
+ }
263
431
  async list(projectId, policy) {
264
432
  return this.transaction(async (registry) => {
265
433
  const result = [];
@@ -465,19 +633,27 @@ export class WorktreePool {
465
633
  });
466
634
  }
467
635
  async forgetUnreadable(projectId, externalTaskId, generation) {
468
- return this.transaction(async (registry) => {
636
+ const candidate = await this.transaction(async (registry) => registry.projects[projectId]?.entries.find((entry) => entry.externalTaskId === externalTaskId && entry.generation === generation));
637
+ if (!candidate)
638
+ throw refuse('This is no longer the same registry entry for that task. List the checkouts again with worktree.list before forgetting one.');
639
+ return localMutex(key(this.stateRoot) + '/prepare/' + key(candidate.repoRoot), () => this.transaction(async (registry) => {
469
640
  const project = registry.projects[projectId];
470
641
  const entry = project?.entries.find((e) => e.externalTaskId === externalTaskId && e.generation === generation);
471
- if (!project || !entry)
642
+ if (!project || !entry || key(entry.repoRoot) !== key(candidate.repoRoot))
472
643
  throw refuse('This is no longer the same registry entry for that task. List the checkouts again with worktree.list before forgetting one.');
473
644
  if (await this.ownedElsewhere(entry))
474
645
  throw refuse('Another live client still owns this task. Stop or pause it there first; worktree.list shows the checkout meanwhile.');
475
646
  if (!(await this.unreadable(entry)))
476
647
  throw new BridgeRecoveryError('This checkout can still be read as a Git worktree, so it is not forgotten. Settle its delivery and free it with worktree.release.', 'worktree.release');
648
+ if (entry.preparationStage) {
649
+ if (entry.preparationStage.repoRoot !== entry.repoRoot)
650
+ throw refuse('Unknown staging owner; the task record is preserved.');
651
+ await discardStagedWorktreeFiles(entry.preparationStage);
652
+ }
477
653
  project.entries = project.entries.filter((e) => e !== entry);
478
654
  await this.save(registry);
479
655
  return structuredClone(entry);
480
- });
656
+ }));
481
657
  }
482
658
  async unreadable(entry) {
483
659
  if (!(await pathExists(entry.repoRoot)))
@@ -661,6 +837,8 @@ export class WorktreePool {
661
837
  repoFingerprint: input.repoFingerprint,
662
838
  commonDir,
663
839
  repoRoot,
840
+ sourceRepoRoot: await canonicalPath(input.repoRoot),
841
+ sourceMainRoot: await this.git.mainWorktree(input.repoRoot),
664
842
  slot,
665
843
  managed,
666
844
  externalTaskId: input.externalTaskId,