engineering-memory 1.11.12 → 1.11.14
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 +2 -2
- package/runtime/build.json +1 -1
- package/runtime/dist/src/mcp/onboarding-tools.js +23 -11
- package/runtime/dist/src/mcp/questionnaire-tools.js +15 -4
- package/runtime/dist/src/mcp/tool-definitions.js +7 -9
- package/runtime/dist/src/mcp/worktree-tools.js +35 -10
- package/runtime/dist/src/runtime/bridge-service.js +27 -10
- package/runtime/dist/src/runtime/worktree-pool.js +5 -2
- package/bin/entry-point.test.mjs +0 -29
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engineering-memory",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.14",
|
|
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",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"engineering-memory": "bin/engineering-memory.mjs"
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
|
-
"bin",
|
|
14
|
+
"bin/engineering-memory.mjs",
|
|
15
15
|
"dispatcher",
|
|
16
16
|
"lib",
|
|
17
17
|
"install",
|
package/runtime/build.json
CHANGED
|
@@ -2,7 +2,7 @@ import * as z from 'zod/v4';
|
|
|
2
2
|
import { sha256, stableStringify } from '../utilities/hash.js';
|
|
3
3
|
import { restrictedValueKind } from '../runtime/privacy-detector.js';
|
|
4
4
|
import { languageTag } from '../runtime/questionnaire-store.js';
|
|
5
|
-
import { answerChoice, answerData, askQuestionnaire, resumeQuestionnaire, } from './questionnaire-tools.js';
|
|
5
|
+
import { answerChoice, answerData, askFailed, askQuestionnaire, resumeQuestionnaire, } from './questionnaire-tools.js';
|
|
6
6
|
const uuid = z.string().uuid();
|
|
7
7
|
const hash = z.string().regex(/^[a-f0-9]{64}$/);
|
|
8
8
|
const project = { projectId: uuid };
|
|
@@ -411,18 +411,30 @@ export function registerOnboardingTools(server, service) {
|
|
|
411
411
|
],
|
|
412
412
|
};
|
|
413
413
|
});
|
|
414
|
-
const
|
|
415
|
-
|
|
414
|
+
const binding = { decision: questionnaireId };
|
|
415
|
+
const bound = definitions.map((entry) => ({ ...entry, binding }));
|
|
416
|
+
const asked = {
|
|
416
417
|
repoRoot: input.repoRoot,
|
|
418
|
+
questionnaireId,
|
|
417
419
|
presentation: input.presentation,
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
...(
|
|
424
|
-
|
|
425
|
-
|
|
420
|
+
};
|
|
421
|
+
const stored = await service.questionnaireResume(asked).catch(() => undefined);
|
|
422
|
+
const form = stored?.binding && stableStringify(stored.binding) === stableStringify(binding)
|
|
423
|
+
? await resumeQuestionnaire(server, service, asked, context)
|
|
424
|
+
: await askQuestionnaire(server, service, {
|
|
425
|
+
...bound.find((entry) => entry.language === language),
|
|
426
|
+
repoRoot: input.repoRoot,
|
|
427
|
+
presentation: input.presentation,
|
|
428
|
+
}, context, [...bound, ...definitions, ...previousWording], {
|
|
429
|
+
tool: 'project.move_repository',
|
|
430
|
+
...(input.decisionAttempt ? { decisionAttempt: input.decisionAttempt } : {}),
|
|
431
|
+
retryArguments: {
|
|
432
|
+
projectId: input.projectId,
|
|
433
|
+
...(input.language ? { language: input.language } : {}),
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
if (askFailed(form))
|
|
437
|
+
return form;
|
|
426
438
|
const record = await service.questionnaireResume({
|
|
427
439
|
repoRoot: input.repoRoot,
|
|
428
440
|
questionnaireId,
|
|
@@ -8,18 +8,29 @@ export async function askQuestionnaire(server, service, input, context, previous
|
|
|
8
8
|
return failure(error);
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
+
export function askFailed(result) {
|
|
12
|
+
return payload(result)?.ok === false;
|
|
13
|
+
}
|
|
11
14
|
export function answerChoice(result) {
|
|
12
15
|
return answerData(result)?.choice;
|
|
13
16
|
}
|
|
14
17
|
export function answerData(result) {
|
|
18
|
+
const value = payload(result);
|
|
19
|
+
return value?.ok && value.data?.status === 'answered' && value.data.answerAvailable
|
|
20
|
+
? value.data.answer
|
|
21
|
+
: undefined;
|
|
22
|
+
}
|
|
23
|
+
function payload(result) {
|
|
15
24
|
const response = result;
|
|
16
25
|
const text = response.content?.find((item) => item.type === 'text')?.text;
|
|
17
26
|
if (!text)
|
|
18
27
|
return undefined;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(text);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
23
34
|
}
|
|
24
35
|
export async function resumeQuestionnaire(server, service, input, context) {
|
|
25
36
|
try {
|
|
@@ -2,7 +2,7 @@ import * as z from 'zod/v4';
|
|
|
2
2
|
import { validationIds } from '../runtime/bridge-service.js';
|
|
3
3
|
import { stableStringify } from '../utilities/hash.js';
|
|
4
4
|
import { hostAnswerSchema, languageTag, questionnaireDefinitionSchema, } from '../runtime/questionnaire-store.js';
|
|
5
|
-
import { answerChoice, answerQuestionnaireFromHost, askQuestionnaire, resumeQuestionnaire, } from './questionnaire-tools.js';
|
|
5
|
+
import { answerChoice, answerQuestionnaireFromHost, askFailed, askQuestionnaire, resumeQuestionnaire, } from './questionnaire-tools.js';
|
|
6
6
|
const optionalRepoRoot = z.string().min(1).optional();
|
|
7
7
|
const stringList = z.array(z.string().min(1));
|
|
8
8
|
const jsonValue = z.lazy(() => z.union([
|
|
@@ -726,6 +726,8 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
726
726
|
...definitions.find((definition) => definition.language === language),
|
|
727
727
|
repoRoot: request.repoRoot,
|
|
728
728
|
}, context, [...definitions, ...earlier]);
|
|
729
|
+
if (askFailed(form))
|
|
730
|
+
return form;
|
|
729
731
|
const record = await service.questionnaireResume({
|
|
730
732
|
repoRoot: request.repoRoot,
|
|
731
733
|
questionnaireId,
|
|
@@ -930,15 +932,13 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
930
932
|
}),
|
|
931
933
|
}, async (input) => toolResult(await service.workItemCreate(input)));
|
|
932
934
|
server.registerTool('work_item.update', {
|
|
933
|
-
description:
|
|
935
|
+
description: "Edit or assign a work item at its current version. Assignees must already be active project members; this does not grant project access. Status is a slug from the project's own status catalogue; a backend that still keeps the six legacy values (backlog, ready, in_progress, in_review, done, cancelled) refuses anything else and names what it accepts.",
|
|
934
936
|
inputSchema: z.object({
|
|
935
937
|
...workItemLocator,
|
|
936
938
|
data: z.object({
|
|
937
939
|
...workItemFields,
|
|
938
940
|
expectedVersion: z.number().int().min(1),
|
|
939
|
-
status: z
|
|
940
|
-
.enum(['backlog', 'ready', 'in_progress', 'in_review', 'done', 'cancelled'])
|
|
941
|
-
.optional(),
|
|
941
|
+
status: z.string().trim().min(1).max(64).optional(),
|
|
942
942
|
}),
|
|
943
943
|
}),
|
|
944
944
|
}, async (input) => toolResult(await service.workItemUpdate(input)));
|
|
@@ -1016,12 +1016,10 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
1016
1016
|
}),
|
|
1017
1017
|
}, async (input) => toolResult(await service.projectUpdate(input)));
|
|
1018
1018
|
server.registerTool('work_item.list', {
|
|
1019
|
-
description:
|
|
1019
|
+
description: "List selectable work items for a project before opening an engineering run in this chat. The status filter is a slug from the project's own status catalogue; a backend that still keeps the six legacy values (backlog, ready, in_progress, in_review, done, cancelled) refuses anything else and names what it accepts.",
|
|
1020
1020
|
inputSchema: z.object({
|
|
1021
1021
|
projectId: z.string().uuid(),
|
|
1022
|
-
status: z
|
|
1023
|
-
.enum(['backlog', 'ready', 'in_progress', 'in_review', 'done', 'cancelled'])
|
|
1024
|
-
.optional(),
|
|
1022
|
+
status: z.string().trim().min(1).max(64).optional(),
|
|
1025
1023
|
priority: z.enum(['lowest', 'low', 'medium', 'high', 'highest']).optional(),
|
|
1026
1024
|
assigneeUserId: z.string().uuid().optional(),
|
|
1027
1025
|
includeArchived: z.boolean().optional(),
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
2
|
import * as z from 'zod/v4';
|
|
3
|
-
import { cancelledDeliveryQuestionId, forgetUnreadableQuestionId, } from '../runtime/bridge-service.js';
|
|
3
|
+
import { cancelledDeliveryQuestionId, forgetUnreadableQuestionId, retireQuestionId, } from '../runtime/bridge-service.js';
|
|
4
4
|
import { configuredBase, sourceLabel, taskStartChoice, taskStartDefinition, } from '../runtime/task-start.js';
|
|
5
5
|
import { sha256, stableStringify } from '../utilities/hash.js';
|
|
6
6
|
import { unreadableWorktree } from '../runtime/worktree-pool.js';
|
|
7
|
-
import { askQuestionnaire } from './questionnaire-tools.js';
|
|
7
|
+
import { askFailed, askQuestionnaire } from './questionnaire-tools.js';
|
|
8
8
|
import { languageTag } from '../runtime/questionnaire-store.js';
|
|
9
9
|
const repo = z.string().min(1);
|
|
10
10
|
const branch = z.string().min(1).max(255);
|
|
@@ -333,6 +333,8 @@ export function registerWorktreeTools(server, service) {
|
|
|
333
333
|
repoRoot: input.repoRoot,
|
|
334
334
|
presentation: input.presentation,
|
|
335
335
|
}, context, [definition(language !== 'tr'), previous(true), previous(false)], owner);
|
|
336
|
+
if (askFailed(form))
|
|
337
|
+
return form;
|
|
336
338
|
const record = await service.questionnaireResume({
|
|
337
339
|
repoRoot: input.repoRoot,
|
|
338
340
|
questionnaireId,
|
|
@@ -400,11 +402,15 @@ export function registerWorktreeTools(server, service) {
|
|
|
400
402
|
}),
|
|
401
403
|
};
|
|
402
404
|
const form = await askQuestionnaire(server, service, { ...definition, repoRoot: input.repoRoot, presentation: input.presentation }, context, [previous], owner);
|
|
403
|
-
const record = await service
|
|
405
|
+
const record = await service
|
|
406
|
+
.questionnaireResume({
|
|
404
407
|
repoRoot: input.repoRoot,
|
|
405
408
|
questionnaireId: definition.questionnaireId,
|
|
406
|
-
})
|
|
407
|
-
|
|
409
|
+
})
|
|
410
|
+
.catch(() => undefined);
|
|
411
|
+
const choice = record?.binding && stableStringify(record.binding) === stableStringify(definition.binding)
|
|
412
|
+
? taskStartChoice(record)
|
|
413
|
+
: null;
|
|
408
414
|
if (!choice)
|
|
409
415
|
return form;
|
|
410
416
|
if (choice.status === 'deferred')
|
|
@@ -490,6 +496,8 @@ export function registerWorktreeTools(server, service) {
|
|
|
490
496
|
repoRoot: input.repoRoot,
|
|
491
497
|
presentation: input.presentation,
|
|
492
498
|
}, context, [definition(language !== 'tr'), previousFallback(true), previousFallback(false)], owner);
|
|
499
|
+
if (askFailed(fallbackForm))
|
|
500
|
+
return fallbackForm;
|
|
493
501
|
const answer = await service.questionnaireResume({
|
|
494
502
|
repoRoot: input.repoRoot,
|
|
495
503
|
questionnaireId: fallbackId,
|
|
@@ -531,7 +539,7 @@ export function registerWorktreeTools(server, service) {
|
|
|
531
539
|
inputSchema: locationSchema,
|
|
532
540
|
}, async (input) => output(await service.worktreeOperation('list', input)));
|
|
533
541
|
server.registerTool('worktree.reconcile', {
|
|
534
|
-
description: 'Reconcile Git with the local registry, including unregistered legacy checkouts. Use releaseUnowned only on a selected unreserved recovered checkout after prior work has finished; a native source-bound confirmation and fresh safety checks precede reuse. Use retireLegacy only when the user wants old-location slots gone: each retired folder is removed with git worktree remove after its own native approval, and the branch stays. Use forgetUnreadable with the externalTaskId of a checkout that is gone or can no longer be read as a Git worktree, called from a working checkout of the same project: after its own native approval Engineering Memory stops tracking that entry, so it no longer counts toward the limit, and no file in the folder is read, moved or deleted. Otherwise never delete files or reset branches.',
|
|
542
|
+
description: 'Reconcile Git with the local registry, including unregistered legacy checkouts. Use releaseUnowned only on a selected unreserved recovered checkout after prior work has finished; a native source-bound confirmation and fresh safety checks precede reuse. Use retireLegacy only when the user wants old-location slots gone: each retired folder is removed with git worktree remove after its own native approval, once every retired folder has an answer, and the branch stays. Reconsider a retired folder with a new decisionAttempt; retries keep the same attempt. Use forgetUnreadable with the externalTaskId of a checkout that is gone or can no longer be read as a Git worktree, called from a working checkout of the same project: after its own native approval Engineering Memory stops tracking that entry, so it no longer counts toward the limit, and no file in the folder is read, moved or deleted. Otherwise never delete files or reset branches.',
|
|
535
543
|
inputSchema: z.strictObject({
|
|
536
544
|
repoRoot: repo,
|
|
537
545
|
releaseUnowned: z.boolean().optional(),
|
|
@@ -613,6 +621,8 @@ export function registerWorktreeTools(server, service) {
|
|
|
613
621
|
language: input.language,
|
|
614
622
|
}),
|
|
615
623
|
});
|
|
624
|
+
if (askFailed(form))
|
|
625
|
+
return form;
|
|
616
626
|
const decision = await service.questionnaireResume({
|
|
617
627
|
repoRoot: input.repoRoot,
|
|
618
628
|
questionnaireId,
|
|
@@ -634,9 +644,10 @@ export function registerWorktreeTools(server, service) {
|
|
|
634
644
|
const retired = [];
|
|
635
645
|
const kept = [];
|
|
636
646
|
const refused = [];
|
|
647
|
+
const approved = [];
|
|
637
648
|
for (const item of items) {
|
|
638
|
-
const questionnaireId = 'worktree-retire-' + sha256(item.repoRoot + item.generation);
|
|
639
649
|
const missing = item.reasons.includes('worktree directory missing');
|
|
650
|
+
const questionnaireId = retireQuestionId(item, !missing, input.decisionAttempt);
|
|
640
651
|
const folder = basename(item.repoRoot);
|
|
641
652
|
const definition = (turkish) => {
|
|
642
653
|
const copy = retireCopy[turkish ? 'tr' : 'en'];
|
|
@@ -677,20 +688,30 @@ export function registerWorktreeTools(server, service) {
|
|
|
677
688
|
],
|
|
678
689
|
});
|
|
679
690
|
const form = await askQuestionnaire(server, service, { ...definition(language === 'tr'), repoRoot: input.repoRoot }, context, [definition(language !== 'tr'), previous(true), previous(false)]);
|
|
691
|
+
if (askFailed(form)) {
|
|
692
|
+
refused.push({
|
|
693
|
+
repoRoot: item.repoRoot,
|
|
694
|
+
message: 'This folder was not asked about and nothing was removed: the decision stored under its question could not be opened.',
|
|
695
|
+
});
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
680
698
|
const decision = await service.questionnaireResume({
|
|
681
699
|
repoRoot: input.repoRoot,
|
|
682
700
|
questionnaireId,
|
|
683
701
|
});
|
|
684
702
|
if (decision.status !== 'answered')
|
|
685
703
|
return form;
|
|
686
|
-
if (decision.answer?.choice
|
|
704
|
+
if (decision.answer?.choice === 'retire')
|
|
705
|
+
approved.push({ item, questionnaireId });
|
|
706
|
+
else
|
|
687
707
|
kept.push(item.repoRoot);
|
|
688
|
-
|
|
689
|
-
|
|
708
|
+
}
|
|
709
|
+
for (const { item, questionnaireId } of approved) {
|
|
690
710
|
const result = await service.worktreeOperation('retire', {
|
|
691
711
|
repoRoot: input.repoRoot,
|
|
692
712
|
worktreePath: item.repoRoot,
|
|
693
713
|
generation: item.generation,
|
|
714
|
+
decisionAttempt: input.decisionAttempt,
|
|
694
715
|
decisionId: questionnaireId,
|
|
695
716
|
});
|
|
696
717
|
if (result.ok)
|
|
@@ -753,6 +774,8 @@ export function registerWorktreeTools(server, service) {
|
|
|
753
774
|
],
|
|
754
775
|
});
|
|
755
776
|
const form = await askQuestionnaire(server, service, { ...definition(language === 'tr'), repoRoot: input.repoRoot }, context, [definition(language !== 'tr'), previous(true), previous(false)]);
|
|
777
|
+
if (askFailed(form))
|
|
778
|
+
return form;
|
|
756
779
|
const decision = await service.questionnaireResume({
|
|
757
780
|
repoRoot: input.repoRoot,
|
|
758
781
|
questionnaireId,
|
|
@@ -857,6 +880,8 @@ export function registerWorktreeTools(server, service) {
|
|
|
857
880
|
],
|
|
858
881
|
});
|
|
859
882
|
const form = await askQuestionnaire(server, service, { ...definition(language === 'tr'), repoRoot: input.repoRoot }, context, [definition(language !== 'tr'), previous(true), previous(false)]);
|
|
883
|
+
if (askFailed(form))
|
|
884
|
+
return form;
|
|
860
885
|
const decision = await service.questionnaireResume({
|
|
861
886
|
repoRoot: input.repoRoot,
|
|
862
887
|
questionnaireId,
|
|
@@ -6,7 +6,7 @@ import { lstat, readFile } from 'node:fs/promises';
|
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
7
|
import { SourceSnapshot } from '../git/source-snapshot.js';
|
|
8
8
|
import { ProjectIntake } from '../project/project-intake.js';
|
|
9
|
-
import { canonicalPath } from '../utilities/files.js';
|
|
9
|
+
import { canonicalPath, pathExists } from '../utilities/files.js';
|
|
10
10
|
import { minimatch } from 'minimatch';
|
|
11
11
|
import { endpoints } from '../config.js';
|
|
12
12
|
import { startPhaseTimer } from './phase-timer.js';
|
|
@@ -2444,9 +2444,10 @@ export class BridgeService {
|
|
|
2444
2444
|
}
|
|
2445
2445
|
if (operation === 'retire') {
|
|
2446
2446
|
const entry = input.worktreePath ? await pool.forPath(projectId, input.worktreePath) : null;
|
|
2447
|
+
const folderPresent = entry ? await pathExists(entry.repoRoot) : false;
|
|
2447
2448
|
if (!entry ||
|
|
2448
2449
|
entry.generation !== input.generation ||
|
|
2449
|
-
input.decisionId !==
|
|
2450
|
+
input.decisionId !== retireQuestionId(entry, folderPresent, input.decisionAttempt ?? 0))
|
|
2450
2451
|
throw refuse('The retired folder changed after its removal was asked about. List the worktrees again.', 'worktree.list');
|
|
2451
2452
|
const decision = await this.questionnaireResume({
|
|
2452
2453
|
repoRoot: repository.repoRoot,
|
|
@@ -2454,7 +2455,7 @@ export class BridgeService {
|
|
|
2454
2455
|
});
|
|
2455
2456
|
if (decision.status !== 'answered' || decision.answer?.choice !== 'retire')
|
|
2456
2457
|
throw refuse('A retired folder is removed only after the user approves it in the native worktree.reconcile form.', 'worktree.reconcile');
|
|
2457
|
-
await pool.retire(projectId, repository.repoRoot, entry.repoRoot, entry.generation);
|
|
2458
|
+
await pool.retire(projectId, repository.repoRoot, entry.repoRoot, entry.generation, folderPresent);
|
|
2458
2459
|
return asJsonValue({ operation, complete: true, removed: entry.repoRoot });
|
|
2459
2460
|
}
|
|
2460
2461
|
const entry = await pool.forPath(projectId, repository.repoRoot);
|
|
@@ -3544,21 +3545,30 @@ export class BridgeService {
|
|
|
3544
3545
|
}
|
|
3545
3546
|
}
|
|
3546
3547
|
async actionableWorkItems(projectId) {
|
|
3548
|
+
const path = `${endpoints.workItemList(projectId)}?limit=100`;
|
|
3547
3549
|
try {
|
|
3548
|
-
const
|
|
3549
|
-
|
|
3550
|
-
|
|
3550
|
+
const chosen = await this.workItemPage(`${path}&actionable=true`).catch((error) => {
|
|
3551
|
+
if (error instanceof ApiResponseError && error.httpStatus === 400)
|
|
3552
|
+
return null;
|
|
3553
|
+
throw error;
|
|
3554
|
+
});
|
|
3555
|
+
if (chosen && chosen.length > 0)
|
|
3556
|
+
return chosen;
|
|
3557
|
+
const every = await this.workItemPage(path);
|
|
3558
|
+
if (chosen && every.some((entry) => objectValue(objectValue(entry)?.projectStatus) !== null))
|
|
3551
3559
|
return [];
|
|
3552
3560
|
const actionable = new Set(['backlog', 'ready', 'in_progress', 'in_review']);
|
|
3553
|
-
return
|
|
3554
|
-
const item = objectValue(entry);
|
|
3555
|
-
return item ? actionable.has(String(item.status)) : false;
|
|
3556
|
-
});
|
|
3561
|
+
return every.filter((entry) => actionable.has(String(objectValue(entry)?.status)));
|
|
3557
3562
|
}
|
|
3558
3563
|
catch {
|
|
3559
3564
|
return [];
|
|
3560
3565
|
}
|
|
3561
3566
|
}
|
|
3567
|
+
async workItemPage(path) {
|
|
3568
|
+
const response = await this.dependencies.client.request(path);
|
|
3569
|
+
const items = objectValue(response.data)?.items;
|
|
3570
|
+
return Array.isArray(items) ? items : [];
|
|
3571
|
+
}
|
|
3562
3572
|
async clientUpdate(authenticated) {
|
|
3563
3573
|
const installed = this.dependencies.clientVersion;
|
|
3564
3574
|
if (!authenticated) {
|
|
@@ -5057,6 +5067,13 @@ export function cancelledDeliveryQuestionId(entry, clean) {
|
|
|
5057
5067
|
return ('worktree-delivery-cancel-' +
|
|
5058
5068
|
sha256(entry.generation + ':' + (clean ? 'clean' : entry.managed ? 'modified' : 'own-folder')));
|
|
5059
5069
|
}
|
|
5070
|
+
export function retireQuestionId(entry, folderPresent, decisionAttempt) {
|
|
5071
|
+
return ('worktree-retire-' +
|
|
5072
|
+
sha256(entry.repoRoot +
|
|
5073
|
+
entry.generation +
|
|
5074
|
+
(folderPresent ? '' : ':record') +
|
|
5075
|
+
(decisionAttempt ? ':' + decisionAttempt : '')));
|
|
5076
|
+
}
|
|
5060
5077
|
function refuse(message, recovery, details) {
|
|
5061
5078
|
return new BridgeRecoveryError(message, recovery, details);
|
|
5062
5079
|
}
|
|
@@ -429,7 +429,7 @@ export class WorktreePool {
|
|
|
429
429
|
await this.save(registry);
|
|
430
430
|
});
|
|
431
431
|
}
|
|
432
|
-
async retire(projectId, repoRoot, path, generation) {
|
|
432
|
+
async retire(projectId, repoRoot, path, generation, folderPresent) {
|
|
433
433
|
const commonDir = await this.commonDirectory(repoRoot);
|
|
434
434
|
const activeRoot = key(await this.documentsDirectory());
|
|
435
435
|
const legacyRoot = await this.legacyDocumentsDirectory();
|
|
@@ -443,7 +443,10 @@ export class WorktreePool {
|
|
|
443
443
|
entry.commonDir !== commonDir ||
|
|
444
444
|
!this.outsideActiveRoot(entry, activeRoot, legacyRoot))
|
|
445
445
|
throw refuse('This folder is no longer the same retired slot. List the worktrees again before removing it.');
|
|
446
|
-
|
|
446
|
+
const present = await pathExists(entry.repoRoot);
|
|
447
|
+
if (present !== folderPresent)
|
|
448
|
+
throw refuse('The retired folder changed after its removal was asked about. List the worktrees again.');
|
|
449
|
+
if (present) {
|
|
447
450
|
const reasons = await this.fileProtection(entry);
|
|
448
451
|
if (reasons.length)
|
|
449
452
|
throw refuse('The retired folder is protected: ' +
|
package/bin/entry-point.test.mjs
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import test from 'node:test';
|
|
4
|
-
|
|
5
|
-
const entryPoint = new URL('./engineering-memory.mjs', import.meta.url);
|
|
6
|
-
const manifest = new URL('../package.json', import.meta.url);
|
|
7
|
-
|
|
8
|
-
test('the published entry point tells the installer which version it is', async () => {
|
|
9
|
-
const source = await readFile(entryPoint, 'utf8');
|
|
10
|
-
|
|
11
|
-
assert.match(
|
|
12
|
-
source,
|
|
13
|
-
/'--client-version',\s*await publishedVersion\(\)/,
|
|
14
|
-
'The installer is never told the version, so every install reports itself as unknown and no update is ever offered',
|
|
15
|
-
);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test('the published entry point tells the installer which backend it was built for', async () => {
|
|
19
|
-
const source = await readFile(entryPoint, 'utf8');
|
|
20
|
-
|
|
21
|
-
assert.match(source, /'--api-url',\s*await publishedApiUrl\(\)/);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test('the manifest carries a version the entry point can stamp', async () => {
|
|
25
|
-
const declared = JSON.parse(await readFile(manifest, 'utf8'));
|
|
26
|
-
|
|
27
|
-
assert.match(String(declared.version), /^\d+\.\d+\.\d+$/);
|
|
28
|
-
assert.equal(declared.name, 'engineering-memory');
|
|
29
|
-
});
|