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.
@@ -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.4",
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
- : ['worktree', 'add', '-b', name, '--', path];
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 gate = new VerificationGate(stateRoot, git, outbox);
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({ projectId: input.projectId, ...choice })));
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(),