engineering-memory 1.11.15 → 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.
- package/package.json +1 -1
- package/runtime/build.json +1 -1
- package/runtime/dist/src/runtime/bridge-service.js +5 -5
- package/runtime/dist/src/runtime/worktree-editor.js +184 -0
- package/runtime/dist/src/runtime/worktree-gradle.js +318 -0
- package/runtime/dist/src/runtime/worktree-pool.js +181 -3
- package/runtime/dist/src/runtime/worktree-preparation.js +901 -0
- package/runtime/dist/src/runtime/worktree-readiness-types.js +18 -0
- package/skill/references/lifecycle.md +2 -0
|
@@ -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
|
-
|
|
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,
|