engineering-memory 1.10.0 → 1.10.1
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 +1 -1
- package/runtime/dist/src/git/verification-gate.js +50 -12
- package/runtime/dist/src/mcp/tool-definitions.js +2 -0
- package/runtime/dist/src/runtime/api-client.js +1 -0
- package/runtime/dist/src/runtime/bridge-service.js +22 -12
- package/runtime/dist/src/runtime/offline-outbox.js +6 -0
- package/skill/references/lifecycle.md +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engineering-memory",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.1",
|
|
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,4 @@
|
|
|
1
|
+
import { sha256 } from '../utilities/hash.js';
|
|
1
2
|
import { readdir } from 'node:fs/promises';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
import { RepositoryResolver } from '../project/repository.js';
|
|
@@ -22,8 +23,10 @@ export class VerificationGate {
|
|
|
22
23
|
}
|
|
23
24
|
const verifiedAt = new Date();
|
|
24
25
|
const taskChanges = await this.git.taskManifest(input.repoRoot, input.taskChanges);
|
|
25
|
-
await
|
|
26
|
-
|
|
26
|
+
const worktreeId = worktreeIdentity(await this.git.findRoot(input.repoRoot));
|
|
27
|
+
await writeJson(this.pathFor(input.repoFingerprint, worktreeId, input.taskId), {
|
|
28
|
+
schemaVersion: 5,
|
|
29
|
+
worktreeId,
|
|
27
30
|
taskId: input.taskId,
|
|
28
31
|
projectId: input.projectId,
|
|
29
32
|
repoFingerprint: input.repoFingerprint,
|
|
@@ -39,16 +42,12 @@ export class VerificationGate {
|
|
|
39
42
|
}, this.root);
|
|
40
43
|
}
|
|
41
44
|
async verify(repoRoot) {
|
|
42
|
-
const [repository, stagedChanges,
|
|
45
|
+
const [repository, stagedChanges, scaffolding] = await Promise.all([
|
|
43
46
|
this.repositories.resolve(repoRoot),
|
|
44
47
|
this.git.stagedManifest(repoRoot),
|
|
45
|
-
this.outbox.list(),
|
|
46
48
|
this.git.temporaryScaffolding(repoRoot),
|
|
47
49
|
]);
|
|
48
50
|
const { repoFingerprint: fingerprint, git: manifest } = repository;
|
|
49
|
-
if (pending.length > 0) {
|
|
50
|
-
return { allowed: false, reason: 'offline_outbox_pending', diffHash: manifest.diffHash };
|
|
51
|
-
}
|
|
52
51
|
const staged = scaffolding.filter((match) => match.staged);
|
|
53
52
|
if (staged.length > 0) {
|
|
54
53
|
return {
|
|
@@ -58,10 +57,29 @@ export class VerificationGate {
|
|
|
58
57
|
details: staged.map((match) => `${match.path}:${match.line}`),
|
|
59
58
|
};
|
|
60
59
|
}
|
|
61
|
-
const
|
|
60
|
+
const branch = await this.git.currentBranch(repository.repoRoot);
|
|
61
|
+
const candidates = await this.receiptsFor(fingerprint, worktreeIdentity(repository.repoRoot));
|
|
62
|
+
const relevant = candidates.filter((candidate) => !repository.projectId || candidate.projectId === repository.projectId);
|
|
63
|
+
relevant.sort((a, b) => b.verifiedAt.localeCompare(a.verifiedAt) || a.taskId.localeCompare(b.taskId));
|
|
64
|
+
const matchesBranch = (candidate) => !candidate.branch || candidate.branch === branch;
|
|
65
|
+
const receipt = relevant.find((candidate) => candidate.taskClosed &&
|
|
66
|
+
matchesBranch(candidate) &&
|
|
67
|
+
candidate.diffHash === manifest.diffHash &&
|
|
68
|
+
isTaskManifest(candidate.taskChanges) &&
|
|
69
|
+
stagedChanges.every((entry) => belongsToTask(entry, candidate.taskChanges))) ??
|
|
70
|
+
relevant.find(matchesBranch) ??
|
|
71
|
+
relevant[0];
|
|
62
72
|
if (!receipt) {
|
|
63
73
|
return { allowed: false, reason: 'task_verify_required', diffHash: manifest.diffHash };
|
|
64
74
|
}
|
|
75
|
+
if ((await this.outbox.listForTask(receipt.taskId)).length > 0) {
|
|
76
|
+
return {
|
|
77
|
+
allowed: false,
|
|
78
|
+
reason: 'offline_outbox_pending',
|
|
79
|
+
diffHash: manifest.diffHash,
|
|
80
|
+
taskId: receipt.taskId,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
65
83
|
if (!receipt.taskClosed) {
|
|
66
84
|
return {
|
|
67
85
|
allowed: false,
|
|
@@ -70,7 +88,7 @@ export class VerificationGate {
|
|
|
70
88
|
taskId: receipt.taskId,
|
|
71
89
|
};
|
|
72
90
|
}
|
|
73
|
-
if (receipt.schemaVersion
|
|
91
|
+
if (![4, 5].includes(receipt.schemaVersion) ||
|
|
74
92
|
!receipt.projectId ||
|
|
75
93
|
typeof receipt.taskVersion !== 'number' ||
|
|
76
94
|
!Number.isSafeInteger(receipt.taskVersion) ||
|
|
@@ -83,7 +101,6 @@ export class VerificationGate {
|
|
|
83
101
|
taskId: receipt.taskId,
|
|
84
102
|
};
|
|
85
103
|
}
|
|
86
|
-
const branch = await this.git.currentBranch(repoRoot);
|
|
87
104
|
if (receipt.branch && branch !== receipt.branch) {
|
|
88
105
|
return {
|
|
89
106
|
allowed: false,
|
|
@@ -191,10 +208,31 @@ export class VerificationGate {
|
|
|
191
208
|
await removeFile(join(this.root, entry.name), this.root);
|
|
192
209
|
}
|
|
193
210
|
}
|
|
194
|
-
|
|
195
|
-
|
|
211
|
+
async receiptsFor(repoFingerprint, worktreeId) {
|
|
212
|
+
await ensureManagedDirectory(this.root, this.root);
|
|
213
|
+
const prefix = safeSegment(repoFingerprint);
|
|
214
|
+
const entries = await readdir(this.root, { withFileTypes: true });
|
|
215
|
+
const receipts = [];
|
|
216
|
+
for (const entry of entries) {
|
|
217
|
+
if (entry.name !== `${prefix}.json` &&
|
|
218
|
+
!(entry.name.startsWith(`${prefix}-`) && entry.name.endsWith('.json')))
|
|
219
|
+
continue;
|
|
220
|
+
if (!entry.isFile() || entry.isSymbolicLink())
|
|
221
|
+
throw new Error(`Unsafe verification gate entry: ${entry.name}`);
|
|
222
|
+
const receipt = await readJson(join(this.root, entry.name), this.root);
|
|
223
|
+
if (receipt?.repoFingerprint === repoFingerprint &&
|
|
224
|
+
(entry.name === `${prefix}.json` || receipt.worktreeId === worktreeId))
|
|
225
|
+
receipts.push(receipt);
|
|
226
|
+
}
|
|
227
|
+
return receipts;
|
|
228
|
+
}
|
|
229
|
+
pathFor(repoFingerprint, worktreeId, taskId) {
|
|
230
|
+
return join(this.root, `${safeSegment(repoFingerprint)}-${worktreeId}-${safeSegment(taskId)}.json`);
|
|
196
231
|
}
|
|
197
232
|
}
|
|
233
|
+
function worktreeIdentity(repoRoot) {
|
|
234
|
+
return sha256(process.platform === 'win32' ? repoRoot.toLowerCase() : repoRoot);
|
|
235
|
+
}
|
|
198
236
|
function asObject(value) {
|
|
199
237
|
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
200
238
|
}
|
|
@@ -487,6 +487,7 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
487
487
|
projectName: z.string().min(2),
|
|
488
488
|
projectSlug: z.string().min(2).optional(),
|
|
489
489
|
framework: z.string().optional(),
|
|
490
|
+
developmentArea: z.enum(['backend', 'web', 'mobile']).nullable().optional(),
|
|
490
491
|
figmaConfig: jsonObject,
|
|
491
492
|
initialProjectProfile: z.object({
|
|
492
493
|
title: z.string().min(2),
|
|
@@ -701,6 +702,7 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
701
702
|
expectedVersion: z.number().int().min(1),
|
|
702
703
|
name: z.string().min(2).max(160).optional(),
|
|
703
704
|
framework: z.string().min(2).max(80).nullable().optional(),
|
|
705
|
+
developmentArea: z.enum(['backend', 'web', 'mobile']).nullable().optional(),
|
|
704
706
|
repositoryUrl: z.string().url().nullable().optional(),
|
|
705
707
|
workItemPrefix: z
|
|
706
708
|
.string()
|
|
@@ -248,7 +248,7 @@ export class BridgeService {
|
|
|
248
248
|
});
|
|
249
249
|
}
|
|
250
250
|
const recoveredDeliveries = await this.recoverJournalOutbox(projectId, taskSlug);
|
|
251
|
-
const outbox = await this.flushOutbox();
|
|
251
|
+
const outbox = await this.flushOutbox(pointer?.taskId);
|
|
252
252
|
const afterSequence = input.afterSequence ?? 0;
|
|
253
253
|
const response = await this.dependencies.client.request(endpoints.sessionResume, {
|
|
254
254
|
method: 'POST',
|
|
@@ -278,7 +278,7 @@ export class BridgeService {
|
|
|
278
278
|
.release(backendTask.id);
|
|
279
279
|
}
|
|
280
280
|
let localJournal = await this.dependencies.journal.load(projectId, taskSlug);
|
|
281
|
-
let pendingOutbox = await this.dependencies.outbox.
|
|
281
|
+
let pendingOutbox = await this.dependencies.outbox.listForTask(backendTask.id);
|
|
282
282
|
const responseSource = this.dependencies.client.getResponseSource(response);
|
|
283
283
|
const recoveredVerification = responseSource !== 'stale_cache' && pointer
|
|
284
284
|
? await this.retryVerificationRecovery(repository, pointer, backend)
|
|
@@ -306,7 +306,7 @@ export class BridgeService {
|
|
|
306
306
|
documents: backendDocuments,
|
|
307
307
|
});
|
|
308
308
|
blockedJournalEventIds = hydration.blockedEventIds;
|
|
309
|
-
const entries = await this.dependencies.outbox.
|
|
309
|
+
const entries = await this.dependencies.outbox.listForTask(backendTask.id);
|
|
310
310
|
for (const eventId of hydration.synchronizedEventIds) {
|
|
311
311
|
const entry = entries.find((value) => value.journalRef?.eventId === eventId);
|
|
312
312
|
if (entry) {
|
|
@@ -337,7 +337,7 @@ export class BridgeService {
|
|
|
337
337
|
}
|
|
338
338
|
}
|
|
339
339
|
localJournal = await this.dependencies.journal.load(projectId, taskSlug);
|
|
340
|
-
pendingOutbox = await this.dependencies.outbox.
|
|
340
|
+
pendingOutbox = await this.dependencies.outbox.listForTask(backendTask.id);
|
|
341
341
|
hydratedFromBackend = !hadLocalProjection;
|
|
342
342
|
mergedFromBackend = hadLocalProjection;
|
|
343
343
|
}
|
|
@@ -437,10 +437,12 @@ export class BridgeService {
|
|
|
437
437
|
async contextPrepareChange(input) {
|
|
438
438
|
return await this.execute(async () => {
|
|
439
439
|
const repository = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd());
|
|
440
|
-
await this.recoverJournalOutbox();
|
|
441
|
-
const outbox = await this.flushOutbox();
|
|
442
|
-
const pendingOutbox = await this.dependencies.outbox.list();
|
|
443
440
|
const pointer = await this.dependencies.activeContexts.loadForSession(repository.repoFingerprint, input.sessionId);
|
|
441
|
+
if (!pointer)
|
|
442
|
+
throw refuse('Resume this task before requesting a change lease.', 'session.resume');
|
|
443
|
+
await this.recoverJournalOutbox(pointer.projectId, pointer.taskSlug);
|
|
444
|
+
const outbox = await this.flushOutbox(pointer.taskId);
|
|
445
|
+
const pendingOutbox = await this.dependencies.outbox.listForTask(pointer.taskId);
|
|
444
446
|
if (pointer?.verificationIntent || pointer?.closeIntent) {
|
|
445
447
|
throw refuse('A verification or close intent from an earlier session is still unsettled, so a new lease would be taken against the wrong state.', 'session.resume');
|
|
446
448
|
}
|
|
@@ -992,8 +994,9 @@ export class BridgeService {
|
|
|
992
994
|
return await this.execute(async () => {
|
|
993
995
|
return await this.taskExclusive(input.taskId, async () => {
|
|
994
996
|
await this.recoverJournalOutbox();
|
|
995
|
-
const outbox = await this.flushOutbox();
|
|
996
|
-
const pendingOutboxCount = (await this.dependencies.outbox.
|
|
997
|
+
const outbox = await this.flushOutbox(input.taskId);
|
|
998
|
+
const pendingOutboxCount = (await this.dependencies.outbox.listForTask(input.taskId))
|
|
999
|
+
.length;
|
|
997
1000
|
const repository = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd());
|
|
998
1001
|
if (pendingOutboxCount > 0) {
|
|
999
1002
|
return asJsonValue({
|
|
@@ -1359,6 +1362,7 @@ export class BridgeService {
|
|
|
1359
1362
|
projectSlug: input.projectSlug ?? slugify(input.projectName),
|
|
1360
1363
|
repoFingerprint: repository.repoFingerprint,
|
|
1361
1364
|
framework: input.framework,
|
|
1365
|
+
developmentArea: input.developmentArea,
|
|
1362
1366
|
figmaConfig: input.figmaConfig,
|
|
1363
1367
|
initialProjectProfile: input.initialProjectProfile,
|
|
1364
1368
|
});
|
|
@@ -2085,11 +2089,17 @@ export class BridgeService {
|
|
|
2085
2089
|
return asJsonValue({ queued: true, outboxId: queued.id, idempotencyKey });
|
|
2086
2090
|
}
|
|
2087
2091
|
}
|
|
2088
|
-
async flushOutbox() {
|
|
2089
|
-
const entries =
|
|
2092
|
+
async flushOutbox(taskId) {
|
|
2093
|
+
const entries = taskId
|
|
2094
|
+
? await this.dependencies.outbox.listForTask(taskId)
|
|
2095
|
+
: await this.dependencies.outbox.list();
|
|
2090
2096
|
const synchronized = [];
|
|
2091
2097
|
const blocked = [];
|
|
2098
|
+
const blockedTasks = new Set();
|
|
2092
2099
|
for (const entry of entries) {
|
|
2100
|
+
const deliveryTaskId = taskIdFromDeliverySafe(entry);
|
|
2101
|
+
if (blockedTasks.has(deliveryTaskId))
|
|
2102
|
+
continue;
|
|
2093
2103
|
try {
|
|
2094
2104
|
const response = await this.dependencies.client.request(entry.path, {
|
|
2095
2105
|
method: entry.method,
|
|
@@ -2115,7 +2125,7 @@ export class BridgeService {
|
|
|
2115
2125
|
await this.dependencies.journal.markDeliveryAttempt(entry.journalRef.projectId, entry.journalRef.taskSlug, entry.journalRef.eventId, errorKind);
|
|
2116
2126
|
}
|
|
2117
2127
|
blocked.push({ id: entry.id, error: errorKind });
|
|
2118
|
-
|
|
2128
|
+
blockedTasks.add(deliveryTaskId);
|
|
2119
2129
|
}
|
|
2120
2130
|
}
|
|
2121
2131
|
return asJsonValue({
|
|
@@ -81,6 +81,12 @@ export class OfflineOutbox {
|
|
|
81
81
|
}));
|
|
82
82
|
return values.filter((entry) => entry !== null).sort(compareEntries);
|
|
83
83
|
}
|
|
84
|
+
async listForTask(taskId) {
|
|
85
|
+
return (await this.list()).filter((entry) => {
|
|
86
|
+
const body = entry.body;
|
|
87
|
+
return (body !== null && typeof body === 'object' && !Array.isArray(body) && body.taskId === taskId);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
84
90
|
async markAttempt(id, errorKind) {
|
|
85
91
|
await this.exclusive(id, async () => {
|
|
86
92
|
const path = this.pathFor(id);
|
|
@@ -184,4 +184,4 @@ When a pull request has been opened, keep going: check whether it merges cleanly
|
|
|
184
184
|
|
|
185
185
|
## Offline Behavior
|
|
186
186
|
|
|
187
|
-
An unexpired cached context pack may be used for implementation. The bridge records checkpoints in its outbox. Permanent proposal review, strict verification, task close, and Git commit
|
|
187
|
+
An unexpired cached context pack may be used for implementation. The bridge records checkpoints in its outbox. Permanent proposal review, strict verification, task close, and Git commit require the backend and synchronization of the relevant task deliveries. An unrelated task or repository queue does not block this task; account switching and logout still require all deliveries to be resolved. Commit receipts are retained per task and worktree and still require an online attestation for the selected closed task and exact staged content.
|