engineering-memory 1.11.1 → 1.11.3
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/dispatcher/sections.mjs +2 -0
- package/package.json +1 -1
- package/runtime/dist/src/mcp/server.js +2 -0
- package/runtime/dist/src/mcp/tool-definitions.js +4 -4
- package/runtime/dist/src/runtime/bridge-service.js +211 -65
- package/runtime/dist/src/runtime/offline-outbox.js +136 -14
- package/skill/SKILL.md +2 -0
- package/skill/references/lifecycle.md +2 -2
- package/skill/references/memory-updates.md +47 -0
- package/skill/references/project-onboarding.md +11 -0
package/dispatcher/sections.mjs
CHANGED
|
@@ -21,6 +21,8 @@ After compaction, a new chat, interruption, or handoff, call \`session.resume\`
|
|
|
21
21
|
|
|
22
22
|
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
23
|
|
|
24
|
+
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.
|
|
25
|
+
|
|
24
26
|
Use the host's native questionnaire for every question to the user, including implementation choices, names, clarification, branch/worktree decisions and delivery. For a required decision, use \`questionnaire.ask\` to open a durable native MCP form and \`questionnaire.resume\` to return to the same unanswered question. Read the skill's questionnaires reference first. Never use request_user_input_async for a required decision: it does not wait for an answer. When the durable form is unavailable, use a blocking native control only where the host permits it: request_user_input in Codex or AskUserQuestion in Claude. Never replace the questionnaire with a chat instruction such as 'type this', 'reply yes', or 'write X if you want Y'. Do not open a survey web page. If the required native control is unavailable or prohibited for that kind of question, follow the host's tool restrictions, explain the limitation, and continue only work already authorized; do not fabricate a survey or silently choose an answer. A timeout, dismissed form, empty response or ended turn is not an answer. Keep the decision pending and resume it; do not start dependent work or report it as resolved. Existing answers remain valid through retries and handoffs.
|
|
25
27
|
|
|
26
28
|
When the repository is unbound, use the native questionnaire required by the skill. Do not silently create or bind a project. Do not open a survey web page.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engineering-memory",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.3",
|
|
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",
|
|
@@ -5,6 +5,8 @@ export function createEngineeringMemoryServer(service) {
|
|
|
5
5
|
const server = new McpServer({
|
|
6
6
|
name: 'engineering-memory',
|
|
7
7
|
version: '0.1.0',
|
|
8
|
+
}, {
|
|
9
|
+
instructions: 'Start with session.entry and follow the returned lifecycle. Do not block independent task work on memory.propose_revision drafting, submission or approval. If the host permits background agents or concurrent calls, dispatch independent proposals and continue useful work; otherwise checkpoint the draft and defer submission until needed. Checkpoint, correction, reconciliation, self-review and scaffold receipts use the durable local outbox; after deliveryStatus pending, continue independent work without polling or resending. Inventory upload, claim-bound evidence submission and test-result reporting may overlap independent analysis using host-permitted concurrent calls. Wait only when the next operation needs the result; required task deliveries must settle before verification. Preserve task/source identity and native user approval; never treat pending work as verified.',
|
|
8
10
|
});
|
|
9
11
|
registerEngineeringMemoryTools(server, service);
|
|
10
12
|
registerQuestionnaireTools(server, service);
|
|
@@ -311,7 +311,7 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
311
311
|
}),
|
|
312
312
|
}, async (input) => toolResult(await service.memoryHistory(input)));
|
|
313
313
|
server.registerTool('memory.propose_revision', {
|
|
314
|
-
description: 'Create an inactive, reviewable proposal for
|
|
314
|
+
description: 'Create an inactive, reviewable proposal for permanent product, organization or project memory. Do not block independent task work on drafting, submission or approval. Use authorized host background agents or concurrent tools and continue useful work; collect the result only before an operation needs its proposal or revision ID. Without concurrency, checkpoint the pending draft and defer this call until needed. Keep one writer per resource and preserve baseRevision and task/source identity. A response distinguishes stored (queued:false) from outbox-queued; neither means approved. Native user approval and task verification remain required.',
|
|
315
315
|
inputSchema: z.object({
|
|
316
316
|
repoRoot: optionalRepoRoot,
|
|
317
317
|
sourceRunId: z.string().uuid().optional(),
|
|
@@ -397,7 +397,7 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
397
397
|
}),
|
|
398
398
|
}, async (input) => toolResult(await service.taskRecordCorrection(input)));
|
|
399
399
|
server.registerTool('task.self_review', {
|
|
400
|
-
description: 'Record that the changed code was read back against the rules that govern it, naming the knowledge resources reviewed and every conflict found with how it was resolved. task.verify refuses until this exists for the current diff, so any further edit requires reviewing again.',
|
|
400
|
+
description: 'Record that the changed code was read back against the rules that govern it, naming the knowledge resources reviewed and every conflict found with how it was resolved. Returns a durable pending receipt without waiting for backend delivery; continue independent local validation. task.verify waits for required delivery and refuses until this exists for the current diff, so any further edit requires reviewing again.',
|
|
401
401
|
inputSchema: z.object({
|
|
402
402
|
repoRoot: optionalRepoRoot,
|
|
403
403
|
taskId: z.string().min(1),
|
|
@@ -411,7 +411,7 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
411
411
|
}),
|
|
412
412
|
}, async (input) => toolResult(await service.taskSelfReview(input)));
|
|
413
413
|
server.registerTool('task.reconcile', {
|
|
414
|
-
description: 'Reconcile changed screens and components with an approved revision or an explicit no-semantic-memory-change reason. Pass every record the task touched as `entries` in one call rather than calling once per record; the whole set is applied together and rejected together.',
|
|
414
|
+
description: 'Reconcile changed screens and components with an approved revision or an explicit no-semantic-memory-change reason. Pass every record the task touched as `entries` in one call rather than calling once per record; the whole set is applied together and rejected together. A pending result is a durable local receipt; continue independent work without polling. Required delivery is checked before verification.',
|
|
415
415
|
inputSchema: z
|
|
416
416
|
.object({
|
|
417
417
|
repoRoot: optionalRepoRoot,
|
|
@@ -608,7 +608,7 @@ export function registerEngineeringMemoryTools(server, service) {
|
|
|
608
608
|
}),
|
|
609
609
|
}, async (input) => toolResult(await service.architectureModule(input)));
|
|
610
610
|
server.registerTool('architecture.record_application', {
|
|
611
|
-
description: 'Record that a scaffold task wrote one architecture module, hashing each written file after the rename so verification can exempt untouched scaffolded files.',
|
|
611
|
+
description: 'Record that a scaffold task wrote one architecture module, hashing each written file after the rename so verification can exempt untouched scaffolded files. Returns a durable pending receipt; independent tests may run while it is delivered. Required delivery must finish before verification',
|
|
612
612
|
inputSchema: z.object({
|
|
613
613
|
repoRoot: optionalRepoRoot,
|
|
614
614
|
taskId: z.string().min(1),
|
|
@@ -31,7 +31,10 @@ export class BridgeService {
|
|
|
31
31
|
dependencies;
|
|
32
32
|
taskQueues = new Map();
|
|
33
33
|
deferDeliveries;
|
|
34
|
-
|
|
34
|
+
backgroundDeliveries = new Set();
|
|
35
|
+
deliveryQueues = new Map();
|
|
36
|
+
deliveryWaiters = [];
|
|
37
|
+
activeDeliveries = 0;
|
|
35
38
|
constructor(dependencies) {
|
|
36
39
|
this.dependencies = dependencies;
|
|
37
40
|
this.deferDeliveries = dependencies.deferDeliveries ?? true;
|
|
@@ -121,20 +124,17 @@ export class BridgeService {
|
|
|
121
124
|
nextAction: 'Call questionnaire.resume with this questionnaireId. No answer, elapsed time, dismissal or interruption is consent. If the user explicitly cancels this decision, call questionnaire.withdraw. Unrelated pending decisions never block independently authorized work.',
|
|
122
125
|
}));
|
|
123
126
|
}
|
|
124
|
-
deliverInBackground() {
|
|
127
|
+
deliverInBackground(taskId) {
|
|
125
128
|
if (!this.deferDeliveries)
|
|
126
129
|
return;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
});
|
|
130
|
+
const delivery = this.flushOutbox(taskId).then(() => undefined, () => undefined);
|
|
131
|
+
this.backgroundDeliveries.add(delivery);
|
|
132
|
+
void delivery.finally(() => this.backgroundDeliveries.delete(delivery));
|
|
135
133
|
}
|
|
136
134
|
async settleDeliveries() {
|
|
137
|
-
|
|
135
|
+
while (this.backgroundDeliveries.size) {
|
|
136
|
+
await Promise.all([...this.backgroundDeliveries]);
|
|
137
|
+
}
|
|
138
138
|
}
|
|
139
139
|
async sessionBootstrap(input) {
|
|
140
140
|
return await this.execute(async () => {
|
|
@@ -154,7 +154,15 @@ export class BridgeService {
|
|
|
154
154
|
return asJsonValue({ authentication, repository: publicRepository(repository) });
|
|
155
155
|
}
|
|
156
156
|
const recoveredDeliveries = await this.recoverJournalOutbox();
|
|
157
|
-
const
|
|
157
|
+
const existingTask = await this.dependencies.activeContexts.loadForSlug(repository.repoFingerprint, input.externalTaskId);
|
|
158
|
+
this.deliverInBackground();
|
|
159
|
+
const outbox = existingTask
|
|
160
|
+
? await this.flushOutbox(existingTask.taskId)
|
|
161
|
+
: asJsonValue({
|
|
162
|
+
synchronized: [],
|
|
163
|
+
blocked: [],
|
|
164
|
+
pending: (await this.dependencies.outbox.list()).length,
|
|
165
|
+
});
|
|
158
166
|
let projectId = repository.projectId;
|
|
159
167
|
if (!repository.bindingPath) {
|
|
160
168
|
const resolved = await this.dependencies.client.request(endpoints.projectResolve, {
|
|
@@ -1133,7 +1141,19 @@ export class BridgeService {
|
|
|
1133
1141
|
}
|
|
1134
1142
|
async onboardingRequest(operation, body) {
|
|
1135
1143
|
return this.execute(async () => {
|
|
1136
|
-
|
|
1144
|
+
if (operation === 'next' || operation === 'submit') {
|
|
1145
|
+
const { claimToken, ...persistentContent } = body;
|
|
1146
|
+
if (operation === 'submit' || Object.hasOwn(body, 'claimToken')) {
|
|
1147
|
+
if (typeof claimToken !== 'string' ||
|
|
1148
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(claimToken)) {
|
|
1149
|
+
throw refuse('Use the claim UUID returned by memory.sync_next for this unit. Reclaim an expired or missing claim with memory.sync_next.', 'memory.sync_next');
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
assertSafeToPersist(persistentContent);
|
|
1153
|
+
}
|
|
1154
|
+
else {
|
|
1155
|
+
assertSafeToPersist(body);
|
|
1156
|
+
}
|
|
1137
1157
|
const routes = {
|
|
1138
1158
|
sources: '/memory/sources/catalog',
|
|
1139
1159
|
status: '/memory/sync/status',
|
|
@@ -1254,6 +1274,13 @@ export class BridgeService {
|
|
|
1254
1274
|
const findings = normalizePersistentInput(input.findings, input.repoRoot ?? process.cwd());
|
|
1255
1275
|
assertSafeToPersist(cleanJson({ findings }));
|
|
1256
1276
|
const repository = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd());
|
|
1277
|
+
if (this.deferDeliveries) {
|
|
1278
|
+
return this.enqueueTaskReceipt('task.self_review', endpoints.taskSelfReview, input.taskId, repository.repoRoot, cleanJson({
|
|
1279
|
+
diffHash: repository.git.diffHash,
|
|
1280
|
+
reviewedResourceIds: [...new Set(input.reviewedResourceIds)],
|
|
1281
|
+
findings,
|
|
1282
|
+
}));
|
|
1283
|
+
}
|
|
1257
1284
|
const snapshot = await this.activeTaskSnapshot(input.taskId, undefined, input.repoRoot);
|
|
1258
1285
|
const response = await this.dependencies.client.request(endpoints.taskSelfReview, {
|
|
1259
1286
|
method: 'POST',
|
|
@@ -1291,6 +1318,9 @@ export class BridgeService {
|
|
|
1291
1318
|
reason: normalizePersistentInput(entry.reason, repoRoot),
|
|
1292
1319
|
}));
|
|
1293
1320
|
assertSafeToPersist(cleanJson({ entries }));
|
|
1321
|
+
if (this.deferDeliveries) {
|
|
1322
|
+
return this.enqueueTaskReceipt('task.reconcile', endpoints.taskReconcile, input.taskId, repoRoot, cleanJson({ entries }));
|
|
1323
|
+
}
|
|
1294
1324
|
const snapshot = await this.activeTaskSnapshot(input.taskId, undefined, input.repoRoot);
|
|
1295
1325
|
const body = cleanJson({
|
|
1296
1326
|
taskId: input.taskId,
|
|
@@ -1298,21 +1328,6 @@ export class BridgeService {
|
|
|
1298
1328
|
expectedTaskVersion: snapshot.taskVersion,
|
|
1299
1329
|
});
|
|
1300
1330
|
await this.dependencies.gate.invalidateTask(input.taskId);
|
|
1301
|
-
if (this.deferDeliveries) {
|
|
1302
|
-
const queued = await this.dependencies.outbox.enqueue({
|
|
1303
|
-
operation: 'task.reconcile',
|
|
1304
|
-
method: 'POST',
|
|
1305
|
-
path: endpoints.taskReconcile,
|
|
1306
|
-
body,
|
|
1307
|
-
});
|
|
1308
|
-
this.deliverInBackground();
|
|
1309
|
-
return asJsonValue({
|
|
1310
|
-
reconciled: entries.map((entry) => entry.resourceId),
|
|
1311
|
-
queued: true,
|
|
1312
|
-
outboxId: queued.id,
|
|
1313
|
-
deliveryStatus: 'pending',
|
|
1314
|
-
});
|
|
1315
|
-
}
|
|
1316
1331
|
const response = await this.dependencies.client.request(endpoints.taskReconcile, {
|
|
1317
1332
|
method: 'POST',
|
|
1318
1333
|
body,
|
|
@@ -2037,7 +2052,6 @@ export class BridgeService {
|
|
|
2037
2052
|
async architectureRecordApplication(input) {
|
|
2038
2053
|
return await this.execute(async () => {
|
|
2039
2054
|
const repository = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd());
|
|
2040
|
-
const snapshot = await this.activeTaskSnapshot(input.taskId, input.projectId, repository.repoRoot);
|
|
2041
2055
|
const files = await Promise.all(input.files.map(async (file) => {
|
|
2042
2056
|
const hash = await hashWorkingTreeFile(repository.repoRoot, file.path);
|
|
2043
2057
|
if (!hash) {
|
|
@@ -2045,6 +2059,15 @@ export class BridgeService {
|
|
|
2045
2059
|
}
|
|
2046
2060
|
return { templatePath: file.templatePath, path: file.path, sha256: hash };
|
|
2047
2061
|
}));
|
|
2062
|
+
if (this.deferDeliveries) {
|
|
2063
|
+
return this.enqueueTaskReceipt('architecture.record_application', endpoints.taskScaffoldApplication, input.taskId, repository.repoRoot, cleanJson({
|
|
2064
|
+
projectId: input.projectId,
|
|
2065
|
+
templateResourceId: input.templateResourceId,
|
|
2066
|
+
templateRevisionId: input.templateRevisionId,
|
|
2067
|
+
files,
|
|
2068
|
+
}));
|
|
2069
|
+
}
|
|
2070
|
+
const snapshot = await this.activeTaskSnapshot(input.taskId, input.projectId, repository.repoRoot);
|
|
2048
2071
|
const body = cleanJson({
|
|
2049
2072
|
taskId: input.taskId,
|
|
2050
2073
|
projectId: input.projectId,
|
|
@@ -2411,7 +2434,7 @@ export class BridgeService {
|
|
|
2411
2434
|
return await this.taskExclusive(input.taskId, async () => {
|
|
2412
2435
|
await this.recoverJournalOutbox(input.projectId, input.taskSlug);
|
|
2413
2436
|
if (this.deferDeliveries)
|
|
2414
|
-
this.deliverInBackground();
|
|
2437
|
+
this.deliverInBackground(input.taskId);
|
|
2415
2438
|
else
|
|
2416
2439
|
await this.flushOutbox();
|
|
2417
2440
|
const journalInput = {
|
|
@@ -2436,10 +2459,8 @@ export class BridgeService {
|
|
|
2436
2459
|
idempotencyKey,
|
|
2437
2460
|
});
|
|
2438
2461
|
}
|
|
2439
|
-
const pointer = await this.resolveCheckpointPointer(input, repoRoot);
|
|
2440
2462
|
const taskEntries = (await this.dependencies.outbox.list()).filter((entry) => taskIdFromDeliverySafe(entry) === input.taskId);
|
|
2441
|
-
const
|
|
2442
|
-
(entry.operation === 'task.checkpoint' || entry.operation === 'task.record_correction'));
|
|
2463
|
+
const pointer = await this.resolveCheckpointPointer(input, repoRoot);
|
|
2443
2464
|
const blockedEntry = taskEntries.find((entry) => entry.lastError && entry.lastError !== 'backend_unavailable');
|
|
2444
2465
|
if (blockedEntry && blockedEntry.idempotencyKey !== idempotencyKey) {
|
|
2445
2466
|
throw refuse('A blocked task delivery must be explicitly resolved before checkpointing', 'task.resolve_pending_delivery');
|
|
@@ -2447,8 +2468,7 @@ export class BridgeService {
|
|
|
2447
2468
|
const existingExpected = pendingJournal
|
|
2448
2469
|
? expectedTaskVersionFromBody(pendingJournal.body)
|
|
2449
2470
|
: null;
|
|
2450
|
-
const expectedTaskVersion = existingExpected ??
|
|
2451
|
-
Math.max(pointer.taskVersion, ...versionedTaskEntries.map((entry) => (expectedTaskVersionFromBody(entry.body) ?? pointer.taskVersion - 1) + 1));
|
|
2471
|
+
const expectedTaskVersion = existingExpected ?? pendingTaskVersion(pointer.taskVersion, taskEntries);
|
|
2452
2472
|
await this.dependencies.gate.invalidateTask(input.taskId);
|
|
2453
2473
|
const staged = await this.dependencies.journal.stage(journalInput, {
|
|
2454
2474
|
operation: correction ? 'task.record_correction' : 'task.checkpoint',
|
|
@@ -2477,7 +2497,7 @@ export class BridgeService {
|
|
|
2477
2497
|
}
|
|
2478
2498
|
const predecessorEntries = taskEntries.filter((entry) => entry.id !== queued.id);
|
|
2479
2499
|
if (predecessorEntries.length > 0 || this.deferDeliveries) {
|
|
2480
|
-
this.deliverInBackground();
|
|
2500
|
+
this.deliverInBackground(input.taskId);
|
|
2481
2501
|
return asJsonValue({
|
|
2482
2502
|
local: { directory: staged.directory, applied: staged.applied },
|
|
2483
2503
|
queued: true,
|
|
@@ -2551,51 +2571,168 @@ export class BridgeService {
|
|
|
2551
2571
|
return asJsonValue({ queued: true, outboxId: queued.id, idempotencyKey });
|
|
2552
2572
|
}
|
|
2553
2573
|
}
|
|
2574
|
+
async enqueueTaskReceipt(operation, path, taskId, repoRoot, content) {
|
|
2575
|
+
const repository = await this.dependencies.repositories.resolve(repoRoot);
|
|
2576
|
+
return this.taskExclusive(taskId, async () => {
|
|
2577
|
+
const pointer = await this.requireActivePointer(repository.repoFingerprint, taskId);
|
|
2578
|
+
if (repository.projectId && repository.projectId !== pointer.projectId)
|
|
2579
|
+
throw refuse('The receipt does not belong to this repository project.', 'session.resume');
|
|
2580
|
+
const fields = objectOrEmpty(content);
|
|
2581
|
+
if (fields.projectId && fields.projectId !== pointer.projectId)
|
|
2582
|
+
throw refuse('The receipt does not belong to this task project.', 'session.resume');
|
|
2583
|
+
if (pointer.resumeConflicts.length || pointer.verificationIntent || pointer.closeIntent)
|
|
2584
|
+
throw refuse('Resume a clean active task before recording this receipt.', 'session.resume');
|
|
2585
|
+
await this.checkoutSource(repository, pointer.source);
|
|
2586
|
+
await this.recoverJournalOutbox(pointer.projectId, pointer.taskSlug);
|
|
2587
|
+
const entries = await this.dependencies.outbox.listForTask(taskId);
|
|
2588
|
+
if (entries.some((entry) => entry.lastError && entry.lastError !== 'backend_unavailable'))
|
|
2589
|
+
throw refuse('Resolve the blocked delivery for this task before adding a receipt.', 'task.resolve_pending_delivery');
|
|
2590
|
+
const latest = await this.requireActivePointer(repository.repoFingerprint, taskId);
|
|
2591
|
+
const expectedTaskVersion = pendingTaskVersion(latest.taskVersion, entries);
|
|
2592
|
+
const body = cleanJson({ ...fields, taskId, expectedTaskVersion });
|
|
2593
|
+
const idempotencyKey = sha256(stableStringify({ operation, body }));
|
|
2594
|
+
await this.dependencies.gate.invalidateTask(taskId);
|
|
2595
|
+
const queued = await this.dependencies.outbox.enqueue({
|
|
2596
|
+
operation,
|
|
2597
|
+
method: 'POST',
|
|
2598
|
+
path,
|
|
2599
|
+
body,
|
|
2600
|
+
idempotencyKey,
|
|
2601
|
+
projectId: pointer.projectId,
|
|
2602
|
+
});
|
|
2603
|
+
this.deliverInBackground(taskId);
|
|
2604
|
+
return asJsonValue({
|
|
2605
|
+
queued: true,
|
|
2606
|
+
outboxId: queued.id,
|
|
2607
|
+
idempotencyKey,
|
|
2608
|
+
deliveryStatus: 'pending',
|
|
2609
|
+
expectedTaskVersion,
|
|
2610
|
+
nextAction: 'Continue independent work. Required delivery is checked before task.verify; session.resume reports pending failures.',
|
|
2611
|
+
});
|
|
2612
|
+
});
|
|
2613
|
+
}
|
|
2554
2614
|
async flushOutbox(taskId) {
|
|
2555
2615
|
const entries = taskId
|
|
2556
2616
|
? await this.dependencies.outbox.listForTask(taskId)
|
|
2557
2617
|
: await this.dependencies.outbox.list();
|
|
2618
|
+
const groups = new Map();
|
|
2619
|
+
for (const entry of entries) {
|
|
2620
|
+
const owner = taskIdFromDeliverySafe(entry);
|
|
2621
|
+
const body = objectOrEmpty(entry.body);
|
|
2622
|
+
const key = owner ? 'task:' + owner : 'project:' + String(body.projectId ?? 'unscoped');
|
|
2623
|
+
const group = groups.get(key) ?? [];
|
|
2624
|
+
group.push(entry);
|
|
2625
|
+
groups.set(key, group);
|
|
2626
|
+
}
|
|
2558
2627
|
const synchronized = [];
|
|
2559
2628
|
const blocked = [];
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
const
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2629
|
+
await Promise.all([...groups].map(async ([key, group]) => {
|
|
2630
|
+
const previous = this.deliveryQueues.get(key) ?? Promise.resolve();
|
|
2631
|
+
const delivery = previous
|
|
2632
|
+
.catch(() => undefined)
|
|
2633
|
+
.then(async () => {
|
|
2634
|
+
if (this.activeDeliveries >= 4)
|
|
2635
|
+
await new Promise((resolve) => this.deliveryWaiters.push(resolve));
|
|
2636
|
+
else
|
|
2637
|
+
this.activeDeliveries += 1;
|
|
2638
|
+
try {
|
|
2639
|
+
for (const candidate of group) {
|
|
2640
|
+
const entry = await this.dependencies.outbox.get(candidate.id);
|
|
2641
|
+
if (!entry)
|
|
2642
|
+
continue;
|
|
2643
|
+
try {
|
|
2644
|
+
await this.deliverEntry(entry);
|
|
2645
|
+
synchronized.push(entry.id);
|
|
2646
|
+
}
|
|
2647
|
+
catch (error) {
|
|
2648
|
+
const errorKind = classifyError(error);
|
|
2649
|
+
await this.dependencies.outbox.markAttempt(entry.id, errorKind);
|
|
2650
|
+
if (entry.journalRef)
|
|
2651
|
+
await this.dependencies.journal.markDeliveryAttempt(entry.journalRef.projectId, entry.journalRef.taskSlug, entry.journalRef.eventId, errorKind);
|
|
2652
|
+
blocked.push({ id: entry.id, error: errorKind });
|
|
2653
|
+
break;
|
|
2654
|
+
}
|
|
2578
2655
|
}
|
|
2579
2656
|
}
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
if (entry.journalRef) {
|
|
2587
|
-
await this.dependencies.journal.markDeliveryAttempt(entry.journalRef.projectId, entry.journalRef.taskSlug, entry.journalRef.eventId, errorKind);
|
|
2657
|
+
finally {
|
|
2658
|
+
const waiter = this.deliveryWaiters.shift();
|
|
2659
|
+
if (waiter)
|
|
2660
|
+
waiter();
|
|
2661
|
+
else
|
|
2662
|
+
this.activeDeliveries -= 1;
|
|
2588
2663
|
}
|
|
2589
|
-
|
|
2590
|
-
|
|
2664
|
+
});
|
|
2665
|
+
this.deliveryQueues.set(key, delivery);
|
|
2666
|
+
try {
|
|
2667
|
+
await delivery;
|
|
2591
2668
|
}
|
|
2592
|
-
|
|
2669
|
+
finally {
|
|
2670
|
+
if (this.deliveryQueues.get(key) === delivery)
|
|
2671
|
+
this.deliveryQueues.delete(key);
|
|
2672
|
+
}
|
|
2673
|
+
}));
|
|
2593
2674
|
return asJsonValue({
|
|
2594
2675
|
synchronized,
|
|
2595
2676
|
blocked,
|
|
2596
2677
|
pending: (await this.dependencies.outbox.list()).length,
|
|
2597
2678
|
});
|
|
2598
2679
|
}
|
|
2680
|
+
async deliverEntry(entry) {
|
|
2681
|
+
let recovered = false;
|
|
2682
|
+
let deliveredData = null;
|
|
2683
|
+
if (entry.operation === 'task.self_review') {
|
|
2684
|
+
const body = objectOrEmpty(entry.body);
|
|
2685
|
+
if (!entry.projectId)
|
|
2686
|
+
throw refuse('The receipt has no project identity; inspect and re-record it.', 'task.resolve_pending_delivery');
|
|
2687
|
+
const pointer = await this.dependencies.activeContexts.findByTaskId(String(body.taskId), entry.projectId);
|
|
2688
|
+
const response = await this.dependencies.client.request(endpoints.sessionResume, {
|
|
2689
|
+
method: 'POST',
|
|
2690
|
+
body: {
|
|
2691
|
+
sessionId: pointer.sessionId,
|
|
2692
|
+
repoFingerprint: pointer.repoFingerprint,
|
|
2693
|
+
source: pointer.source ?? null,
|
|
2694
|
+
afterSequence: 0,
|
|
2695
|
+
},
|
|
2696
|
+
});
|
|
2697
|
+
const backend = objectValue(response.data);
|
|
2698
|
+
const task = objectValue(backend?.task);
|
|
2699
|
+
if (!task ||
|
|
2700
|
+
task.id !== body.taskId ||
|
|
2701
|
+
task.projectId !== entry.projectId ||
|
|
2702
|
+
backend?.requiresContextRefresh === true)
|
|
2703
|
+
throw refuse('Refresh the matching task context before delivering this review.', 'session.resume');
|
|
2704
|
+
recovered = Boolean(task &&
|
|
2705
|
+
task.id === body.taskId &&
|
|
2706
|
+
task.lockVersion === Number(body.expectedTaskVersion) + 1 &&
|
|
2707
|
+
task.selfReviewDiffHash === body.diffHash &&
|
|
2708
|
+
stableStringify(asJsonValue(task.selfReviewResourceIds)) ===
|
|
2709
|
+
stableStringify(asJsonValue(body.reviewedResourceIds)) &&
|
|
2710
|
+
stableStringify(asJsonValue(task.selfReviewFindings)) ===
|
|
2711
|
+
stableStringify(asJsonValue(body.findings)));
|
|
2712
|
+
if (recovered)
|
|
2713
|
+
await this.captureTaskSnapshot(response.data);
|
|
2714
|
+
}
|
|
2715
|
+
if (!recovered) {
|
|
2716
|
+
const response = await this.dependencies.client.request(entry.path, {
|
|
2717
|
+
method: entry.method,
|
|
2718
|
+
body: entry.body,
|
|
2719
|
+
idempotencyKey: entry.idempotencyKey,
|
|
2720
|
+
});
|
|
2721
|
+
deliveredData = response.data;
|
|
2722
|
+
await this.captureTaskSnapshot(entry.operation === 'task.self_review'
|
|
2723
|
+
? asJsonValue({ task: response.data })
|
|
2724
|
+
: response.data);
|
|
2725
|
+
}
|
|
2726
|
+
if (entry.journalRef) {
|
|
2727
|
+
await this.dependencies.journal.markDeliverySynchronized(entry.journalRef.projectId, entry.journalRef.taskSlug, entry.journalRef.eventId);
|
|
2728
|
+
const owner = taskIdFromDeliverySafe(entry);
|
|
2729
|
+
if (owner) {
|
|
2730
|
+
const pointer = await this.dependencies.activeContexts.findByTaskId(owner, entry.journalRef.projectId, false);
|
|
2731
|
+
await this.seedResumeSnapshot(pointer, objectValue(deliveredData) ?? undefined);
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
await this.dependencies.outbox.acknowledge(entry.id);
|
|
2735
|
+
}
|
|
2599
2736
|
async recoverJournalOutbox(projectId, taskSlug) {
|
|
2600
2737
|
const pending = await this.dependencies.journal.listPendingDeliveries(projectId, taskSlug);
|
|
2601
2738
|
for (const delivery of pending) {
|
|
@@ -3367,6 +3504,15 @@ function expectedTaskVersionFromBody(value) {
|
|
|
3367
3504
|
? numericTaskVersion(body.expectedTaskVersion)
|
|
3368
3505
|
: null;
|
|
3369
3506
|
}
|
|
3507
|
+
function pendingTaskVersion(current, entries) {
|
|
3508
|
+
return Math.max(current, ...entries.map((entry) => {
|
|
3509
|
+
const version = expectedTaskVersionFromBody(entry.body);
|
|
3510
|
+
if (version === null)
|
|
3511
|
+
return current;
|
|
3512
|
+
const advances = ['task.checkpoint', 'task.record_correction', 'task.self_review'].includes(entry.operation);
|
|
3513
|
+
return version + (advances ? 1 : 0);
|
|
3514
|
+
}));
|
|
3515
|
+
}
|
|
3370
3516
|
function taskIdFromDeliverySafe(entry) {
|
|
3371
3517
|
try {
|
|
3372
3518
|
return taskIdFromDelivery(entry.body);
|
|
@@ -4,7 +4,9 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { assertManagedPath, ensureManagedDirectory, readJson, removeFile, writeJson, } from '../utilities/files.js';
|
|
5
5
|
import { sha256, stableStringify } from '../utilities/hash.js';
|
|
6
6
|
const sensitiveKeyPattern = /(authorization|password|passcode|secret|token|cookie|email|phone|mobile|customer|account|user.?id|user.?name)/i;
|
|
7
|
-
const
|
|
7
|
+
const emailSource = '(?:^|[^\\p{L}\\p{N}._%+-])[\\p{L}\\p{N}._%+-]{1,64}@([\\p{L}\\p{N}.-]+\\.[\\p{L}]{2,63})(?=$|[^\\p{L}\\p{N}.-])';
|
|
8
|
+
const emailPattern = new RegExp(emailSource, 'iu');
|
|
9
|
+
const emailMatchPattern = new RegExp(emailSource, 'giu');
|
|
8
10
|
const phonePattern = /\+\d[\d ()-]{7,}\d/;
|
|
9
11
|
const repositoryPathFields = new Set([
|
|
10
12
|
'path',
|
|
@@ -13,21 +15,128 @@ const repositoryPathFields = new Set([
|
|
|
13
15
|
'changedPaths',
|
|
14
16
|
'pathPatterns',
|
|
15
17
|
'evidencePaths',
|
|
18
|
+
'screenPathPatterns',
|
|
19
|
+
'componentPathPatterns',
|
|
20
|
+
'pathChanges',
|
|
21
|
+
'templatePath',
|
|
16
22
|
]);
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
const topLevelDomains = new Set([
|
|
24
|
+
'com',
|
|
25
|
+
'net',
|
|
26
|
+
'org',
|
|
27
|
+
'edu',
|
|
28
|
+
'gov',
|
|
29
|
+
'mil',
|
|
30
|
+
'int',
|
|
31
|
+
'info',
|
|
32
|
+
'biz',
|
|
33
|
+
'name',
|
|
34
|
+
'pro',
|
|
35
|
+
'mobi',
|
|
36
|
+
'tel',
|
|
37
|
+
'asia',
|
|
38
|
+
'jobs',
|
|
39
|
+
'coop',
|
|
40
|
+
'aero',
|
|
41
|
+
'museum',
|
|
42
|
+
'travel',
|
|
43
|
+
'app',
|
|
44
|
+
'dev',
|
|
45
|
+
'cloud',
|
|
46
|
+
'online',
|
|
47
|
+
'site',
|
|
48
|
+
'store',
|
|
49
|
+
'tech',
|
|
50
|
+
'space',
|
|
51
|
+
'website',
|
|
52
|
+
'digital',
|
|
53
|
+
'agency',
|
|
54
|
+
'systems',
|
|
55
|
+
'solutions',
|
|
56
|
+
'email',
|
|
57
|
+
'xyz',
|
|
58
|
+
'top',
|
|
59
|
+
'club',
|
|
60
|
+
'live',
|
|
61
|
+
'life',
|
|
62
|
+
'world',
|
|
63
|
+
'today',
|
|
64
|
+
'network',
|
|
65
|
+
'group',
|
|
66
|
+
'company',
|
|
67
|
+
'center',
|
|
68
|
+
'media',
|
|
69
|
+
'studio',
|
|
70
|
+
'design',
|
|
71
|
+
'software',
|
|
72
|
+
'tools',
|
|
73
|
+
'works',
|
|
74
|
+
'team',
|
|
75
|
+
'zone',
|
|
76
|
+
'one',
|
|
77
|
+
'ltd',
|
|
78
|
+
'inc',
|
|
79
|
+
'llc',
|
|
80
|
+
'gmbh',
|
|
81
|
+
'shop',
|
|
82
|
+
'page',
|
|
83
|
+
'blog',
|
|
84
|
+
'wiki',
|
|
85
|
+
'news',
|
|
86
|
+
'art',
|
|
87
|
+
'link',
|
|
88
|
+
'host',
|
|
89
|
+
'press',
|
|
90
|
+
'fyi',
|
|
91
|
+
'test',
|
|
92
|
+
'example',
|
|
93
|
+
'invalid',
|
|
94
|
+
'local',
|
|
95
|
+
'localhost',
|
|
96
|
+
'internal',
|
|
97
|
+
]);
|
|
98
|
+
const countryCodeTopLevelDomains = new Set('ac ad ae af ag ai al am ao aq ar as at au aw ax az ba bb bd be bf bg bh bi bj bm bn bo br bs bt bw by bz ca cc cd cf cg ch ci ck cl cm cn co cr cu cv cw cx cy cz de dj dk dm do dz ec ee eg er es et eu fi fj fk fm fo fr ga gd ge gf gg gh gi gl gm gn gp gq gr gs gt gu gw gy hk hm hn hr ht hu id ie il im in io iq ir is it je jm jo jp ke kg kh ki km kn kp kr kw ky kz la lb lc li lk lr ls lt lu lv ly ma mc md me mg mh mk ml mm mn mo mp mq mr ms mt mu mv mw mx my mz na nc ne nf ng ni nl no np nr nu nz om pa pe pf pg ph pk pl pm pn pr ps pt pw py qa re ro rs ru rw sa sb sc sd se sg sh si sk sl sm sn so sr ss st su sv sx sy sz tc td tf tg th tj tk tl tm tn to tr tt tv tw tz ua ug uk us uy uz va vc ve vg vi vn vu wf ws ye yt za zm zw'.split(' '));
|
|
99
|
+
function isTopLevelDomain(label) {
|
|
100
|
+
const value = label.toLowerCase();
|
|
101
|
+
return countryCodeTopLevelDomains.has(value) || topLevelDomains.has(value);
|
|
102
|
+
}
|
|
103
|
+
const extensionTopLevelDomains = new Set([
|
|
104
|
+
'ai',
|
|
105
|
+
'app',
|
|
106
|
+
'as',
|
|
107
|
+
'cc',
|
|
108
|
+
'md',
|
|
109
|
+
'ml',
|
|
110
|
+
'mm',
|
|
111
|
+
'pl',
|
|
112
|
+
'py',
|
|
113
|
+
'rs',
|
|
114
|
+
'sh',
|
|
115
|
+
'so',
|
|
116
|
+
]);
|
|
117
|
+
function hasEmailAddress(value, repositoryPath) {
|
|
118
|
+
if (!repositoryPath)
|
|
119
|
+
return emailPattern.test(value);
|
|
120
|
+
for (const match of value.matchAll(emailMatchPattern)) {
|
|
121
|
+
const labels = (match[1] ?? '').split('.');
|
|
122
|
+
const last = (labels[labels.length - 1] ?? '').toLowerCase();
|
|
123
|
+
if (isTopLevelDomain(last) && !extensionTopLevelDomains.has(last))
|
|
124
|
+
return true;
|
|
125
|
+
const beforeExtension = (labels[labels.length - 2] ?? '').toLowerCase();
|
|
126
|
+
if (labels.length >= 3 &&
|
|
127
|
+
topLevelDomains.has(beforeExtension) &&
|
|
128
|
+
!extensionTopLevelDomains.has(beforeExtension))
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
24
132
|
}
|
|
133
|
+
const knownCredentialPattern = /\b(?:A(?:KI|SI)A[0-9A-Z]{16}|gh[pousr]_[A-Z0-9]{30,}|github_pat_[A-Z0-9_]{30,}|sk_live_[A-Z0-9]{16,}|rk_live_[A-Z0-9]{16,}|xox[baprs]-[A-Z0-9-]{12,}|AIza[A-Z0-9_-]{35}|sk-(?:proj-)?[A-Z0-9_-]{20,})\b/i;
|
|
25
134
|
const bearerPattern = /\bbearer\s+[a-z0-9._~-]+/i;
|
|
26
135
|
const jwtPattern = /\beyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/;
|
|
27
136
|
const privateKeyPattern = /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/;
|
|
28
137
|
const credentialUrlPattern = /\b[a-z][a-z0-9+.-]*:\/\/[^\s/@:]+:[^\s/@]+@/i;
|
|
29
138
|
const ipv4Pattern = /\b(?:\d{1,3}\.){3}\d{1,3}\b/;
|
|
30
|
-
const personalHomePathPattern = /(
|
|
139
|
+
const personalHomePathPattern = /(?:[A-Z]:[\\/](?:Users|Documents and Settings)[\\/][^\\/\s"'<>:|?*]+|(?:^|[\s"'(<=])\/(?:Users|home)\/[^/\s"'<>*]+)(?:[\\/]|$)/i;
|
|
31
140
|
export class OfflineOutbox {
|
|
32
141
|
root;
|
|
33
142
|
queues = new Map();
|
|
@@ -36,12 +145,14 @@ export class OfflineOutbox {
|
|
|
36
145
|
}
|
|
37
146
|
async enqueue(input) {
|
|
38
147
|
assertSafeToPersist(input.body);
|
|
148
|
+
if (input.projectId)
|
|
149
|
+
assertSafeToPersist(input.projectId);
|
|
39
150
|
if (input.journalRef) {
|
|
40
151
|
assertSafeToPersist(JSON.parse(JSON.stringify(input.journalRef)));
|
|
41
152
|
}
|
|
42
153
|
const idempotencyKey = input.idempotencyKey ?? randomUUID();
|
|
43
154
|
const id = sha256(`${input.operation}\n${idempotencyKey}`);
|
|
44
|
-
return await this.exclusive(
|
|
155
|
+
return await this.exclusive('enqueue', async () => {
|
|
45
156
|
const path = this.pathFor(id);
|
|
46
157
|
const existing = await readJson(path, this.root);
|
|
47
158
|
if (existing) {
|
|
@@ -52,6 +163,7 @@ export class OfflineOutbox {
|
|
|
52
163
|
body: input.body,
|
|
53
164
|
idempotencyKey,
|
|
54
165
|
journalRef: input.journalRef ?? null,
|
|
166
|
+
projectId: input.projectId ?? null,
|
|
55
167
|
});
|
|
56
168
|
const actual = stableStringify({
|
|
57
169
|
operation: existing.operation,
|
|
@@ -60,13 +172,17 @@ export class OfflineOutbox {
|
|
|
60
172
|
body: existing.body,
|
|
61
173
|
idempotencyKey: existing.idempotencyKey,
|
|
62
174
|
journalRef: existing.journalRef ?? null,
|
|
175
|
+
projectId: existing.projectId ?? null,
|
|
63
176
|
});
|
|
64
177
|
if (actual !== expected) {
|
|
65
178
|
throw new Error('Outbox idempotency key was reused with different content');
|
|
66
179
|
}
|
|
67
180
|
return existing;
|
|
68
181
|
}
|
|
182
|
+
const order = 1 + Math.max(0, ...(await this.list()).map((entry) => entry.queueOrder ?? 0));
|
|
69
183
|
const entry = {
|
|
184
|
+
queueOrder: order,
|
|
185
|
+
...(input.projectId ? { projectId: input.projectId } : {}),
|
|
70
186
|
id,
|
|
71
187
|
operation: input.operation,
|
|
72
188
|
method: input.method,
|
|
@@ -171,15 +287,17 @@ export function assertSafeToPersist(value, key = '') {
|
|
|
171
287
|
throw new Error(`Sensitive field cannot be persisted: ${key}`);
|
|
172
288
|
}
|
|
173
289
|
if (typeof value === 'string') {
|
|
174
|
-
|
|
290
|
+
const repositoryPath = repositoryPathFields.has(key);
|
|
291
|
+
if (hasEmailAddress(value, repositoryPath) ||
|
|
175
292
|
phonePattern.test(value) ||
|
|
176
293
|
bearerPattern.test(value) ||
|
|
177
294
|
jwtPattern.test(value) ||
|
|
178
295
|
privateKeyPattern.test(value) ||
|
|
296
|
+
knownCredentialPattern.test(value) ||
|
|
179
297
|
credentialUrlPattern.test(value) ||
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
throw new Error(
|
|
298
|
+
personalHomePathPattern.test(value) ||
|
|
299
|
+
(!repositoryPath && ipv4Pattern.test(value))) {
|
|
300
|
+
throw new Error(`PII or credentials cannot be persisted: ${key}`);
|
|
183
301
|
}
|
|
184
302
|
return;
|
|
185
303
|
}
|
|
@@ -286,6 +404,10 @@ function compareEntries(left, right) {
|
|
|
286
404
|
leftBody.expectedTaskVersion !== rightBody.expectedTaskVersion) {
|
|
287
405
|
return leftBody.expectedTaskVersion - rightBody.expectedTaskVersion;
|
|
288
406
|
}
|
|
407
|
+
if (left.queueOrder !== undefined &&
|
|
408
|
+
right.queueOrder !== undefined &&
|
|
409
|
+
left.queueOrder !== right.queueOrder)
|
|
410
|
+
return left.queueOrder - right.queueOrder;
|
|
289
411
|
return left.createdAt === right.createdAt
|
|
290
412
|
? left.id.localeCompare(right.id)
|
|
291
413
|
: left.createdAt.localeCompare(right.createdAt);
|
package/skill/SKILL.md
CHANGED
|
@@ -13,6 +13,8 @@ Mandatory behavior:
|
|
|
13
13
|
|
|
14
14
|
Use the host's native questionnaire for every question to the user, including implementation choices, names, clarification, branch/worktree decisions and delivery. For a required decision, read [questionnaires.md](references/questionnaires.md), then use `questionnaire.ask` to open a durable native MCP form and `questionnaire.resume` to return to the same unanswered question. Never use request_user_input_async for a required decision: it does not wait for an answer. When the durable form is unavailable, use a blocking native control only where the host permits it: request_user_input in Codex or AskUserQuestion in Claude. Never replace the questionnaire with a chat instruction such as 'type this', 'reply yes', or 'write X if you want Y'. Do not open a survey web page. If the required native control is unavailable or prohibited for that kind of question, follow the host's tool restrictions, explain the limitation, and continue only work already authorized; do not fabricate a survey or silently choose an answer. A timeout, dismissed form, empty response or ended turn is not an answer. Keep the decision pending and resume it; do not start dependent work or report it as resolved. Existing answers remain valid through retries and handoffs.
|
|
15
15
|
|
|
16
|
+
Do not block independent task work on `memory.propose_revision` drafting, submission or approval. Follow [memory-updates.md](references/memory-updates.md): use background agents or concurrent tools when the host permits them and continue useful work; without concurrency, checkpoint the pending draft and defer submission until needed. Wait only at the operation that depends on the proposal or revision. This scheduling rule applies to every project and AI host, while native approval and required task verification remain in force. Apply the same dependency rule to reconciliation, self-review, scaffold receipts, inventory upload, completed-unit evidence and test-result reporting. A bridge result with deliveryStatus pending means durable local recording, so continue independent work without polling or resending. Keep claim acquisition, required approval, edit leases and final verification as real dependencies.
|
|
17
|
+
|
|
16
18
|
1. Discover the repository binding through `session.entry`. Bindings live in the user-level Engineering Memory state directory, outside the repository and installed runtime. No project settings file is required. For a bound repository, the project's knowledge is in the backend, so answer nothing about it before bootstrapping; the absence of local design files or records says nothing about its stored knowledge.
|
|
17
19
|
2. Call `session.entry` before answering anything in a repository, and act on what it reports before the message itself: sign in when it says so, ask for organization and project when nothing has been decided, and stay completely silent about Engineering Memory in a repository where the user switched it off. Record every one of those answers with `session.set_decision`, and only ever from something the user actually said.
|
|
18
20
|
3. Before planning or editing, call `session.bootstrap`. After compaction, a new chat, interruption, or handoff, call `session.resume` first.
|
|
@@ -75,9 +75,9 @@ Name the areas this task works in, against the coverage map the core pack carrie
|
|
|
75
75
|
|
|
76
76
|
Do not pull history for everything. Pull it for the records the task actually touches, and for anything the header shows a surprising number of corrections on. Record `task.checkpoint` with type `discovery` and update STATE, DECISIONS, DISCOVERY, and HANDOFF projections through the bridge.
|
|
77
77
|
|
|
78
|
-
Send calls that do not feed each other in one batch rather than one at a time: reads of any kind, and proposals for different records. Reconcile every record the task touched in a single `task.reconcile` call with `entries`, not one call per record.
|
|
78
|
+
Send calls that do not feed each other in one batch rather than one at a time: reads of any kind, and proposals for different records. Proposal drafting, submission and pending approval do not block independent task work. Use host-authorized background agents or concurrent tools and continue the next independent unit; do not immediately wait for the worker. If concurrency is unavailable, checkpoint the pending draft and defer submission until an operation needs it. Follow `references/memory-updates.md` for ownership, delivery tracking and recovery. Reconcile every record the task touched in a single `task.reconcile` call with `entries`, not one call per record.
|
|
79
79
|
|
|
80
|
-
Checkpoints, recorded corrections and
|
|
80
|
+
Checkpoints, recorded corrections, reconciliations, self-reviews and scaffold application receipts return before the backend has them, reporting `deliveryStatus: 'pending'` with the task version they will occupy. The local recording call is complete; backend delivery remains pending and is already under way. Do not wait for it, poll it, or send it again before a dependent operation. Collect a proposal's identifiers when its approval or reconciliation needs them, and an approved revision before work that depends on its changed contract. A prepared lease, required synchronization and verification must succeed before the operations that require them; unrelated proposal work is never a prerequisite for continuing this task.
|
|
81
81
|
|
|
82
82
|
Temporary code written to reach or force a path — a pinned state, a fixed service response, a jump straight to the screen — is allowed and expected, carries the marker `ENGINEERING-MEMORY-TEMPORARY` immediately followed by a colon and its reason, and is removed before verification. `task.verify` refuses while any marker is in the tree and names every line, and the commit gate refuses while one is staged.
|
|
83
83
|
|
|
@@ -4,6 +4,53 @@ Backend resources are immutable revisions. The local agent drafts structured con
|
|
|
4
4
|
|
|
5
5
|
Use `memory.propose_revision` for project profiles, engineering rules, service contracts, localization contracts, navigation contracts, state contracts, screen logic, component mappings, Figma mappings, current deviations, quality gates, task history, and architecture templates.
|
|
6
6
|
|
|
7
|
+
## Continue independent work while proposals progress
|
|
8
|
+
|
|
9
|
+
Do not block independent task work on proposal drafting, submission or approval. This is the usage contract for every project, user and AI host, including Codex and Claude. Decide whether the next operation actually needs the proposal ID or its approved revision; the existence of a pending proposal is not itself a reason to wait.
|
|
10
|
+
|
|
11
|
+
When the host permits background agents or concurrent tool calls, delegate a bounded set of independent proposals and immediately continue useful work. Collect that worker's result only at a real dependency, rather than spawning a worker and immediately waiting for it. Keep one writer per resource; proposals for different resources may progress together. Keep task-version-changing lifecycle mutations ordered under the owning agent, and handle a resulting version conflict through the normal recovery instead of weakening version checks.
|
|
12
|
+
|
|
13
|
+
Pass the worker the exact project, task/session and source-run identity that applies, resource identity, baseRevision, verified evidence, proposed selectors and scope. Reuse the owning task's context; delegation neither opens another engineering task nor grants application-edit, approval or delivery authority. Do not pass credentials or claim tokens in prompts or saved documents. The main agent owns native questionnaires and may review or activate a proposal only after the user's explicit decision.
|
|
14
|
+
|
|
15
|
+
If background execution is unavailable or prohibited, preserve the pending draft and its evidence through `task.checkpoint` documents and continue the independent work first. Submit the draft when an operation needs its result. Never claim a background worker exists on a serial-only host or skip the proposal permanently. An approved architectural or flow change that implementation depends on is still a prerequisite for that implementation.
|
|
16
|
+
|
|
17
|
+
Track each draft, in-flight call, returned proposal ID, queued delivery and failure in the task's checkpoint documents, with its resource/base revision and the bridge-returned delivery identifiers when available. The MCP call still waits for its own real response: `queued: false` reports a stored inactive proposal, while `queued: true` reports a durable outbox entry, not an approved revision. Reuse the existing outbox delivery and its idempotency key through the supported recovery; do not invoke a fresh proposal call merely to retry an already queued one. If the response is lost, use `memory.list_proposals` to check for the exact proposed content before resubmitting. Compare a changed base revision rather than overwriting it.
|
|
18
|
+
|
|
19
|
+
For example, while a worker records a finished screen's contract, the main agent can inspect an unrelated service or implement an already-approved component. If the next screen needs a changed flow contract, wait for that specific contract's native approval before implementing it. During adoption or refresh, pending proposals for one unit do not stop inspection of independent units. Required proposals, approvals and reconciliations must still be settled before claiming their dependent unit or the whole inspection complete. Unrelated proposal approval never becomes a prerequisite for another task.
|
|
20
|
+
|
|
21
|
+
Before a dependent reconciliation, verification or handoff, collect the relevant results and report pending or failed work accurately. Do not close or describe unfinished required memory work as complete. A dismissed questionnaire, timeout or deferred submission is not consent.
|
|
22
|
+
|
|
23
|
+
## Independent backend recording beyond proposals
|
|
24
|
+
|
|
25
|
+
Use the same dependency test for every backend write. Recording finished work is not by itself a
|
|
26
|
+
reason to idle the implementation agent. The local bridge durably queues checkpoints, corrections,
|
|
27
|
+
reconciliations, self-reviews and scaffold receipts. Once it returns deliveryStatus pending,
|
|
28
|
+
continue independent work; do not poll, resend or describe delivery as completed. Pending task
|
|
29
|
+
mutations keep their own order. Different tasks have independent delivery lanes, and a new task
|
|
30
|
+
does not wait for another task's outbox. Task-less entries retain project ordering.
|
|
31
|
+
|
|
32
|
+
Self-review records the exact diff it reviewed; record it again after any later edit. Scaffold
|
|
33
|
+
receipts name the actual file hashes. The backend still checks current authority and expected task
|
|
34
|
+
versions when a receipt arrives. A failure is retained for session.resume and
|
|
35
|
+
task.resolve_pending_delivery; only the affected task waits at its dependent verification.
|
|
36
|
+
A response-lost self-review is acknowledged only when its authorized backend snapshot proves the
|
|
37
|
+
exact recorded effect. Do not discard a failed receipt to make verification pass.
|
|
38
|
+
|
|
39
|
+
Inventory upload, completed-unit evidence submission and test-result reporting still return their
|
|
40
|
+
real server response. When the host supports concurrent execution, let those calls run while reading
|
|
41
|
+
an independent source area or running an unrelated local validation. The owner tracks the in-flight
|
|
42
|
+
operation and collects the result before the next operation needs it. Without concurrency, preserve
|
|
43
|
+
safe pending evidence in checkpoints and schedule submission before its first dependency. Do not
|
|
44
|
+
defer time-sensitive claim renewal; never save a claim token in a journal or generic outbox.
|
|
45
|
+
|
|
46
|
+
A new run or task ID, sealed inventory required for planning, a claim required for ownership, an
|
|
47
|
+
approved contract required for implementation, a lease and final verify/close are true dependencies.
|
|
48
|
+
Likewise a work-item status or assignment change may govern the next action: use its result before
|
|
49
|
+
that action. This is not permission to perform unapproved administrative writes or to invent an
|
|
50
|
+
always-running worker. Background execution must use the host's permitted facilities.
|
|
51
|
+
|
|
52
|
+
## Revision scope and review
|
|
53
|
+
|
|
7
54
|
Knowledge is layered. `scope: product` proposes a change to the shared engineering core that every organization reads, `scope: organization` proposes one that only this organization reads and which hides the product text for that key, and `scope: project` proposes a record for this project alone. The nearest layer wins when context is delivered. A product-scope proposal is the way a developer improves the product itself from inside their own project, and it requires nothing but the skill. It remains inactive for the configured product release principal to review; waiting in that platform queue does not prevent the contributor's task from verifying or closing.
|
|
8
55
|
|
|
9
56
|
An architecture template is organization-scoped source, not prose. When a task establishes or changes a shared architecture structure that the templates carry, the template is stale and needs its own revision. Its manifest and content must stay in step: one `files[]` entry per `## file:` block, same path, byte count, SHA-256 and order. See `scaffolding.md`.
|
|
@@ -235,3 +235,14 @@ required for verified delivery. Verified resume, close and commit attestation do
|
|
|
235
235
|
counterpart memory or depend on a counterpart link/membership that was revoked after verification.
|
|
236
236
|
Fresh linked-source selection and work still check current access. Publishing new source memory
|
|
237
237
|
with memory.publish_task is a separate operation and still requires current linked authority.
|
|
238
|
+
|
|
239
|
+
## Delivery while independent analysis continues
|
|
240
|
+
|
|
241
|
+
Use the scheduling contract in memory-updates.md during adoption and refresh too. A committed
|
|
242
|
+
source can be read independently while its inventory upload is in flight. Wait for sealed inventory
|
|
243
|
+
before dependent planning/claims; never report the upload complete from a run identifier alone.
|
|
244
|
+
A finished unit's memory.sync_submit can run alongside analysis of another independently claimed
|
|
245
|
+
unit. Retain the exact request and returned result; renew claims before expiry through the owning
|
|
246
|
+
agent, without sharing claim tokens in prompts or persisting them in the outbox. Collect all required
|
|
247
|
+
submissions and exact approvals before coverage verification. Serial-only hosts schedule independent
|
|
248
|
+
reading first when safe, and submit before the next dependency instead of pretending to run a worker.
|