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.
|
|
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",
|
package/runtime/build.json
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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;
|