engineering-memory 1.11.24 → 1.11.25

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engineering-memory",
3
- "version": "1.11.24",
3
+ "version": "1.11.25",
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",
@@ -1,3 +1,3 @@
1
1
  {
2
- "gitHead": "8d3dfecc2ad6ed7593bb48fa0dbed384acac1a64"
2
+ "gitHead": "487a37fd63becd1520c4e1f5e4ba13ed2de828e2"
3
3
  }
@@ -10,8 +10,10 @@ export const temporaryScaffoldingMarker = 'ENGINEERING-MEMORY-TEMPORARY';
10
10
  export const temporaryScaffoldingPattern = `${temporaryScaffoldingMarker}:`;
11
11
  export class GitInspector {
12
12
  runner;
13
- constructor(runner = new NativeCommandRunner()) {
13
+ knownReservations;
14
+ constructor(runner = new NativeCommandRunner(), knownReservations) {
14
15
  this.runner = runner;
16
+ this.knownReservations = knownReservations;
15
17
  }
16
18
  async findRoot(startPath) {
17
19
  const result = await this.runner.run('git', ['rev-parse', '--show-toplevel'], {
@@ -49,7 +51,7 @@ export class GitInspector {
49
51
  : null;
50
52
  }
51
53
  branchStore(repoRoot) {
52
- return new TaskBranchStore(repoRoot, this.runner);
54
+ return new TaskBranchStore(repoRoot, this.runner, this.knownReservations);
53
55
  }
54
56
  async validateBranch(repoRoot, name) {
55
57
  const result = await this.runner.run('git', ['check-ref-format', '--branch', name], {
@@ -46,9 +46,20 @@ export function createBridgeService(options = {}) {
46
46
  liveHeader: (path) => live.header(path),
47
47
  });
48
48
  const live = new LiveSignals(client);
49
- const git = new GitInspector();
50
- const outbox = new OfflineOutbox(stateRoot);
51
49
  const activeContexts = new ActiveContextStore(stateRoot);
50
+ const knownReservations = async (repoRoot) => (await activeContexts.list((await repositories.resolveIdentity(repoRoot)).repoFingerprint)).flatMap((pointer) => pointer.branch === undefined
51
+ ? []
52
+ : [
53
+ {
54
+ projectId: pointer.projectId,
55
+ externalTaskId: pointer.taskSlug,
56
+ branch: pointer.branch,
57
+ taskId: pointer.taskId,
58
+ },
59
+ ]);
60
+ const git = new GitInspector(undefined, knownReservations);
61
+ const repositories = new RepositoryResolver(git, config.markerSchemaVersion, stateRoot);
62
+ const outbox = new OfflineOutbox(stateRoot);
52
63
  const repositoryDecisions = new RepositoryDecisionStore(stateRoot);
53
64
  const updateChoices = new UpdateChoiceStore(stateRoot);
54
65
  const shadowNotices = new ShadowNoticeStore(stateRoot);
@@ -64,6 +75,7 @@ export function createBridgeService(options = {}) {
64
75
  (await outbox.listForTask(entry.taskId)).length);
65
76
  },
66
77
  onDeliveryOutcome: async (report) => service?.reportDeliveryOutcome(report),
78
+ knownReservations,
67
79
  });
68
80
  const gate = new VerificationGate(stateRoot, git, outbox, undefined, worktreePool);
69
81
  const principalState = new PrincipalStateGuard(stateRoot, credentials, cache, outbox, activeContexts, gate);
@@ -76,7 +88,7 @@ export function createBridgeService(options = {}) {
76
88
  client,
77
89
  credentials,
78
90
  browserAuth: new BrowserAuthCoordinator(client, credentials, () => languages.read(sha256('anonymous'))),
79
- repositories: new RepositoryResolver(git, config.markerSchemaVersion, stateRoot),
91
+ repositories,
80
92
  journal: new JournalStore(stateRoot),
81
93
  outbox,
82
94
  gate,
@@ -1,13 +1,18 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { NativeCommandRunner } from '../utilities/process.js';
2
3
  import { assertSafeToPersist } from './offline-outbox.js';
3
4
  import { BridgeRecoveryError } from './recovery-error.js';
4
5
  const decisionRef = 'refs/worktree/engineering-memory-task';
6
+ const keptRef = (oid) => 'refs/engineering-memory/task-reservations/' + oid;
7
+ const kept = new Set();
5
8
  export class TaskBranchStore {
6
9
  repoRoot;
7
10
  runner;
8
- constructor(repoRoot, runner = new NativeCommandRunner()) {
11
+ known;
12
+ constructor(repoRoot, runner = new NativeCommandRunner(), known) {
9
13
  this.repoRoot = repoRoot;
10
14
  this.runner = runner;
15
+ this.known = known;
11
16
  }
12
17
  async read() {
13
18
  const reference = await this.runner.run('git', ['rev-parse', '--verify', '--quiet', decisionRef], { cwd: this.repoRoot });
@@ -17,8 +22,12 @@ export class TaskBranchStore {
17
22
  throw new BridgeRecoveryError('Git could not read the worktree task reservation. Retry the branch operation.', 'task.branch');
18
23
  const oid = reference.stdout.trim();
19
24
  const blob = await this.runner.run('git', ['cat-file', 'blob', oid], { cwd: this.repoRoot });
20
- if (blob.exitCode !== 0)
21
- throw new BridgeRecoveryError('Git could not read the recorded branch decision. Retry the branch operation.', 'task.branch');
25
+ if (blob.exitCode !== 0) {
26
+ const found = await this.runner.run('git', ['cat-file', '-e', oid], { cwd: this.repoRoot });
27
+ if (found.exitCode !== 1 || !this.known)
28
+ throw new BridgeRecoveryError('Git could not read the recorded branch decision. Retry the branch operation.', 'task.branch');
29
+ return await this.restore(oid, await this.known(this.repoRoot));
30
+ }
22
31
  let decision;
23
32
  try {
24
33
  decision = JSON.parse(blob.stdout);
@@ -33,6 +42,7 @@ export class TaskBranchStore {
33
42
  !(decision.branch === null || typeof decision.branch === 'string')) {
34
43
  throw new BridgeRecoveryError('The worktree branch record is invalid. Use a separate worktree to continue this task.', 'task.branch');
35
44
  }
45
+ await this.keep(oid);
36
46
  return { oid, decision };
37
47
  }
38
48
  async reserve(decision) {
@@ -43,7 +53,10 @@ export class TaskBranchStore {
43
53
  return;
44
54
  }
45
55
  const oid = await this.hash(decision);
46
- const written = await this.runner.run('git', ['update-ref', '--no-deref', decisionRef, oid, '0'.repeat(oid.length)], { cwd: this.repoRoot });
56
+ const written = await this.update([
57
+ `create ${decisionRef} ${oid}`,
58
+ `update ${keptRef(oid)} ${oid}`,
59
+ ]);
47
60
  if (written.exitCode !== 0) {
48
61
  const winner = await this.read();
49
62
  if (winner) {
@@ -62,7 +75,11 @@ export class TaskBranchStore {
62
75
  if (current.decision.taskId === taskId)
63
76
  return;
64
77
  const oid = await this.hash({ ...current.decision, taskId });
65
- const result = await this.runner.run('git', ['update-ref', '--no-deref', decisionRef, oid, current.oid], { cwd: this.repoRoot });
78
+ const result = await this.update([
79
+ `update ${decisionRef} ${oid} ${current.oid}`,
80
+ `update ${keptRef(oid)} ${oid}`,
81
+ `delete ${keptRef(current.oid)}`,
82
+ ]);
66
83
  if (result.exitCode !== 0) {
67
84
  const latest = await this.read();
68
85
  if (latest?.decision.taskId !== taskId)
@@ -76,7 +93,7 @@ export class TaskBranchStore {
76
93
  current.decision.projectId !== projectId ||
77
94
  current.decision.externalTaskId !== externalTaskId)
78
95
  return;
79
- const result = await this.runner.run('git', ['update-ref', '--no-deref', '-d', decisionRef, current.oid], { cwd: this.repoRoot });
96
+ const result = await this.remove(current.oid);
80
97
  if (result.exitCode !== 0 && (await this.read())?.oid === current.oid)
81
98
  throw new BridgeRecoveryError('The failed branch operation could not release its reservation. Retry task.branch with the same task identifier.', 'task.branch');
82
99
  }
@@ -84,7 +101,7 @@ export class TaskBranchStore {
84
101
  const current = await this.read();
85
102
  if (!current || current.decision.taskId !== taskId)
86
103
  return;
87
- const result = await this.runner.run('git', ['update-ref', '--no-deref', '-d', decisionRef, current.oid], { cwd: this.repoRoot });
104
+ const result = await this.remove(current.oid);
88
105
  if (result.exitCode !== 0 && (await this.read())?.decision.taskId === taskId) {
89
106
  throw new BridgeRecoveryError('The task has finished but its worktree reservation could not be released. Resume the task to retry cleanup.', 'session.resume');
90
107
  }
@@ -98,7 +115,7 @@ export class TaskBranchStore {
98
115
  current.decision.externalTaskId !== expected.externalTaskId ||
99
116
  current.decision.taskId !== expected.taskId)
100
117
  throw changed();
101
- const result = await this.runner.run('git', ['update-ref', '--no-deref', '-d', decisionRef, oid], { cwd: this.repoRoot });
118
+ const result = await this.remove(oid);
102
119
  if (result.exitCode !== 0) {
103
120
  if ((await this.read())?.oid !== oid)
104
121
  throw changed();
@@ -114,6 +131,48 @@ export class TaskBranchStore {
114
131
  throw new BridgeRecoveryError(`This task selected ${current.branch ?? 'detached HEAD'}. Return to that branch before continuing.`, 'task.branch');
115
132
  }
116
133
  }
134
+ async restore(oid, known) {
135
+ for (const task of known) {
136
+ const unopened = {
137
+ projectId: task.projectId,
138
+ externalTaskId: task.externalTaskId,
139
+ branch: task.branch,
140
+ };
141
+ for (const decision of [{ ...unopened, taskId: task.taskId }, unopened]) {
142
+ const body = Buffer.from(JSON.stringify(decision));
143
+ const id = createHash(oid.length === 64 ? 'sha256' : 'sha1')
144
+ .update(`blob ${body.length}\0`)
145
+ .update(body)
146
+ .digest('hex');
147
+ if (id !== oid)
148
+ continue;
149
+ await this.hash(decision);
150
+ await this.keep(oid);
151
+ return { oid, decision };
152
+ }
153
+ }
154
+ if ((await this.remove(oid)).exitCode !== 0)
155
+ throw new BridgeRecoveryError('Git could not read the recorded branch decision. Retry the branch operation.', 'task.branch');
156
+ return null;
157
+ }
158
+ async keep(oid) {
159
+ if (kept.has(this.repoRoot + '\n' + oid))
160
+ return;
161
+ const result = await this.runner.run('git', ['update-ref', keptRef(oid), oid], {
162
+ cwd: this.repoRoot,
163
+ });
164
+ if (result.exitCode === 0)
165
+ kept.add(this.repoRoot + '\n' + oid);
166
+ }
167
+ async remove(oid) {
168
+ return await this.update([`delete ${decisionRef} ${oid}`, `delete ${keptRef(oid)}`]);
169
+ }
170
+ async update(commands) {
171
+ return await this.runner.run('git', ['update-ref', '--no-deref', '--stdin'], {
172
+ cwd: this.repoRoot,
173
+ input: commands.join('\n') + '\n',
174
+ });
175
+ }
117
176
  async hash(decision) {
118
177
  const result = await this.runner.run('git', ['hash-object', '-w', '--stdin'], {
119
178
  cwd: this.repoRoot,
@@ -131,7 +131,7 @@ export class WorktreePool {
131
131
  this.stateRoot = stateRoot;
132
132
  this.ownerId = options.ownerId ?? randomUUID();
133
133
  this.runner = options.runner ?? new NativeCommandRunner();
134
- this.git = new GitInspector(this.runner);
134
+ this.git = new GitInspector(this.runner, options.knownReservations);
135
135
  this.now = options.now ?? Date.now;
136
136
  this.alive = options.processAlive ?? processAlive;
137
137
  this.documents = options.documentsRoot;