engineering-memory 1.10.0 → 1.10.2
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/project/repository.js +8 -3
- package/runtime/dist/src/runtime/api-client.js +1 -0
- package/runtime/dist/src/runtime/bridge-service.js +41 -18
- package/runtime/dist/src/runtime/offline-outbox.js +6 -0
- package/skill/references/lifecycle.md +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engineering-memory",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.2",
|
|
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()
|
|
@@ -59,19 +59,24 @@ export class RepositoryResolver {
|
|
|
59
59
|
projectId: explicitProjectId ?? binding?.projectId ?? null,
|
|
60
60
|
schemaVersion: binding?.schemaVersion ?? null,
|
|
61
61
|
repoFingerprint,
|
|
62
|
+
legacyFingerprints: binding ? [] : [await this.git.fingerprint(repoRoot, 1)],
|
|
62
63
|
git,
|
|
63
64
|
};
|
|
64
65
|
}
|
|
65
|
-
async writeBinding(repoRoot, projectId) {
|
|
66
|
+
async writeBinding(repoRoot, projectId, fingerprint) {
|
|
66
67
|
assertProjectId(projectId);
|
|
67
68
|
const repository = await this.resolve(repoRoot, projectId);
|
|
69
|
+
const older = repository.legacyFingerprints.includes(fingerprint);
|
|
70
|
+
if (fingerprint !== repository.repoFingerprint && !older) {
|
|
71
|
+
throw new Error('The confirmed project fingerprint does not identify this repository');
|
|
72
|
+
}
|
|
68
73
|
const repositoryKey = await this.keyFor(repository.repoRoot);
|
|
69
74
|
const bindingPath = this.pathFor(repositoryKey);
|
|
70
75
|
await writeJson(bindingPath, {
|
|
71
76
|
projectId,
|
|
72
|
-
schemaVersion: repository.schemaVersion ?? this.markerSchemaVersion,
|
|
77
|
+
schemaVersion: older ? 1 : (repository.schemaVersion ?? this.markerSchemaVersion),
|
|
73
78
|
repositoryKey,
|
|
74
|
-
repoFingerprint:
|
|
79
|
+
repoFingerprint: fingerprint,
|
|
75
80
|
}, this.stateRoot);
|
|
76
81
|
return bindingPath;
|
|
77
82
|
}
|
|
@@ -70,7 +70,10 @@ export class BridgeService {
|
|
|
70
70
|
if (!repository.bindingPath) {
|
|
71
71
|
const resolved = await this.dependencies.client.request(endpoints.projectResolve, {
|
|
72
72
|
method: 'POST',
|
|
73
|
-
body: {
|
|
73
|
+
body: {
|
|
74
|
+
repoFingerprint: repository.repoFingerprint,
|
|
75
|
+
legacyFingerprints: repository.legacyFingerprints,
|
|
76
|
+
},
|
|
74
77
|
});
|
|
75
78
|
const project = objectValue(resolved.data);
|
|
76
79
|
const restoreRequired = Boolean(project?.archivedAt);
|
|
@@ -248,7 +251,7 @@ export class BridgeService {
|
|
|
248
251
|
});
|
|
249
252
|
}
|
|
250
253
|
const recoveredDeliveries = await this.recoverJournalOutbox(projectId, taskSlug);
|
|
251
|
-
const outbox = await this.flushOutbox();
|
|
254
|
+
const outbox = await this.flushOutbox(pointer?.taskId);
|
|
252
255
|
const afterSequence = input.afterSequence ?? 0;
|
|
253
256
|
const response = await this.dependencies.client.request(endpoints.sessionResume, {
|
|
254
257
|
method: 'POST',
|
|
@@ -278,7 +281,7 @@ export class BridgeService {
|
|
|
278
281
|
.release(backendTask.id);
|
|
279
282
|
}
|
|
280
283
|
let localJournal = await this.dependencies.journal.load(projectId, taskSlug);
|
|
281
|
-
let pendingOutbox = await this.dependencies.outbox.
|
|
284
|
+
let pendingOutbox = await this.dependencies.outbox.listForTask(backendTask.id);
|
|
282
285
|
const responseSource = this.dependencies.client.getResponseSource(response);
|
|
283
286
|
const recoveredVerification = responseSource !== 'stale_cache' && pointer
|
|
284
287
|
? await this.retryVerificationRecovery(repository, pointer, backend)
|
|
@@ -306,7 +309,7 @@ export class BridgeService {
|
|
|
306
309
|
documents: backendDocuments,
|
|
307
310
|
});
|
|
308
311
|
blockedJournalEventIds = hydration.blockedEventIds;
|
|
309
|
-
const entries = await this.dependencies.outbox.
|
|
312
|
+
const entries = await this.dependencies.outbox.listForTask(backendTask.id);
|
|
310
313
|
for (const eventId of hydration.synchronizedEventIds) {
|
|
311
314
|
const entry = entries.find((value) => value.journalRef?.eventId === eventId);
|
|
312
315
|
if (entry) {
|
|
@@ -337,7 +340,7 @@ export class BridgeService {
|
|
|
337
340
|
}
|
|
338
341
|
}
|
|
339
342
|
localJournal = await this.dependencies.journal.load(projectId, taskSlug);
|
|
340
|
-
pendingOutbox = await this.dependencies.outbox.
|
|
343
|
+
pendingOutbox = await this.dependencies.outbox.listForTask(backendTask.id);
|
|
341
344
|
hydratedFromBackend = !hadLocalProjection;
|
|
342
345
|
mergedFromBackend = hadLocalProjection;
|
|
343
346
|
}
|
|
@@ -437,10 +440,12 @@ export class BridgeService {
|
|
|
437
440
|
async contextPrepareChange(input) {
|
|
438
441
|
return await this.execute(async () => {
|
|
439
442
|
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
443
|
const pointer = await this.dependencies.activeContexts.loadForSession(repository.repoFingerprint, input.sessionId);
|
|
444
|
+
if (!pointer)
|
|
445
|
+
throw refuse('Resume this task before requesting a change lease.', 'session.resume');
|
|
446
|
+
await this.recoverJournalOutbox(pointer.projectId, pointer.taskSlug);
|
|
447
|
+
const outbox = await this.flushOutbox(pointer.taskId);
|
|
448
|
+
const pendingOutbox = await this.dependencies.outbox.listForTask(pointer.taskId);
|
|
444
449
|
if (pointer?.verificationIntent || pointer?.closeIntent) {
|
|
445
450
|
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
451
|
}
|
|
@@ -992,8 +997,9 @@ export class BridgeService {
|
|
|
992
997
|
return await this.execute(async () => {
|
|
993
998
|
return await this.taskExclusive(input.taskId, async () => {
|
|
994
999
|
await this.recoverJournalOutbox();
|
|
995
|
-
const outbox = await this.flushOutbox();
|
|
996
|
-
const pendingOutboxCount = (await this.dependencies.outbox.
|
|
1000
|
+
const outbox = await this.flushOutbox(input.taskId);
|
|
1001
|
+
const pendingOutboxCount = (await this.dependencies.outbox.listForTask(input.taskId))
|
|
1002
|
+
.length;
|
|
997
1003
|
const repository = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd());
|
|
998
1004
|
if (pendingOutboxCount > 0) {
|
|
999
1005
|
return asJsonValue({
|
|
@@ -1358,7 +1364,9 @@ export class BridgeService {
|
|
|
1358
1364
|
projectName: input.projectName,
|
|
1359
1365
|
projectSlug: input.projectSlug ?? slugify(input.projectName),
|
|
1360
1366
|
repoFingerprint: repository.repoFingerprint,
|
|
1367
|
+
legacyFingerprints: repository.legacyFingerprints,
|
|
1361
1368
|
framework: input.framework,
|
|
1369
|
+
developmentArea: input.developmentArea,
|
|
1362
1370
|
figmaConfig: input.figmaConfig,
|
|
1363
1371
|
initialProjectProfile: input.initialProjectProfile,
|
|
1364
1372
|
});
|
|
@@ -1380,6 +1388,7 @@ export class BridgeService {
|
|
|
1380
1388
|
const marker = objectValue(data?.marker);
|
|
1381
1389
|
if (!project ||
|
|
1382
1390
|
typeof project.id !== 'string' ||
|
|
1391
|
+
typeof project.repoFingerprint !== 'string' ||
|
|
1383
1392
|
!profileResource ||
|
|
1384
1393
|
!profileRevision ||
|
|
1385
1394
|
!policy ||
|
|
@@ -1388,7 +1397,7 @@ export class BridgeService {
|
|
|
1388
1397
|
readDiscoveryUnits(policy).length === 0) {
|
|
1389
1398
|
throw refuse('Project setup response is not policy-ready', 'project.setup');
|
|
1390
1399
|
}
|
|
1391
|
-
const bindingPath = await this.dependencies.repositories.writeBinding(repository.repoRoot, project.id);
|
|
1400
|
+
const bindingPath = await this.dependencies.repositories.writeBinding(repository.repoRoot, project.id, project.repoFingerprint);
|
|
1392
1401
|
return asJsonValue({ ...data, marker: null, markerPath: null, bindingPath });
|
|
1393
1402
|
});
|
|
1394
1403
|
}
|
|
@@ -1685,13 +1694,18 @@ export class BridgeService {
|
|
|
1685
1694
|
}
|
|
1686
1695
|
const response = await this.dependencies.client.request(endpoints.projectBind(input.projectId), {
|
|
1687
1696
|
method: 'POST',
|
|
1688
|
-
body: {
|
|
1697
|
+
body: {
|
|
1698
|
+
repoFingerprint: repository.repoFingerprint,
|
|
1699
|
+
legacyFingerprints: repository.legacyFingerprints,
|
|
1700
|
+
},
|
|
1689
1701
|
});
|
|
1690
1702
|
const project = objectValue(response.data);
|
|
1691
|
-
if (!project ||
|
|
1703
|
+
if (!project ||
|
|
1704
|
+
project.id !== input.projectId ||
|
|
1705
|
+
typeof project.repoFingerprint !== 'string') {
|
|
1692
1706
|
throw refuse('Project bind response does not match the selected project', 'project.resolve');
|
|
1693
1707
|
}
|
|
1694
|
-
const bindingPath = await this.dependencies.repositories.writeBinding(repository.repoRoot, input.projectId);
|
|
1708
|
+
const bindingPath = await this.dependencies.repositories.writeBinding(repository.repoRoot, input.projectId, project.repoFingerprint);
|
|
1695
1709
|
return asJsonValue({
|
|
1696
1710
|
resolved: true,
|
|
1697
1711
|
bound: true,
|
|
@@ -1704,7 +1718,10 @@ export class BridgeService {
|
|
|
1704
1718
|
}
|
|
1705
1719
|
const response = await this.dependencies.client.request(endpoints.projectResolve, {
|
|
1706
1720
|
method: 'POST',
|
|
1707
|
-
body: {
|
|
1721
|
+
body: {
|
|
1722
|
+
repoFingerprint: repository.repoFingerprint,
|
|
1723
|
+
legacyFingerprints: repository.legacyFingerprints,
|
|
1724
|
+
},
|
|
1708
1725
|
});
|
|
1709
1726
|
const project = objectValue(response.data);
|
|
1710
1727
|
const restoreRequired = Boolean(project?.archivedAt);
|
|
@@ -2085,11 +2102,17 @@ export class BridgeService {
|
|
|
2085
2102
|
return asJsonValue({ queued: true, outboxId: queued.id, idempotencyKey });
|
|
2086
2103
|
}
|
|
2087
2104
|
}
|
|
2088
|
-
async flushOutbox() {
|
|
2089
|
-
const entries =
|
|
2105
|
+
async flushOutbox(taskId) {
|
|
2106
|
+
const entries = taskId
|
|
2107
|
+
? await this.dependencies.outbox.listForTask(taskId)
|
|
2108
|
+
: await this.dependencies.outbox.list();
|
|
2090
2109
|
const synchronized = [];
|
|
2091
2110
|
const blocked = [];
|
|
2111
|
+
const blockedTasks = new Set();
|
|
2092
2112
|
for (const entry of entries) {
|
|
2113
|
+
const deliveryTaskId = taskIdFromDeliverySafe(entry);
|
|
2114
|
+
if (blockedTasks.has(deliveryTaskId))
|
|
2115
|
+
continue;
|
|
2093
2116
|
try {
|
|
2094
2117
|
const response = await this.dependencies.client.request(entry.path, {
|
|
2095
2118
|
method: entry.method,
|
|
@@ -2115,7 +2138,7 @@ export class BridgeService {
|
|
|
2115
2138
|
await this.dependencies.journal.markDeliveryAttempt(entry.journalRef.projectId, entry.journalRef.taskSlug, entry.journalRef.eventId, errorKind);
|
|
2116
2139
|
}
|
|
2117
2140
|
blocked.push({ id: entry.id, error: errorKind });
|
|
2118
|
-
|
|
2141
|
+
blockedTasks.add(deliveryTaskId);
|
|
2119
2142
|
}
|
|
2120
2143
|
}
|
|
2121
2144
|
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);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Sign-in decides nothing beyond who the user is. The organization and the project are chosen after it, through the questionnaires in `questionnaires.md`, and both listings end with an option to create a new one. Ask for both whenever this session has not already confirmed them, and ask again the moment the user says they want to change either — changing the organization always means choosing the project again.
|
|
6
6
|
|
|
7
|
-
Use the binding returned by `session.entry` as the authority. The bridge stores the selected project and repository identity under `~/.engineering-memory/origins/<api-hash>/project-bindings/`, outside the installed runtime and working tree. It imports a legacy `.engineering-memory/project.json` once, preserving its schema and exact backend fingerprint for existing tasks and receipts. It leaves that file unchanged; after migration the file is optional and may be removed through the repository's normal Git workflow. New bindings never create it. Never infer a selection from a directory or remote, and never edit binding records by hand. Separate clones or a changed remote require a new explicit selection; worktrees of the same checkout share the local binding.
|
|
7
|
+
Use the binding returned by `session.entry` as the authority. The bridge stores the selected project and repository identity under `~/.engineering-memory/origins/<api-hash>/project-bindings/`, outside the installed runtime and working tree. It imports a legacy `.engineering-memory/project.json` once, preserving its schema and exact backend fingerprint for existing tasks and receipts. It leaves that file unchanged; after migration the file is optional and may be removed through the repository's normal Git workflow. New bindings never create it. A checkout that never imported the marker still finds a project bound under the older identity: the bridge presents that identity alongside the current one and stores whichever the backend confirms. Never infer a selection from a directory or remote, and never edit binding records by hand. Separate clones or a changed remote require a new explicit selection; worktrees of the same checkout share the local binding.
|
|
8
8
|
|
|
9
9
|
An archived project is recoverable state, not a missing or conflicting binding. When an owner receives `project.restore`, use the `projectId` and `expectedVersion` in its data and call that operation before retrying. A non-owner receives `project.member_list` instead: list the members, identify a project owner and explain that the owner must restore the exact project before this session can retry. Use `project.list` with `includeArchived: true` when an owner must first select the archived project. Never create or bind a replacement project to escape archive state.
|
|
10
10
|
|
|
@@ -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.
|