engineering-memory 1.11.4 → 1.11.5
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/dispatcher/sections.mjs +2 -0
- package/package.json +1 -1
- package/runtime/dist/src/config.js +2 -0
- package/runtime/dist/src/git/git-inspector.js +41 -2
- package/runtime/dist/src/git/verification-gate.js +40 -1
- package/runtime/dist/src/index.js +15 -1
- package/runtime/dist/src/mcp/onboarding-tools.js +39 -1
- package/runtime/dist/src/mcp/server.js +2 -0
- package/runtime/dist/src/mcp/tool-definitions.js +13 -10
- package/runtime/dist/src/mcp/worktree-tools.js +392 -0
- package/runtime/dist/src/project/project-intake.js +9 -2
- package/runtime/dist/src/runtime/api-client.js +4 -0
- package/runtime/dist/src/runtime/branch-preferences.js +53 -0
- package/runtime/dist/src/runtime/bridge-service.js +377 -13
- package/runtime/dist/src/runtime/task-branch-store.js +1 -1
- package/runtime/dist/src/runtime/worktree-policy.js +37 -0
- package/runtime/dist/src/runtime/worktree-pool.js +847 -0
- package/runtime/dist/src/utilities/local-mutex.js +37 -0
- package/skill/SKILL.md +1 -1
- package/skill/references/lifecycle.md +12 -4
- package/skill/references/project-onboarding.md +4 -0
- package/skill/references/questionnaires.md +8 -0
package/dispatcher/sections.mjs
CHANGED
|
@@ -19,6 +19,8 @@ Never answer a question about a bound repository from the working tree alone. Wh
|
|
|
19
19
|
|
|
20
20
|
After compaction, a new chat, interruption, or handoff, call \`session.resume\` before continuing.
|
|
21
21
|
|
|
22
|
+
Before a new write/scaffold task, use \`task.branch\` for native base selection and automatic worktree allocation. Run all commands from its returned \`repoRoot\`. Renew \`task.heartbeat\` during actual work, use \`task.pause\` on handoff, and resume before writing again. Inactivity never permits force checkout or cleanup. Keep closed worktrees until authorized delivery completes. Ask only unanswered shared branch preferences when \`session.entry\` says the user can manage them; otherwise choose a task-specific base.
|
|
23
|
+
|
|
22
24
|
Do not edit until the skill lifecycle has completed discovery, its checkpoint, and \`context.prepare_change\`. Do not claim completion until \`task.verify\` succeeds.
|
|
23
25
|
|
|
24
26
|
Do not block independent task work on \`memory.propose_revision\` drafting, submission or approval. Follow the skill's memory-updates reference: use background agents or concurrent tools when the host permits them, and continue useful work instead of immediately waiting. Without concurrency, checkpoint the pending draft and defer submission until needed. Collect the result only at the operation that depends on it; native approval and required task verification still apply.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engineering-memory",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.5",
|
|
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",
|
|
@@ -26,6 +26,8 @@ export function apiStateRoot(config) {
|
|
|
26
26
|
return join(config.stateRoot, 'origins', apiNamespaceKey(config.apiBaseUrl));
|
|
27
27
|
}
|
|
28
28
|
export const endpoints = {
|
|
29
|
+
projectGitPreferences: (projectId) => '/projects/' + projectId + '/git-preferences',
|
|
30
|
+
worktreePolicy: '/runtime/worktree-policy',
|
|
29
31
|
auditList: '/audit',
|
|
30
32
|
authRefresh: '/auth/refresh',
|
|
31
33
|
authLogout: '/auth/logout',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { lstat, readFile } from 'node:fs/promises';
|
|
2
3
|
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
4
|
import { assertManagedPath, canonicalPath } from '../utilities/files.js';
|
|
@@ -44,7 +45,7 @@ export class GitInspector {
|
|
|
44
45
|
if (name.length > 255 || result.exitCode !== 0 || result.stdout.trim() !== name)
|
|
45
46
|
throw new Error('Choose a valid literal Git branch name of at most 255 characters');
|
|
46
47
|
}
|
|
47
|
-
async createWorktree(repoRoot, name, target) {
|
|
48
|
+
async createWorktree(repoRoot, name, target, baseCommit) {
|
|
48
49
|
await this.validateBranch(repoRoot, name);
|
|
49
50
|
const path = resolve(repoRoot, target);
|
|
50
51
|
const present = await lstat(path).then(() => true, (error) => {
|
|
@@ -73,12 +74,50 @@ export class GitInspector {
|
|
|
73
74
|
const exists = await this.branchExists(repoRoot, name);
|
|
74
75
|
const args = exists
|
|
75
76
|
? ['worktree', 'add', '--', path, name]
|
|
76
|
-
: [
|
|
77
|
+
: [
|
|
78
|
+
'worktree',
|
|
79
|
+
'add',
|
|
80
|
+
'--no-track',
|
|
81
|
+
'-b',
|
|
82
|
+
name,
|
|
83
|
+
'--',
|
|
84
|
+
path,
|
|
85
|
+
...(baseCommit ? [baseCommit] : []),
|
|
86
|
+
];
|
|
77
87
|
const result = await this.runner.run('git', args, { cwd: repoRoot });
|
|
78
88
|
if (result.exitCode !== 0)
|
|
79
89
|
throw new Error(`Git could not create the worktree: ${result.stderr.trim()}`);
|
|
80
90
|
return await this.findRoot(path);
|
|
81
91
|
}
|
|
92
|
+
async fetchBranchCommit(repoRoot, remote, branch) {
|
|
93
|
+
await this.validateBranch(repoRoot, branch);
|
|
94
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/.test(remote))
|
|
95
|
+
throw new Error('Select a configured remote name, not a URL or credential');
|
|
96
|
+
const remotes = await this.gitValue(repoRoot, ['remote']);
|
|
97
|
+
if (!remotes?.split(/\r?\n/).includes(remote))
|
|
98
|
+
throw new Error('The selected remote is unavailable in this clone');
|
|
99
|
+
const ref = 'refs/engineering-memory/fetched-bases/' + randomUUID();
|
|
100
|
+
try {
|
|
101
|
+
const result = await this.runner.run('git', [
|
|
102
|
+
'fetch',
|
|
103
|
+
'--no-tags',
|
|
104
|
+
'--no-write-fetch-head',
|
|
105
|
+
'--refmap=',
|
|
106
|
+
'--',
|
|
107
|
+
remote,
|
|
108
|
+
'refs/heads/' + branch + ':' + ref,
|
|
109
|
+
], { cwd: repoRoot });
|
|
110
|
+
if (result.exitCode !== 0)
|
|
111
|
+
throw new Error('The remote branch could not be fetched. Ask whether to use an explicitly selected local commit.');
|
|
112
|
+
const source = await this.sourceIdentity(repoRoot, ref);
|
|
113
|
+
if (!source)
|
|
114
|
+
throw new Error('The fetched branch has no commit');
|
|
115
|
+
return source;
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
await this.runner.run('git', ['update-ref', '-d', ref], { cwd: repoRoot });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
82
121
|
async currentBranch(repoRoot) {
|
|
83
122
|
return await this.gitValue(repoRoot, ['symbolic-ref', '--short', '-q', 'HEAD']);
|
|
84
123
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { WorktreePool } from '../runtime/worktree-pool.js';
|
|
1
2
|
import { sha256 } from '../utilities/hash.js';
|
|
2
3
|
import { readdir } from 'node:fs/promises';
|
|
3
4
|
import { join } from 'node:path';
|
|
@@ -8,12 +9,14 @@ export class VerificationGate {
|
|
|
8
9
|
outbox;
|
|
9
10
|
commitGateAttestor;
|
|
10
11
|
root;
|
|
12
|
+
pool;
|
|
11
13
|
repositories;
|
|
12
|
-
constructor(stateRoot, git, outbox, commitGateAttestor) {
|
|
14
|
+
constructor(stateRoot, git, outbox, commitGateAttestor, pool) {
|
|
13
15
|
this.git = git;
|
|
14
16
|
this.outbox = outbox;
|
|
15
17
|
this.commitGateAttestor = commitGateAttestor;
|
|
16
18
|
this.root = join(stateRoot, 'verification-gates');
|
|
19
|
+
this.pool = pool ?? new WorktreePool(stateRoot);
|
|
17
20
|
this.repositories = new RepositoryResolver(git, 2, stateRoot);
|
|
18
21
|
}
|
|
19
22
|
async record(input) {
|
|
@@ -21,6 +24,9 @@ export class VerificationGate {
|
|
|
21
24
|
(!Number.isSafeInteger(input.taskVersion) || (input.taskVersion ?? 0) < 1)) {
|
|
22
25
|
throw new Error('Closed task verification receipt requires its backend task version');
|
|
23
26
|
}
|
|
27
|
+
const allocation = input.projectId
|
|
28
|
+
? await this.pool.forPath(input.projectId, input.repoRoot)
|
|
29
|
+
: null;
|
|
24
30
|
const verifiedAt = new Date();
|
|
25
31
|
const taskChanges = await this.git.taskManifest(input.repoRoot, input.taskChanges);
|
|
26
32
|
const worktreeId = worktreeIdentity(await this.git.findRoot(input.repoRoot));
|
|
@@ -28,6 +34,7 @@ export class VerificationGate {
|
|
|
28
34
|
schemaVersion: 5,
|
|
29
35
|
source: input.source,
|
|
30
36
|
worktreeId,
|
|
37
|
+
...(allocation ? { worktreeGeneration: allocation.generation } : {}),
|
|
31
38
|
taskId: input.taskId,
|
|
32
39
|
projectId: input.projectId,
|
|
33
40
|
repoFingerprint: input.repoFingerprint,
|
|
@@ -42,6 +49,23 @@ export class VerificationGate {
|
|
|
42
49
|
branch: input.branch ?? null,
|
|
43
50
|
}, this.root);
|
|
44
51
|
}
|
|
52
|
+
async rebindWorktreeOwnership(repoRoot, taskId, previous, generation) {
|
|
53
|
+
const fingerprint = await this.git.fingerprint(repoRoot);
|
|
54
|
+
const identity = worktreeIdentity(await this.git.findRoot(repoRoot));
|
|
55
|
+
const path = this.pathFor(fingerprint, identity, taskId);
|
|
56
|
+
const receipt = await readJson(path, this.root);
|
|
57
|
+
if (!receipt || receipt.taskId !== taskId || receipt.worktreeGeneration !== previous)
|
|
58
|
+
return;
|
|
59
|
+
if (!receipt.projectId)
|
|
60
|
+
return;
|
|
61
|
+
const allocation = await this.pool.forPath(receipt.projectId, repoRoot);
|
|
62
|
+
if (!allocation ||
|
|
63
|
+
allocation.taskId !== taskId ||
|
|
64
|
+
allocation.generation !== generation ||
|
|
65
|
+
allocation.branch !== receipt.branch)
|
|
66
|
+
throw new Error('Resumed ownership does not match the verification receipt');
|
|
67
|
+
await writeJson(path, { ...receipt, worktreeGeneration: generation }, this.root);
|
|
68
|
+
}
|
|
45
69
|
async verify(repoRoot) {
|
|
46
70
|
const [repository, stagedChanges, scaffolding] = await Promise.all([
|
|
47
71
|
this.repositories.resolve(repoRoot),
|
|
@@ -73,6 +97,21 @@ export class VerificationGate {
|
|
|
73
97
|
if (!receipt) {
|
|
74
98
|
return { allowed: false, reason: 'task_verify_required', diffHash: manifest.diffHash };
|
|
75
99
|
}
|
|
100
|
+
try {
|
|
101
|
+
if (receipt.projectId)
|
|
102
|
+
await this.pool.validateCommit(receipt.projectId, receipt.taskId, repository.repoRoot, receipt.worktreeGeneration);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return {
|
|
106
|
+
allowed: false,
|
|
107
|
+
reason: 'worktree_ownership_changed',
|
|
108
|
+
diffHash: manifest.diffHash,
|
|
109
|
+
taskId: receipt.taskId,
|
|
110
|
+
details: [
|
|
111
|
+
'Resume the task in its current owned worktree; stale verification cannot authorize another allocation.',
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
76
115
|
if ((await this.outbox.listForTask(receipt.taskId)).length > 0) {
|
|
77
116
|
return {
|
|
78
117
|
allowed: false,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { WorktreePool } from './runtime/worktree-pool.js';
|
|
3
|
+
import { WorktreePolicyCache } from './runtime/worktree-policy.js';
|
|
2
4
|
import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
3
5
|
import { BrowserAuthCoordinator } from './auth/browser-auth.js';
|
|
4
6
|
import { createCredentialStore } from './auth/credential-store.js';
|
|
@@ -47,9 +49,21 @@ export function createBridgeService() {
|
|
|
47
49
|
const repositoryDecisions = new RepositoryDecisionStore(stateRoot);
|
|
48
50
|
const updateChoices = new UpdateChoiceStore(stateRoot);
|
|
49
51
|
const shadowNotices = new ShadowNoticeStore(stateRoot);
|
|
50
|
-
const
|
|
52
|
+
const worktreePool = new WorktreePool(stateRoot, {
|
|
53
|
+
pendingWork: async (entry) => {
|
|
54
|
+
if (!entry.taskId)
|
|
55
|
+
return false;
|
|
56
|
+
const pointer = await activeContexts.loadForTask(entry.repoFingerprint, entry.taskId);
|
|
57
|
+
return Boolean(pointer?.verificationIntent ||
|
|
58
|
+
pointer?.closeIntent ||
|
|
59
|
+
(await outbox.listForTask(entry.taskId)).length);
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
const gate = new VerificationGate(stateRoot, git, outbox, undefined, worktreePool);
|
|
51
63
|
const principalState = new PrincipalStateGuard(stateRoot, credentials, cache, outbox, activeContexts, gate);
|
|
52
64
|
return new BridgeService({
|
|
65
|
+
worktreePool,
|
|
66
|
+
worktreePolicy: new WorktreePolicyCache(stateRoot),
|
|
53
67
|
client,
|
|
54
68
|
credentials,
|
|
55
69
|
browserAuth: new BrowserAuthCoordinator(client, credentials),
|
|
@@ -656,9 +656,41 @@ async function approveOnboarding(server, service, input, context, initialize) {
|
|
|
656
656
|
processedFields.add(field);
|
|
657
657
|
fields = profileFields(effectiveProfile);
|
|
658
658
|
}
|
|
659
|
+
let gitPreferences;
|
|
660
|
+
if (preparation && service.managesWorktrees()) {
|
|
661
|
+
gitPreferences = {};
|
|
662
|
+
for (const role of ['development', 'production', 'test']) {
|
|
663
|
+
const response = await askQuestionnaire(server, service, {
|
|
664
|
+
...scope,
|
|
665
|
+
questionnaireId: 'onboard-git-' + role + '-' + sha256(input.requestKey),
|
|
666
|
+
message: role === 'development'
|
|
667
|
+
? 'Which development branch should new task branches start from? For a new repository, choose its planned initial branch name. This choice uses the origin remote after it is connected.'
|
|
668
|
+
: 'Which ' +
|
|
669
|
+
role +
|
|
670
|
+
' branch does this project use? Choose explicitly none if there is no such branch yet; you will not be asked again.',
|
|
671
|
+
options: [
|
|
672
|
+
{ id: 'set', label: 'Use the branch named below' },
|
|
673
|
+
...(role === 'development'
|
|
674
|
+
? [{ id: 'defer', label: 'Do not start setup yet' }]
|
|
675
|
+
: [{ id: 'none', label: 'No ' + role + ' branch yet' }]),
|
|
676
|
+
],
|
|
677
|
+
textField: { title: 'Branch name', maxLength: 255, requiredForChoice: 'set' },
|
|
678
|
+
allowFreeText: false,
|
|
679
|
+
}, context);
|
|
680
|
+
const answer = answerData(response);
|
|
681
|
+
if (answer?.choice !== 'none' && (answer?.choice !== 'set' || !answer.text))
|
|
682
|
+
return response;
|
|
683
|
+
gitPreferences[role] = {
|
|
684
|
+
answered: true,
|
|
685
|
+
branch: answer.choice === 'none' ? null : answer.text,
|
|
686
|
+
remote: answer.choice === 'none' ? null : 'origin',
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
}
|
|
659
690
|
const choice = {
|
|
660
691
|
requestKey: input.requestKey,
|
|
661
692
|
mode: input.mode,
|
|
693
|
+
...(gitPreferences ? { gitPreferences } : {}),
|
|
662
694
|
profile: body(onboardingInput.parse({ ...input, profile: effectiveProfile }).profile),
|
|
663
695
|
...(input.reworkBehavior ? { reworkBehavior: input.reworkBehavior } : {}),
|
|
664
696
|
};
|
|
@@ -698,7 +730,13 @@ async function approveOnboarding(server, service, input, context, initialize) {
|
|
|
698
730
|
});
|
|
699
731
|
if (!saved.ok || !input.projectId)
|
|
700
732
|
return output(saved);
|
|
701
|
-
return output(await service.onboardingRequest('onboarding_choose', body({
|
|
733
|
+
return output(await service.onboardingRequest('onboarding_choose', body({
|
|
734
|
+
projectId: input.projectId,
|
|
735
|
+
requestKey: choice.requestKey,
|
|
736
|
+
mode: choice.mode,
|
|
737
|
+
profile: choice.profile,
|
|
738
|
+
reworkBehavior: choice.reworkBehavior,
|
|
739
|
+
})));
|
|
702
740
|
}
|
|
703
741
|
function deferredProfile(requestKey, field) {
|
|
704
742
|
return output({
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { registerWorktreeTools } from './worktree-tools.js';
|
|
1
2
|
import { McpServer } from '@modelcontextprotocol/server';
|
|
2
3
|
import { registerOnboardingTools } from './onboarding-tools.js';
|
|
3
4
|
import { registerEngineeringMemoryTools, registerQuestionnaireTools } from './tool-definitions.js';
|
|
@@ -11,6 +12,7 @@ export function createEngineeringMemoryServer(service) {
|
|
|
11
12
|
registerEngineeringMemoryTools(server, service);
|
|
12
13
|
registerQuestionnaireTools(server, service);
|
|
13
14
|
registerOnboardingTools(server, service);
|
|
15
|
+
registerWorktreeTools(server, service);
|
|
14
16
|
return server;
|
|
15
17
|
}
|
|
16
18
|
//# sourceMappingURL=server.js.map
|
|
@@ -46,6 +46,15 @@ const checkpointBase = {
|
|
|
46
46
|
idempotencyKey: z.string().uuid().optional(),
|
|
47
47
|
};
|
|
48
48
|
export const engineeringMemoryToolNames = [
|
|
49
|
+
'project.git_preferences',
|
|
50
|
+
'project.set_git_preferences',
|
|
51
|
+
'worktree.policy',
|
|
52
|
+
'worktree.set_policy',
|
|
53
|
+
'worktree.list',
|
|
54
|
+
'worktree.reconcile',
|
|
55
|
+
'task.heartbeat',
|
|
56
|
+
'task.pause',
|
|
57
|
+
'worktree.release',
|
|
49
58
|
'audit.list',
|
|
50
59
|
'questionnaire.ask',
|
|
51
60
|
'questionnaire.resume',
|
|
@@ -524,16 +533,6 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
524
533
|
reason: z.string().min(1).max(2000),
|
|
525
534
|
}),
|
|
526
535
|
}, async (input) => toolResult(await service.taskAbandon(input)));
|
|
527
|
-
server.registerTool('task.branch', {
|
|
528
|
-
description: 'Record the native questionnaire answer for the exact externalTaskId before any new write/scaffold task or read-only-to-write transition. Choose name or keepCurrent. For another task in a busy checkout supply worktreePath and name; use the returned repoRoot for all subsequent commands. Existing branches may be selected without resetting them. Decisions persist across retries and restarts.',
|
|
529
|
-
inputSchema: z.object({
|
|
530
|
-
repoRoot: optionalRepoRoot,
|
|
531
|
-
externalTaskId: z.string().min(2).max(160),
|
|
532
|
-
worktreePath: z.string().min(1).optional(),
|
|
533
|
-
name: z.string().min(1).max(255).optional(),
|
|
534
|
-
keepCurrent: z.boolean().optional(),
|
|
535
|
-
}),
|
|
536
|
-
}, async (input) => toolResult(await service.taskBranch(input)));
|
|
537
536
|
server.registerTool('organization.create', {
|
|
538
537
|
description: 'Create an organization the user named, with the short identifier they typed. Offer this underneath the organizations they already belong to; never invent the identifier from the name.',
|
|
539
538
|
inputSchema: z.object({
|
|
@@ -567,6 +566,10 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
567
566
|
description: 'Create a policy-ready backend project from the native questionnaire and local repository audit, then store its binding in user-local state.',
|
|
568
567
|
inputSchema: z.object({
|
|
569
568
|
repoRoot: optionalRepoRoot,
|
|
569
|
+
onboardingRequestKey: z
|
|
570
|
+
.string()
|
|
571
|
+
.regex(/^[A-Za-z0-9_-]{1,80}$/)
|
|
572
|
+
.optional(),
|
|
570
573
|
organizationId: z.string().uuid(),
|
|
571
574
|
projectName: z.string().min(2),
|
|
572
575
|
projectSlug: z.string().min(2).optional(),
|