engineering-memory 1.11.26 → 1.11.27
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/build.json +1 -1
- package/runtime/dist/src/config.js +1 -0
- package/runtime/dist/src/mcp/delivery-tools.js +29 -0
- package/runtime/dist/src/mcp/tool-annotations.js +1 -0
- package/runtime/dist/src/mcp/tool-definitions.js +1 -0
- package/runtime/dist/src/runtime/active-context-store.js +19 -9
- package/runtime/dist/src/runtime/api-client.js +1 -0
- package/runtime/dist/src/runtime/bridge-service.js +61 -10
- package/runtime/dist/src/runtime/worktree-pool.js +9 -0
- package/skill/references/lifecycle.md +6 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engineering-memory",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.27",
|
|
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
|
@@ -70,6 +70,7 @@ export const endpoints = {
|
|
|
70
70
|
taskDeliveryAnswer: (projectId, taskId) => `/projects/${projectId}/tasks/${taskId}/delivery/answer`,
|
|
71
71
|
taskDeliveryDeliver: (projectId, taskId) => `/projects/${projectId}/tasks/${taskId}/delivery/deliver`,
|
|
72
72
|
taskDeliveryCancel: (projectId, taskId) => `/projects/${projectId}/tasks/${taskId}/delivery/cancel`,
|
|
73
|
+
taskReviewRequest: (projectId, taskId) => `/projects/${projectId}/tasks/${taskId}/review-request`,
|
|
73
74
|
projectSetup: '/projects/setup',
|
|
74
75
|
projectList: '/projects',
|
|
75
76
|
projectResolve: '/projects/resolve',
|
|
@@ -66,6 +66,7 @@ const recordSchema = z.object({
|
|
|
66
66
|
});
|
|
67
67
|
export function registerDeliveryTools(server, service) {
|
|
68
68
|
registerTaskDelivery(server, service);
|
|
69
|
+
registerTaskReviewRequest(server, service);
|
|
69
70
|
server.registerTool('task.close', {
|
|
70
71
|
description: 'Close the verified task, then open its durable mode-governed Git delivery selector in the same call. Memory publication is separate. Cancellation/feedback leave delivery pending; retry identical input to resume. PR/MR is one review-request concept, not an instruction to merge. No Git action is performed by this tool. Read-only tasks need no delivery. Supply complete copy when the product text catalogue lacks the conversation language. If the user already explicitly chose Git delivery in their own message, deliveryInstruction preserves that choice with its exact relevant excerpt; it is an agent-reported instruction, never a native answer or a substitute for missing user consent. Do not use it for agent decisions or memory-publication approval.',
|
|
71
72
|
inputSchema: z.object({
|
|
@@ -507,6 +508,34 @@ function registerTaskDelivery(server, service) {
|
|
|
507
508
|
});
|
|
508
509
|
});
|
|
509
510
|
}
|
|
511
|
+
function registerTaskReviewRequest(server, service) {
|
|
512
|
+
server.registerTool('task.review_request', {
|
|
513
|
+
description: "Record the pull request (GitHub) or merge request (GitLab) a closed task's changes went into, and its state: draft, ready, merged or closed. Call it with the link copied from the provider as soon as the pull request is opened, and again when the user says it became ready, was merged or was closed. Engineering Memory then moves the task's work item as the project's workflow rules say, for example to PR when every pull request of the work item is ready and to READY FOR QA when they are all merged. It works from any chat, also for a delivery session.entry lists. What is recorded is what was reported: Engineering Memory does not check it with the provider, and performs no Git or provider action.",
|
|
514
|
+
inputSchema: z.strictObject({
|
|
515
|
+
projectId: z.string().uuid(),
|
|
516
|
+
taskId: z.string().uuid(),
|
|
517
|
+
url: z.string().trim().min(1).max(500),
|
|
518
|
+
state: z.enum(['draft', 'ready', 'merged', 'closed']),
|
|
519
|
+
}),
|
|
520
|
+
}, async (input) => {
|
|
521
|
+
const result = await service.taskReviewRequest(input);
|
|
522
|
+
if (!result.ok)
|
|
523
|
+
return output(result);
|
|
524
|
+
const recorded = result.data;
|
|
525
|
+
return output({
|
|
526
|
+
ok: true,
|
|
527
|
+
data: {
|
|
528
|
+
...result.data,
|
|
529
|
+
nextAction: recorded.queued
|
|
530
|
+
? 'Engineering Memory is unreachable, so this report is queued. It is recorded, and the work item moves as the rules say, when Engineering Memory is reachable again.'
|
|
531
|
+
: (recorded.data?.reviewEvent?.nextAction ??
|
|
532
|
+
(recorded.data?.reviewEvent
|
|
533
|
+
? 'The pull request is recorded; the work item stays where it is.'
|
|
534
|
+
: 'The pull request is recorded. The work item does not move yet: other work on it is still open or undelivered, or the task has no active work item.')),
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
}
|
|
510
539
|
function askPullRequestBase(server, service, context, input, language, copy, owner) {
|
|
511
540
|
return askQuestionnaire(server, service, {
|
|
512
541
|
repoRoot: input.repoRoot,
|
|
@@ -78,6 +78,7 @@ export const toolAnnotations = {
|
|
|
78
78
|
'task.verify': write,
|
|
79
79
|
'task.close': write,
|
|
80
80
|
'task.delivery': write,
|
|
81
|
+
'task.review_request': repeatableWrite,
|
|
81
82
|
'task.abandon': destructive,
|
|
82
83
|
'task.branch': outward, // fetches the chosen base from the Git remote
|
|
83
84
|
'architecture.plan': read,
|
|
@@ -122,7 +122,7 @@ export class ActiveContextStore {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
-
async setChangeBaseline(repoFingerprint, sessionId, manifest, leasePaths, taskSnapshot) {
|
|
125
|
+
async setChangeBaseline(repoFingerprint, sessionId, manifest, leasePaths, taskSnapshot, selfReviewRequired) {
|
|
126
126
|
const pointer = taskSnapshot
|
|
127
127
|
? await this.loadForTask(repoFingerprint, taskSnapshot.taskId)
|
|
128
128
|
: await this.loadForSession(repoFingerprint, sessionId);
|
|
@@ -140,13 +140,12 @@ export class ActiveContextStore {
|
|
|
140
140
|
lastSequence: taskSnapshot.lastSequence,
|
|
141
141
|
}
|
|
142
142
|
: {}),
|
|
143
|
-
changeBaseline:
|
|
144
|
-
|
|
145
|
-
:
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
},
|
|
143
|
+
changeBaseline: {
|
|
144
|
+
diffHash: pointer.changeBaseline?.diffHash ?? manifest.diffHash,
|
|
145
|
+
changedPaths: pointer.changeBaseline?.changedPaths ?? manifest.changedPaths,
|
|
146
|
+
leasePaths,
|
|
147
|
+
...(selfReviewRequired ? { selfReviewRequired } : {}),
|
|
148
|
+
},
|
|
150
149
|
});
|
|
151
150
|
}
|
|
152
151
|
async setVerificationIntent(repoFingerprint, input) {
|
|
@@ -399,7 +398,11 @@ export class ActiveContextStore {
|
|
|
399
398
|
pointer.changeBaseline.changedPaths.some((entry) => !isChangedPath(entry)) ||
|
|
400
399
|
!Array.isArray(pointer.changeBaseline.leasePaths) ||
|
|
401
400
|
pointer.changeBaseline.leasePaths.some((path) => !isRepositoryRelative(path)) ||
|
|
402
|
-
new Set(pointer.changeBaseline.leasePaths).size !==
|
|
401
|
+
new Set(pointer.changeBaseline.leasePaths).size !==
|
|
402
|
+
pointer.changeBaseline.leasePaths.length ||
|
|
403
|
+
(pointer.changeBaseline.selfReviewRequired !== undefined &&
|
|
404
|
+
(!Array.isArray(pointer.changeBaseline.selfReviewRequired) ||
|
|
405
|
+
!pointer.changeBaseline.selfReviewRequired.every(isSelfReviewRecord)))) {
|
|
403
406
|
throw new Error('Active Engineering Memory change baseline is invalid');
|
|
404
407
|
}
|
|
405
408
|
}
|
|
@@ -429,6 +432,13 @@ function isDurableIntent(value) {
|
|
|
429
432
|
typeof value.createdAt === 'string' &&
|
|
430
433
|
Number.isFinite(Date.parse(value.createdAt)));
|
|
431
434
|
}
|
|
435
|
+
function isSelfReviewRecord(value) {
|
|
436
|
+
return (typeof value?.resourceId === 'string' &&
|
|
437
|
+
typeof value.resourceKey === 'string' &&
|
|
438
|
+
typeof value.title === 'string' &&
|
|
439
|
+
Array.isArray(value.paths) &&
|
|
440
|
+
value.paths.every((path) => typeof path === 'string'));
|
|
441
|
+
}
|
|
432
442
|
function isChangedPath(value) {
|
|
433
443
|
return (typeof value.path === 'string' &&
|
|
434
444
|
isRepositoryRelative(value.path) &&
|
|
@@ -51,6 +51,7 @@ export const backendRecoveryOperationNames = [
|
|
|
51
51
|
'task.branch',
|
|
52
52
|
'task.resolve_pending_delivery',
|
|
53
53
|
'task.delivery',
|
|
54
|
+
'task.review_request',
|
|
54
55
|
];
|
|
55
56
|
const backendRecoveryOperations = new Set(backendRecoveryOperationNames);
|
|
56
57
|
const browserSigninRecovery = 'auth.signin_browser';
|
|
@@ -987,6 +987,7 @@ export class BridgeService {
|
|
|
987
987
|
await this.live.taskStarted(backendTask.id);
|
|
988
988
|
}
|
|
989
989
|
const backendFresh = responseSource !== 'stale_cache';
|
|
990
|
+
const deliveryState = objectValue(backend.taskDelivery)?.state;
|
|
990
991
|
return asJsonValue({
|
|
991
992
|
backend: resumeBackendView(backend, backendFresh || !localJournal.projection),
|
|
992
993
|
requirements: backend.requirements ?? null,
|
|
@@ -999,7 +1000,12 @@ export class BridgeService {
|
|
|
999
1000
|
staleReason: backend.staleReason ?? null,
|
|
1000
1001
|
nextAction: contextRefreshAction(backend.staleReason, resumedBaseline),
|
|
1001
1002
|
}
|
|
1002
|
-
:
|
|
1003
|
+
: backendTask.status === 'closed' &&
|
|
1004
|
+
(deliveryState === 'unanswered' || deliveryState === 'answered')
|
|
1005
|
+
? {
|
|
1006
|
+
nextAction: 'This task is closed and its changes are not delivered yet. Its delivery continues as before: task.close in this folder asks the delivery question. If the user asks for a further change instead, call context.prepare_change with this sessionId and the paths the change touches: that reopens the task, and the next task.close asks the delivery question again for the new content.',
|
|
1007
|
+
}
|
|
1008
|
+
: {}),
|
|
1003
1009
|
repository: publicRepository(repository),
|
|
1004
1010
|
worktree: (await this.ownedAllocation(projectId, repository.repoRoot)) ?? null,
|
|
1005
1011
|
localJournal: backendFresh ? journalSummary(localJournal) : localJournal,
|
|
@@ -1165,6 +1171,9 @@ export class BridgeService {
|
|
|
1165
1171
|
const response = await this.dependencies.client.request(endpoints.contextPrepareChange, {
|
|
1166
1172
|
method: 'POST',
|
|
1167
1173
|
body,
|
|
1174
|
+
...(this.dependencies.clientVersion
|
|
1175
|
+
? { headers: { 'x-client-version': this.dependencies.clientVersion } }
|
|
1176
|
+
: {}),
|
|
1168
1177
|
...(input.transitionToWrite
|
|
1169
1178
|
? {}
|
|
1170
1179
|
: {
|
|
@@ -1174,6 +1183,8 @@ export class BridgeService {
|
|
|
1174
1183
|
});
|
|
1175
1184
|
const responseSource = this.dependencies.client.getResponseSource(response);
|
|
1176
1185
|
if (responseSource === 'stale_cache') {
|
|
1186
|
+
if (pointer.closedAt)
|
|
1187
|
+
throw refuse('This task is closed, and reopening it for a further change needs Engineering Memory, which cannot be reached now. Try again once it is reachable.', 'context.prepare_change');
|
|
1177
1188
|
validateOfflineLease(response.data, input.sessionId, changedPaths, baselineDiffHash);
|
|
1178
1189
|
await this.dependencies.activeContexts.setChangeBaseline(repository.repoFingerprint, input.sessionId, repository.git, changedPaths);
|
|
1179
1190
|
return asJsonValue({
|
|
@@ -1192,7 +1203,8 @@ export class BridgeService {
|
|
|
1192
1203
|
resumeConflicts: pointer.resumeConflicts.filter((value) => value !== 'backend_resume_is_stale'),
|
|
1193
1204
|
});
|
|
1194
1205
|
}
|
|
1195
|
-
const
|
|
1206
|
+
const preparedData = objectValue(response.data);
|
|
1207
|
+
const preparedTask = objectValue(preparedData?.task);
|
|
1196
1208
|
await this.dependencies.activeContexts.setChangeBaseline(repository.repoFingerprint, input.sessionId, repository.git, changedPaths, preparedTask &&
|
|
1197
1209
|
typeof preparedTask.id === 'string' &&
|
|
1198
1210
|
typeof preparedTask.lockVersion === 'number'
|
|
@@ -1201,8 +1213,16 @@ export class BridgeService {
|
|
|
1201
1213
|
taskVersion: numericTaskVersion(preparedTask.lockVersion),
|
|
1202
1214
|
lastSequence: numericSequence(preparedTask.lastSequence),
|
|
1203
1215
|
}
|
|
1204
|
-
: undefined);
|
|
1205
|
-
|
|
1216
|
+
: undefined, selfReviewRecords(preparedData?.selfReviewRequired));
|
|
1217
|
+
let preparedPointer = await this.dependencies.activeContexts.loadForSession(repository.repoFingerprint, input.sessionId);
|
|
1218
|
+
if (preparedPointer?.closedAt && preparedTask?.status === 'open') {
|
|
1219
|
+
const allocation = await this.ownedAllocation(preparedPointer.projectId, repository.repoRoot);
|
|
1220
|
+
if (allocation && this.dependencies.worktreePool)
|
|
1221
|
+
await this.dependencies.worktreePool.reopen(preparedPointer.projectId, repository.repoRoot, allocation.generation);
|
|
1222
|
+
const { closedAt: _closedAt, ...reopened } = preparedPointer;
|
|
1223
|
+
await this.dependencies.activeContexts.save(reopened);
|
|
1224
|
+
preparedPointer = reopened;
|
|
1225
|
+
}
|
|
1206
1226
|
if (preparedPointer) {
|
|
1207
1227
|
if (decision) {
|
|
1208
1228
|
await this.dependencies.repositories.git
|
|
@@ -1221,7 +1241,6 @@ export class BridgeService {
|
|
|
1221
1241
|
allocation.taskId = preparedPointer.taskId;
|
|
1222
1242
|
}
|
|
1223
1243
|
}
|
|
1224
|
-
const preparedData = objectValue(response.data);
|
|
1225
1244
|
await this.seedResumeSnapshot(preparedPointer, {
|
|
1226
1245
|
...objectOrEmpty(response.data),
|
|
1227
1246
|
...(preparedData?.lease ? { activeLease: preparedData.lease } : {}),
|
|
@@ -1910,10 +1929,18 @@ export class BridgeService {
|
|
|
1910
1929
|
}
|
|
1911
1930
|
async taskSelfReview(input) {
|
|
1912
1931
|
const reviewed = await this.execute(async () => {
|
|
1932
|
+
const checkout = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd());
|
|
1933
|
+
const pointer = await this.dependencies.activeContexts.loadForTask(checkout.repoFingerprint, input.taskId);
|
|
1934
|
+
const named = new Set(input.files.flatMap(({ rules }) => rules.map((rule) => rule.resourceId)));
|
|
1935
|
+
const leasePaths = pointer?.changeBaseline?.leasePaths ?? [];
|
|
1936
|
+
const unnamed = (pointer?.changeBaseline?.selfReviewRequired ?? []).filter(({ resourceId }) => !named.has(resourceId));
|
|
1937
|
+
if (unnamed.length > 0)
|
|
1938
|
+
throw refuse(`This self review leaves out records the change lease requires, so task.verify would refuse it: ${unnamed
|
|
1939
|
+
.map(({ resourceKey, title, paths }) => `${resourceKey} "${title}" (${paths.length === leasePaths.length ? 'every leased path' : paths.join(', ') || 'no single path'})`)
|
|
1940
|
+
.join('; ')}. Read the changed files against each of them and name it on a changed file it covers, or on any changed file when it covers no single path; then record task.self_review again. Nothing was recorded.`, 'task.self_review');
|
|
1913
1941
|
const files = await this.approvedDeviations(input);
|
|
1914
1942
|
assertSafeToPersist(cleanJson({ files }));
|
|
1915
|
-
const
|
|
1916
|
-
const repository = await this.taskRepository(checkout, await this.dependencies.activeContexts.loadForTask(checkout.repoFingerprint, input.taskId));
|
|
1943
|
+
const repository = await this.taskRepository(checkout, pointer);
|
|
1917
1944
|
if (this.deferDeliveries) {
|
|
1918
1945
|
return this.enqueueTaskReceipt('task.self_review', endpoints.taskSelfReview, input.taskId, repository.repoRoot, cleanJson({
|
|
1919
1946
|
diffHash: repository.git.diffHash,
|
|
@@ -4519,9 +4546,9 @@ export class BridgeService {
|
|
|
4519
4546
|
const taskEntries = (await this.dependencies.outbox.list()).filter((entry) => taskIdFromDeliverySafe(entry) === input.taskId);
|
|
4520
4547
|
const pointer = await this.resolveCheckpointPointer(input, repoRoot);
|
|
4521
4548
|
if (pointer.closedAt && correction)
|
|
4522
|
-
throw refuse(`Task ${pointer.taskSlug} is closed, so it takes no more corrections.
|
|
4549
|
+
throw refuse(`Task ${pointer.taskSlug} is closed, so it takes no more corrections. When the correction asks for a further code change and the task's changes are not delivered yet, call context.prepare_change in its session first: that reopens the task, and the correction is then recorded on it. Otherwise record it as a proposal for the rule it concerns with memory.propose_revision, or in the task that next changes this code.`, 'memory.propose_revision');
|
|
4523
4550
|
if (pointer.closedAt)
|
|
4524
|
-
throw refuse(`Task ${pointer.taskSlug} is closed, so it takes no more checkpoints.
|
|
4551
|
+
throw refuse(`Task ${pointer.taskSlug} is closed, so it takes no more checkpoints. While its changes are not delivered, a further change continues it: call context.prepare_change in its session with the paths the change touches, which reopens it. Work after its delivery belongs to a new task.`, 'context.prepare_change');
|
|
4525
4552
|
const blockedEntry = taskEntries.find((entry) => entry.lastError && entry.lastError !== 'backend_unavailable');
|
|
4526
4553
|
if (blockedEntry && blockedEntry.idempotencyKey !== idempotencyKey) {
|
|
4527
4554
|
throw refuse('A blocked task delivery must be explicitly resolved before checkpointing', 'task.resolve_pending_delivery');
|
|
@@ -4643,6 +4670,9 @@ export class BridgeService {
|
|
|
4643
4670
|
async taskDeliveryCancel(input) {
|
|
4644
4671
|
return this.execute(() => this.mutateWithOutbox('task.delivery', endpoints.taskDeliveryCancel(input.projectId, input.taskId), { reason: input.reason }));
|
|
4645
4672
|
}
|
|
4673
|
+
async taskReviewRequest(input) {
|
|
4674
|
+
return this.execute(() => this.mutateWithOutbox('task.review_request', endpoints.taskReviewRequest(input.projectId, input.taskId), { url: input.url, state: input.state }));
|
|
4675
|
+
}
|
|
4646
4676
|
async taskDeliveryRecord(input) {
|
|
4647
4677
|
return this.execute(async () => {
|
|
4648
4678
|
const response = await this.dependencies.client.request(endpoints.taskDelivery(input.projectId, input.taskId));
|
|
@@ -5669,7 +5699,7 @@ function deliveryQuestion(touchedContract, destination, publishApplicable, commi
|
|
|
5669
5699
|
alsoAsk: 'This task changed an endpoint contract. Ask whether to document the change for the linked project, and if so write what the other side must do differently with memory.propose_revision as an integration_note.',
|
|
5670
5700
|
}
|
|
5671
5701
|
: {}),
|
|
5672
|
-
afterPullRequest: 'Do not end the turn once a pull request exists.
|
|
5702
|
+
afterPullRequest: 'Do not end the turn once a pull request exists. Record it with task.review_request: its link, and draft or ready as it was opened. Then check whether it merges cleanly, report the conflicting files if it does not, and ask whether to resolve them before touching anything. When the user later says it became ready, was merged or was closed, record that with task.review_request too.',
|
|
5673
5703
|
afterCommit,
|
|
5674
5704
|
});
|
|
5675
5705
|
}
|
|
@@ -5732,6 +5762,27 @@ function checkpointView(checkpoint) {
|
|
|
5732
5762
|
documents: documentViews(checkpoint.documents),
|
|
5733
5763
|
};
|
|
5734
5764
|
}
|
|
5765
|
+
function selfReviewRecords(value) {
|
|
5766
|
+
if (!Array.isArray(value))
|
|
5767
|
+
return undefined;
|
|
5768
|
+
return value.flatMap((entry) => {
|
|
5769
|
+
const record = objectValue(entry);
|
|
5770
|
+
return record &&
|
|
5771
|
+
typeof record.resourceId === 'string' &&
|
|
5772
|
+
typeof record.resourceKey === 'string' &&
|
|
5773
|
+
typeof record.title === 'string' &&
|
|
5774
|
+
Array.isArray(record.paths)
|
|
5775
|
+
? [
|
|
5776
|
+
{
|
|
5777
|
+
resourceId: record.resourceId,
|
|
5778
|
+
resourceKey: record.resourceKey,
|
|
5779
|
+
title: record.title,
|
|
5780
|
+
paths: record.paths.filter((path) => typeof path === 'string'),
|
|
5781
|
+
},
|
|
5782
|
+
]
|
|
5783
|
+
: [];
|
|
5784
|
+
});
|
|
5785
|
+
}
|
|
5735
5786
|
function resumeBackendView(backend, includeDocuments) {
|
|
5736
5787
|
const rest = Object.fromEntries(Object.entries(backend).filter(([key]) => key !== 'requirements'));
|
|
5737
5788
|
const task = objectValue(backend.task);
|
|
@@ -588,6 +588,15 @@ export class WorktreePool {
|
|
|
588
588
|
await this.save(registry);
|
|
589
589
|
});
|
|
590
590
|
}
|
|
591
|
+
async reopen(projectId, repoRoot, generation) {
|
|
592
|
+
return this.transaction(async (registry) => {
|
|
593
|
+
const entry = this.owned(registry, projectId, repoRoot, generation);
|
|
594
|
+
entry.phase = 'working';
|
|
595
|
+
entry.pendingDelivery = false;
|
|
596
|
+
entry.lastActivityAt = this.now();
|
|
597
|
+
await this.save(registry);
|
|
598
|
+
});
|
|
599
|
+
}
|
|
591
600
|
async release(projectId, repoRoot, generation, deliveryOutcome) {
|
|
592
601
|
return this.transaction(async (registry) => {
|
|
593
602
|
const entry = await this.deliveryOwned(registry, projectId, repoRoot, generation);
|
|
@@ -254,7 +254,7 @@ A project-specific correction is offered as task-only or permanent for this proj
|
|
|
254
254
|
|
|
255
255
|
Before validation, read the changed code back against the rules that govern it. Not from memory: reread the returned records for the paths that changed, including anything the context pack deferred, and read the diff as the next person to open the file would. The restraint document is the first thing to hold it against.
|
|
256
256
|
|
|
257
|
-
`context.prepare_change` returns `governingRules`: for each changed path, the rules its role in the project profile binds it to. Record `task.self_review` with one entry per changed file (deleted files excepted) that names every one of those rules, and for a file the profile maps to no role, the engineering rules you actually read it against. Each rule gets one outcome:
|
|
257
|
+
`context.prepare_change` returns `governingRules`: for each changed path, the rules its role in the project profile binds it to. Record `task.self_review` with one entry per changed file (deleted files excepted) that names every one of those rules, and for a file the profile maps to no role, the engineering rules you actually read it against. It also returns `selfReviewRequired`: every record the change lease is taken against — the records of the leased paths, deferred ones included, and the project profile — each with the leased paths it covers. The review names each of them too, on a changed file it covers, or on any changed file when it covers no single path; `task.self_review` refuses a review that leaves one out and records nothing, and `task.verify` would refuse it as well. Each rule gets one outcome:
|
|
258
258
|
|
|
259
259
|
- `follows`: the file does what the rule says.
|
|
260
260
|
- `fixed`: it did not, and you changed it; record what was wrong and what you changed.
|
|
@@ -286,6 +286,8 @@ Record `handoff_before`, then call `task.verify`. For a write task, verify the e
|
|
|
286
286
|
|
|
287
287
|
A verified task is not frozen. When a change is needed after verification — a product manager's update, a fix the user asks for, a correction — call `context.prepare_change` for the paths it touches: that reopens the task and keeps everything recorded so far. A correction, a checkpoint or a self review reopens it as well. `task.reconcile` and `memory.propose_revision` on a verified task refuse with `Task already verified` and recovery `context.prepare_change`; call it, then repeat the refused call. Make the change, review it with `task.self_review`, run the validations again with `validation_before`, `validation_after` and `handoff_before` recorded around them, and verify again before `task.close`.
|
|
288
288
|
|
|
289
|
+
A closed task whose changes are not delivered yet is not frozen either. When the user asks for a further change before the delivery — after trying the branch, or a new request before the push — call `context.prepare_change` in the task's session with the paths the change touches; in a new chat, `session.resume` shows that session. It reopens the task (`reopenedFromClose: true`), keeps everything recorded so far and clears the delivery answer, so the next `task.close` asks the delivery question again for the new content. Until then a checkpoint, correction, self review or reconciliation on the closed task refuses with `Task closed before delivery` and recovery `context.prepare_change`. Once its delivery is recorded as delivered or concluded, the task stays closed: start the next change as new work with `task.branch`, which can continue on the same branch.
|
|
290
|
+
|
|
289
291
|
When a task adds a structure the system did not have — a cache, a broker, a read replica, a second deployable, a projection, an event store — name it in `introducedStructures` at verification. The backend refuses any the project profile has not recorded under `architecture.adopted`, with the pressure it relieves, and refuses with the stated reason any the project recorded as deliberately declined. Recording it is a project profile revision like any other: propose, have the user approve, reconcile. Do not reach for the structure first and record it afterwards — the point of the record is that somebody decided.
|
|
290
292
|
|
|
291
293
|
A new screen or component fails verification until its memory exists, which takes four steps in order: `memory.propose_revision` for each new path, the user's explicit approval, `memory.review_proposal`, then `task.reconcile`. The error names the paths. Walk the chain rather than retrying the same verification.
|
|
@@ -294,7 +296,9 @@ Call `task.close` only after verification and only when the current diff still m
|
|
|
294
296
|
|
|
295
297
|
After memory approval, finish any required reconciliation and verification, then call `task.close` in the same turn; do not stop at publishing memory or at a successful verify result. Closing is not the end of the turn either. The MCP `task.close` call closes the verified task and opens its durable, mode-governed delivery selector itself: commit, commit and push, commit and push with a draft PR/MR, commit and push with a PR/MR, or keep it for now. Do not open a duplicate delivery questionnaire. Follow its pending/delegated/native result and retry the same call until a real decision is recorded. Cancellation and feedback preserve the closed worktree and pending delivery. PR/MR target selection is a second question when the target is not already explicitly supplied; merging is separate and never implied. An explicit Git delivery instruction already present in the user's own message can be passed as `deliveryInstruction` with its exact relevant `userRequestExcerpt`, choice and any explicit baseBranch. It is reported as an agent-reported user instruction, not a native answer; never use memory approval or an agent preference as that instruction. Read-only tasks have no Git delivery form. Perform only the selected and authorized Git action; the tool itself commits, pushes and merges nothing. When the host has to approve a push, follow the Delivery section of `questionnaires.md`; an Engineering Memory form is never the way past a host's denial.
|
|
296
298
|
|
|
297
|
-
When a pull request has been opened, keep going: check whether it merges cleanly, report the result with the link, and if it conflicts, name the files and ask whether to resolve them. Never end the turn on a pull request whose mergeability was never checked, and never resolve a conflict without being told to.
|
|
299
|
+
When a pull request has been opened, keep going: record it with `task.review_request` — its link as the provider shows it, and `draft` or `ready` as it was opened — then check whether it merges cleanly, report the result with the link, and if it conflicts, name the files and ask whether to resolve them. Never end the turn on a pull request whose mergeability was never checked, and never resolve a conflict without being told to.
|
|
300
|
+
|
|
301
|
+
When the user later says a pull request became ready, was merged or was closed, record that state with `task.review_request` too; it works from any chat, also for a delivery `session.entry` lists. Engineering Memory then moves the task's work item as the project's workflow rules say: to the `pr_opened` target once every pull request of the work item is ready, to the `all_prs_merged` target (READY FOR QA by default) once they are all merged, and to the draft target when one is still a draft and the project names one. It waits while another task of the work item is still open, or closed with a delivery that has no pull request yet. Relay the returned `nextAction` in one line. The record is what was reported: Engineering Memory does not check it with the provider, so never report a merge the user has not confirmed. A merge is final, and a delivery keeps the first pull request recorded for it. GitHub pull request and GitLab merge request links are recorded; for another provider, move the work item with `work_item.update`.
|
|
298
302
|
|
|
299
303
|
## Code source and memory applicability
|
|
300
304
|
|