engineering-memory 1.11.25 → 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/git/git-inspector.js +20 -9
- package/runtime/dist/src/git/verification-gate.js +2 -3
- package/runtime/dist/src/mcp/delivery-tools.js +40 -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 +112 -24
- package/runtime/dist/src/runtime/worktree-pool.js +13 -2
- package/skill/references/lifecycle.md +12 -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',
|
|
@@ -375,10 +375,10 @@ export class GitInspector {
|
|
|
375
375
|
diffHash: sha256(`${head ?? '<unborn>'}\n${stableStringify(hashManifest)}\n`),
|
|
376
376
|
};
|
|
377
377
|
}
|
|
378
|
-
async manifestAgainst(repoRoot, baseCommit) {
|
|
378
|
+
async manifestAgainst(repoRoot, baseCommit, from = baseCommit) {
|
|
379
379
|
const root = await this.findRoot(repoRoot);
|
|
380
380
|
const head = await this.gitValue(root, ['rev-parse', 'HEAD']);
|
|
381
|
-
const changedPaths = await this.changedPathsAgainstHead(root,
|
|
381
|
+
const changedPaths = await this.changedPathsAgainstHead(root, from);
|
|
382
382
|
const hashManifest = canonicalHashManifest(changedPaths);
|
|
383
383
|
return {
|
|
384
384
|
repoRoot: root,
|
|
@@ -386,9 +386,26 @@ export class GitInspector {
|
|
|
386
386
|
baseCommit,
|
|
387
387
|
changedPaths,
|
|
388
388
|
worktreeHash: sha256(`${stableStringify(hashManifest)}\n`),
|
|
389
|
-
diffHash: sha256(`${
|
|
389
|
+
diffHash: sha256(`${from}\n${head ?? '<unborn>'}\n${stableStringify(hashManifest)}\n`),
|
|
390
390
|
};
|
|
391
391
|
}
|
|
392
|
+
async taskBase(repoRoot, baseCommit, branch) {
|
|
393
|
+
const listed = await this.runner.run('git', ['rev-list', '--boundary', 'HEAD', '--not', baseCommit, `--exclude=*/${branch}`, '--remotes'], { cwd: repoRoot });
|
|
394
|
+
if (listed.exitCode !== 0)
|
|
395
|
+
return baseCommit;
|
|
396
|
+
const upstream = [];
|
|
397
|
+
for (const line of listed.stdout.split(/\r?\n/)) {
|
|
398
|
+
if (line.startsWith('-') && (await this.isAncestor(repoRoot, baseCommit, line.slice(1))))
|
|
399
|
+
upstream.push(line.slice(1));
|
|
400
|
+
}
|
|
401
|
+
if (upstream.length === 0)
|
|
402
|
+
return baseCommit;
|
|
403
|
+
const newest = await this.runner.run('git', ['merge-base', '--independent', ...upstream], {
|
|
404
|
+
cwd: repoRoot,
|
|
405
|
+
});
|
|
406
|
+
const tips = newest.exitCode === 0 ? newest.stdout.split(/\r?\n/).filter(Boolean) : [];
|
|
407
|
+
return tips.length === 1 ? tips[0] : baseCommit;
|
|
408
|
+
}
|
|
392
409
|
async isAncestor(repoRoot, ancestor, descendant = 'HEAD') {
|
|
393
410
|
const result = await this.runner.run('git', ['merge-base', '--is-ancestor', ancestor, descendant], { cwd: repoRoot });
|
|
394
411
|
return result.exitCode === 0;
|
|
@@ -403,12 +420,6 @@ export class GitInspector {
|
|
|
403
420
|
'refs/remotes/',
|
|
404
421
|
]);
|
|
405
422
|
}
|
|
406
|
-
async committedPaths(repoRoot, baseCommit) {
|
|
407
|
-
const result = await this.runner.run('git', ['diff', '--name-only', '-z', '--find-renames', baseCommit, 'HEAD', '--'], { cwd: repoRoot });
|
|
408
|
-
if (result.exitCode !== 0)
|
|
409
|
-
throw new Error(`Git committed diff failed: ${result.stderr.trim()}`);
|
|
410
|
-
return result.stdout.split('\0').filter(Boolean).map(normalizeGitPath).sort();
|
|
411
|
-
}
|
|
412
423
|
async stagedManifest(repoRoot) {
|
|
413
424
|
const root = await this.findRoot(repoRoot);
|
|
414
425
|
let result = await this.runner.run('git', ['diff', '--cached', '--name-status', '-z', '--find-renames', '--find-copies', 'HEAD', '--'], { cwd: root });
|
|
@@ -155,9 +155,8 @@ export class VerificationGate {
|
|
|
155
155
|
receipt.source &&
|
|
156
156
|
actualSource &&
|
|
157
157
|
actualSource.sourceCommit !== receipt.source.sourceCommit &&
|
|
158
|
-
(await this.git.isAncestor(repository.repoRoot, receipt.source.sourceCommit))
|
|
159
|
-
|
|
160
|
-
? await this.git.manifestAgainst(repository.repoRoot, receipt.source.sourceCommit)
|
|
158
|
+
(await this.git.isAncestor(repository.repoRoot, receipt.source.sourceCommit))
|
|
159
|
+
? await this.git.manifestAgainst(repository.repoRoot, receipt.source.sourceCommit, await this.git.taskBase(repository.repoRoot, receipt.source.sourceCommit, receipt.branch))
|
|
161
160
|
: null;
|
|
162
161
|
const committedAhead = ahead?.diffHash === receipt.diffHash;
|
|
163
162
|
const current = ahead && committedAhead ? ahead : manifest;
|
|
@@ -39,6 +39,7 @@ const closedSchema = z.object({
|
|
|
39
39
|
pushUrl: z.string().nullable(),
|
|
40
40
|
headCommit: z.string().nullable(),
|
|
41
41
|
}),
|
|
42
|
+
alreadyDelivered: z.object({ commit: z.string(), ref: z.string() }).optional(),
|
|
42
43
|
}),
|
|
43
44
|
deliveryRecord: z
|
|
44
45
|
.object({
|
|
@@ -65,6 +66,7 @@ const recordSchema = z.object({
|
|
|
65
66
|
});
|
|
66
67
|
export function registerDeliveryTools(server, service) {
|
|
67
68
|
registerTaskDelivery(server, service);
|
|
69
|
+
registerTaskReviewRequest(server, service);
|
|
68
70
|
server.registerTool('task.close', {
|
|
69
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.',
|
|
70
72
|
inputSchema: z.object({
|
|
@@ -119,6 +121,16 @@ export function registerDeliveryTools(server, service) {
|
|
|
119
121
|
const data = parsed.data;
|
|
120
122
|
if (data.deliveryContext.mode === 'read_only')
|
|
121
123
|
return output(closed);
|
|
124
|
+
const delivered = data.delivery.alreadyDelivered;
|
|
125
|
+
if (delivered)
|
|
126
|
+
return output({
|
|
127
|
+
ok: true,
|
|
128
|
+
data: {
|
|
129
|
+
...closed.data,
|
|
130
|
+
deliveryStatus: 'delivered',
|
|
131
|
+
nextAction: `Everything this task changed was already committed and pushed by hand: ${delivered.ref.replace(/^refs\/remotes\//, '')} contains ${delivered.commit.slice(0, 12)}. There is nothing left to deliver, so no delivery question is asked, and the delivery is recorded for the project. Call worktree.release in this folder with deliveryOutcome delivered to settle the folder.`,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
122
134
|
const destination = data.delivery.destination;
|
|
123
135
|
if (!destination.branch)
|
|
124
136
|
return output({
|
|
@@ -496,6 +508,34 @@ function registerTaskDelivery(server, service) {
|
|
|
496
508
|
});
|
|
497
509
|
});
|
|
498
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
|
+
}
|
|
499
539
|
function askPullRequestBase(server, service, context, input, language, copy, owner) {
|
|
500
540
|
return askQuestionnaire(server, service, {
|
|
501
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,
|
|
@@ -1105,7 +1111,7 @@ export class BridgeService {
|
|
|
1105
1111
|
body: {
|
|
1106
1112
|
sessionId: input.sessionId,
|
|
1107
1113
|
repoFingerprint: repository.repoFingerprint,
|
|
1108
|
-
source: (await this.checkoutSource(repository, pointer.source)) ?? null,
|
|
1114
|
+
source: (await this.checkoutSource(await this.taskRepository(repository, pointer), pointer.source)) ?? null,
|
|
1109
1115
|
afterSequence: 0,
|
|
1110
1116
|
},
|
|
1111
1117
|
})).data);
|
|
@@ -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,
|
|
@@ -2388,6 +2415,7 @@ export class BridgeService {
|
|
|
2388
2415
|
taskChanges,
|
|
2389
2416
|
});
|
|
2390
2417
|
await this.dependencies.activeContexts.clearVerificationIntent(repository.repoFingerprint, input.taskId);
|
|
2418
|
+
await this.dependencies.activeContexts.updateTaskSnapshot(input.taskId, taskVersion, pointer.lastSequence);
|
|
2391
2419
|
}
|
|
2392
2420
|
else {
|
|
2393
2421
|
await this.dependencies.activeContexts.clearVerificationIntent(repository.repoFingerprint, input.taskId);
|
|
@@ -2412,11 +2440,12 @@ export class BridgeService {
|
|
|
2412
2440
|
}
|
|
2413
2441
|
async validationWaiverSubject(input) {
|
|
2414
2442
|
return await this.execute(async () => {
|
|
2415
|
-
const
|
|
2416
|
-
const pointer = await this.requireActivePointer(
|
|
2443
|
+
const checkout = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd());
|
|
2444
|
+
const pointer = await this.requireActivePointer(checkout.repoFingerprint, input.taskId, false, 'verify');
|
|
2417
2445
|
if (!pointer.changeBaseline) {
|
|
2418
2446
|
throw refuse('This task has no local change baseline, so what it changed cannot be measured. The baseline is restored from the task record.', 'session.resume');
|
|
2419
2447
|
}
|
|
2448
|
+
const repository = await this.taskRepository(checkout, pointer);
|
|
2420
2449
|
const response = await this.dependencies.client.request(endpoints.sessionResume, {
|
|
2421
2450
|
method: 'POST',
|
|
2422
2451
|
body: {
|
|
@@ -4517,9 +4546,9 @@ export class BridgeService {
|
|
|
4517
4546
|
const taskEntries = (await this.dependencies.outbox.list()).filter((entry) => taskIdFromDeliverySafe(entry) === input.taskId);
|
|
4518
4547
|
const pointer = await this.resolveCheckpointPointer(input, repoRoot);
|
|
4519
4548
|
if (pointer.closedAt && correction)
|
|
4520
|
-
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');
|
|
4521
4550
|
if (pointer.closedAt)
|
|
4522
|
-
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');
|
|
4523
4552
|
const blockedEntry = taskEntries.find((entry) => entry.lastError && entry.lastError !== 'backend_unavailable');
|
|
4524
4553
|
if (blockedEntry && blockedEntry.idempotencyKey !== idempotencyKey) {
|
|
4525
4554
|
throw refuse('A blocked task delivery must be explicitly resolved before checkpointing', 'task.resolve_pending_delivery');
|
|
@@ -4641,6 +4670,9 @@ export class BridgeService {
|
|
|
4641
4670
|
async taskDeliveryCancel(input) {
|
|
4642
4671
|
return this.execute(() => this.mutateWithOutbox('task.delivery', endpoints.taskDeliveryCancel(input.projectId, input.taskId), { reason: input.reason }));
|
|
4643
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
|
+
}
|
|
4644
4676
|
async taskDeliveryRecord(input) {
|
|
4645
4677
|
return this.execute(async () => {
|
|
4646
4678
|
const response = await this.dependencies.client.request(endpoints.taskDelivery(input.projectId, input.taskId));
|
|
@@ -4866,10 +4898,12 @@ export class BridgeService {
|
|
|
4866
4898
|
!pointer.changeBaseline ||
|
|
4867
4899
|
typeof pointer.branch !== 'string' ||
|
|
4868
4900
|
(await git.currentBranch(repository.repoRoot)) !== pointer.branch ||
|
|
4869
|
-
!(await git.isAncestor(repository.repoRoot, base))
|
|
4870
|
-
!pathsContainAll(pointer.changeBaseline.leasePaths, await git.committedPaths(repository.repoRoot, base)))
|
|
4901
|
+
!(await git.isAncestor(repository.repoRoot, base)))
|
|
4871
4902
|
return repository;
|
|
4872
|
-
return {
|
|
4903
|
+
return {
|
|
4904
|
+
...repository,
|
|
4905
|
+
git: await git.manifestAgainst(repository.repoRoot, base, await git.taskBase(repository.repoRoot, base, pointer.branch)),
|
|
4906
|
+
};
|
|
4873
4907
|
}
|
|
4874
4908
|
async checkoutSource(repository, expected, task) {
|
|
4875
4909
|
const current = await this.dependencies.repositories.git.sourceIdentity(repository.repoRoot);
|
|
@@ -4881,8 +4915,15 @@ export class BridgeService {
|
|
|
4881
4915
|
repository.git.baseCommit === expected.sourceCommit &&
|
|
4882
4916
|
repository.git.head === current.sourceCommit)
|
|
4883
4917
|
return { ...expected };
|
|
4884
|
-
const
|
|
4885
|
-
|
|
4918
|
+
const git = this.dependencies.repositories.git;
|
|
4919
|
+
const branch = await git.currentBranch(repository.repoRoot);
|
|
4920
|
+
const rewritten = branch !== null &&
|
|
4921
|
+
branch === task?.branch &&
|
|
4922
|
+
!(await git.isAncestor(repository.repoRoot, expected.sourceCommit));
|
|
4923
|
+
const way = rewritten
|
|
4924
|
+
? `${branch} no longer contains that commit: it was rewritten onto another history (a rebase, reset or amend). Nothing of the task is lost and nothing has to be abandoned: once ${branch} contains that commit again, for example after rebasing it back onto the branch it started from if the user wants that, call session.resume and the task continues where it stopped.`
|
|
4925
|
+
: `Nothing of the task is lost: switch this checkout back to ${task?.branch ?? 'a branch that contains that commit'} and call session.resume, and the task continues where it stopped. Commits, merges and rebases made on the task branch itself stay the task's work.`;
|
|
4926
|
+
throw refuse(`The checkout no longer matches this task source. ${task?.taskSlug ? 'Task ' + task.taskSlug : 'This task'} was opened${task?.branch ? ' on ' + task.branch : ''} at commit ${expected.sourceCommit.slice(0, 12)}; this checkout is on ${branch ?? 'detached HEAD'} at ${current?.sourceCommit.slice(0, 12) ?? 'no commit'}. ${way}`, 'session.resume');
|
|
4886
4927
|
}
|
|
4887
4928
|
if (current && repository.git.head !== current.sourceCommit) {
|
|
4888
4929
|
throw refuse('The Git source changed during the request. Retry session.resume with a stable checkout.', 'session.resume');
|
|
@@ -5170,6 +5211,19 @@ export class BridgeService {
|
|
|
5170
5211
|
await this.dependencies.repositories.git
|
|
5171
5212
|
.branchStore(repository.repoRoot)
|
|
5172
5213
|
.release(String(body.taskId));
|
|
5214
|
+
const git = this.dependencies.repositories.git;
|
|
5215
|
+
const head = repository.git.head;
|
|
5216
|
+
const onBranch = Boolean(head && head !== sourceFromContext(body.source)?.sourceCommit);
|
|
5217
|
+
const clean = onBranch && (await git.manifest(repository.repoRoot)).changedPaths.length === 0;
|
|
5218
|
+
const onRemote = clean && head ? await git.remoteRefContaining(repository.repoRoot, head) : null;
|
|
5219
|
+
if (onRemote && head && repository.projectId && closedTask.deliveryRecord)
|
|
5220
|
+
await this.reportDeliveryOutcome({
|
|
5221
|
+
projectId: repository.projectId,
|
|
5222
|
+
taskId: String(body.taskId),
|
|
5223
|
+
outcome: 'delivered',
|
|
5224
|
+
commit: head,
|
|
5225
|
+
pushed: true,
|
|
5226
|
+
});
|
|
5173
5227
|
return asJsonValue({
|
|
5174
5228
|
...closedTask,
|
|
5175
5229
|
repository: publicRepository(repository),
|
|
@@ -5180,7 +5234,7 @@ export class BridgeService {
|
|
|
5180
5234
|
diffHash: String(body.diffHash),
|
|
5181
5235
|
mode: normalizeTaskMode(closedPointer?.mode),
|
|
5182
5236
|
},
|
|
5183
|
-
delivery: deliveryQuestion(touchedContract, await
|
|
5237
|
+
delivery: deliveryQuestion(touchedContract, await git.pushDestination(repository.repoRoot), objectValue(closedTask.sourcePublication)?.applicable === true, { onBranch, clean, onRemote }),
|
|
5184
5238
|
});
|
|
5185
5239
|
}
|
|
5186
5240
|
async seedResumeSnapshot(pointer, backendPatch) {
|
|
@@ -5591,9 +5645,24 @@ function entryNextAction(authenticated, decision) {
|
|
|
5591
5645
|
}
|
|
5592
5646
|
return 'Ask which organization and then which project, whatever the user asked for, listing what they already have with the option to create a new one last. Switching Engineering Memory off in this repository is the other answer, and it is remembered.';
|
|
5593
5647
|
}
|
|
5594
|
-
function deliveryQuestion(touchedContract, destination, publishApplicable) {
|
|
5648
|
+
function deliveryQuestion(touchedContract, destination, publishApplicable, committed) {
|
|
5595
5649
|
const { branch, remote, pushUrl, remoteDefaultBranch, headCommit } = destination;
|
|
5650
|
+
const afterCommit = publishApplicable
|
|
5651
|
+
? "After the authorized delivery, call worktree.release in this folder with deliveryOutcome delivered: it publishes this task's source memory and then records the delivery for the project. Its sourcePublication says whether publication happened; call memory.publish_task only when it names a reason you can fix. A task without a worktree to release calls memory.publish_task after the commit instead."
|
|
5652
|
+
: 'This task has nothing to publish (see sourcePublication on the close response); do not call memory.publish_task.';
|
|
5653
|
+
if (committed.onRemote && headCommit)
|
|
5654
|
+
return asJsonValue({
|
|
5655
|
+
required: false,
|
|
5656
|
+
destination,
|
|
5657
|
+
alreadyDelivered: { commit: headCommit, ref: committed.onRemote },
|
|
5658
|
+
afterCommit,
|
|
5659
|
+
});
|
|
5596
5660
|
const onto = branch ? `branch ${branch}` : 'a detached HEAD, which is on no branch';
|
|
5661
|
+
const state = committed.clean
|
|
5662
|
+
? `Everything this task changed is already committed on ${onto}; nothing is left to commit.`
|
|
5663
|
+
: committed.onBranch
|
|
5664
|
+
? `Part of this task's work is already committed on ${onto}; the rest is not committed yet.`
|
|
5665
|
+
: 'The task is closed and nothing has been committed.';
|
|
5597
5666
|
const target = branch && remote ? `${remote} (${pushUrl ?? 'no push URL'}) branch ${branch}` : null;
|
|
5598
5667
|
const head = headCommit ? `, on top of ${headCommit.slice(0, 12)}` : '';
|
|
5599
5668
|
const pushed = target ? `, and a push goes to ${target}` : '';
|
|
@@ -5609,7 +5678,7 @@ function deliveryQuestion(touchedContract, destination, publishApplicable) {
|
|
|
5609
5678
|
: '');
|
|
5610
5679
|
return asJsonValue({
|
|
5611
5680
|
required: true,
|
|
5612
|
-
question:
|
|
5681
|
+
question: `${state} The commit goes on ${onto}${head}${pushed}. If the user already said in their own message which of these they want, that is the answer; do not ask it again. Otherwise ask, and do only what they choose.`,
|
|
5613
5682
|
destination,
|
|
5614
5683
|
options: [
|
|
5615
5684
|
{ id: 'commit', label: `Commit on ${onto}` },
|
|
@@ -5630,10 +5699,8 @@ function deliveryQuestion(touchedContract, destination, publishApplicable) {
|
|
|
5630
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.',
|
|
5631
5700
|
}
|
|
5632
5701
|
: {}),
|
|
5633
|
-
afterPullRequest: 'Do not end the turn once a pull request exists.
|
|
5634
|
-
afterCommit
|
|
5635
|
-
? "After the authorized delivery, call worktree.release in this folder with deliveryOutcome delivered: it publishes this task's source memory and then records the delivery for the project. Its sourcePublication says whether publication happened; call memory.publish_task only when it names a reason you can fix. A task without a worktree to release calls memory.publish_task after the commit instead."
|
|
5636
|
-
: 'This task has nothing to publish (see sourcePublication on the close response); do not call memory.publish_task.',
|
|
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.',
|
|
5703
|
+
afterCommit,
|
|
5637
5704
|
});
|
|
5638
5705
|
}
|
|
5639
5706
|
function cleanJson(value) {
|
|
@@ -5695,6 +5762,27 @@ function checkpointView(checkpoint) {
|
|
|
5695
5762
|
documents: documentViews(checkpoint.documents),
|
|
5696
5763
|
};
|
|
5697
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
|
+
}
|
|
5698
5786
|
function resumeBackendView(backend, includeDocuments) {
|
|
5699
5787
|
const rest = Object.fromEntries(Object.entries(backend).filter(([key]) => key !== 'requirements'));
|
|
5700
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);
|
|
@@ -1169,9 +1178,11 @@ export class WorktreePool {
|
|
|
1169
1178
|
}
|
|
1170
1179
|
async assertIdentity(entry, allowBranchChange = false) {
|
|
1171
1180
|
if (key(await this.git.findRoot(entry.repoRoot)) !== key(await canonicalPath(entry.repoRoot)) ||
|
|
1172
|
-
(await this.commonDirectory(entry.repoRoot)) !== entry.commonDir
|
|
1173
|
-
(!allowBranchChange && (await this.git.currentBranch(entry.repoRoot)) !== entry.branch))
|
|
1181
|
+
(await this.commonDirectory(entry.repoRoot)) !== entry.commonDir)
|
|
1174
1182
|
throw refuse('The worktree Git identity changed. Reconcile it before continuing; the task was not moved or reset.');
|
|
1183
|
+
const branch = allowBranchChange ? entry.branch : await this.git.currentBranch(entry.repoRoot);
|
|
1184
|
+
if (branch !== entry.branch)
|
|
1185
|
+
throw new BridgeRecoveryError(`This folder is on ${branch ?? 'a detached HEAD'}, but its task works on ${entry.branch ?? 'a detached HEAD'}. Nothing of the task is lost: switch this folder back to ${entry.branch ?? 'the commit the task started from'} and call session.resume, and the task continues where it stopped.`, 'session.resume');
|
|
1175
1186
|
const reservation = await this.git.branchStore(entry.repoRoot).read();
|
|
1176
1187
|
if (reservation &&
|
|
1177
1188
|
(reservation.decision.projectId !== entry.projectId ||
|
|
@@ -214,6 +214,10 @@ proved against the current commit. When it cannot prove it, resume reports
|
|
|
214
214
|
list, and do not edit the pointer file: say what happened, and let the user decide between
|
|
215
215
|
reopening the task and abandoning it.
|
|
216
216
|
|
|
217
|
+
## Work committed, merged or rebased by hand
|
|
218
|
+
|
|
219
|
+
Anything Engineering Memory does in Git the developer may do by hand while the task is open: commit on the task branch, merge the base branch into it, rebase it, push it. None of that loses the task, and none of it is a reason to abandon or reset anything. The task's changes are measured the way a pull request of the task branch shows them: against the newest commit the branch took in from the remote's other branches, so a merged-in `develop` is not counted as the task's work, while the task's own commits and its uncommitted files still are. When the checkout is on another branch or a detached HEAD, the refusal names the task branch to switch back to; switch back and call `session.resume`, and the task continues where it stopped. Do not call `worktree.reconcile` for it: that quarantines the folder. When the task branch itself was rewritten so that it no longer contains the commit the task started from (a rebase onto another history, a reset, an amend of that commit), the refusal says so; once that commit is back in the branch, `session.resume` continues the task. When everything the task changed is already committed and a remote branch contains it, `task.close` asks no delivery question: it reports `deliveryStatus: 'delivered'` and records the delivery for the project, and `worktree.release` with `deliveryOutcome:'delivered'` settles the folder.
|
|
220
|
+
|
|
217
221
|
## Change Preparation
|
|
218
222
|
|
|
219
223
|
Skip change preparation for a read-only task. Do not request an edit lease, send changed paths, record `pre_edit`, or reconcile code resources in that mode. If the user later authorizes a change, call `context.prepare_change` with `transitionToWrite: true`, the bridge-owned current task version, intended paths, and current baseline diff. Continue only after the bridge returns the updated write task and lease.
|
|
@@ -250,7 +254,7 @@ A project-specific correction is offered as task-only or permanent for this proj
|
|
|
250
254
|
|
|
251
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.
|
|
252
256
|
|
|
253
|
-
`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:
|
|
254
258
|
|
|
255
259
|
- `follows`: the file does what the rule says.
|
|
256
260
|
- `fixed`: it did not, and you changed it; record what was wrong and what you changed.
|
|
@@ -280,6 +284,10 @@ When `context.prepare_change`'s `requirements` list a path under `mappings`, dra
|
|
|
280
284
|
|
|
281
285
|
Record `handoff_before`, then call `task.verify`. For a write task, verify the exact changed paths, current diff hash, validations, session, and lease. For a read-only task, send no changed paths, lease, or write baseline; the bridge supplies the current Git diff hash and the backend compares it with the baseline captured at bootstrap while also verifying bootstrap, discovery, validation-before, validation-after, handoff, pinned read receipts, and synchronized outbox evidence. If verification fails, the refusal lists every unmet requirement at once; resolve all of them, then verify again. When `session.resume` reports `context_refresh_required`, call `context.refresh`, then `context.prepare_change` for the same paths; resume alone never reactivates a stale session, and the task baseline does not change. `context.refresh` sends in full only the revisions that changed since the session pinned them and lists the rest in `unchangedResources`; reread one of those with `memory.read_revisions` only when its text is no longer in view. Do not state that the task is complete while verification is failing, and do not carry on writing code with the failure unaddressed — a task that never verifies never closes, and everything that depends on closing, including the commit gate, silently never happens.
|
|
282
286
|
|
|
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
|
+
|
|
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
|
+
|
|
283
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.
|
|
284
292
|
|
|
285
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.
|
|
@@ -288,7 +296,9 @@ Call `task.close` only after verification and only when the current diff still m
|
|
|
288
296
|
|
|
289
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.
|
|
290
298
|
|
|
291
|
-
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`.
|
|
292
302
|
|
|
293
303
|
## Code source and memory applicability
|
|
294
304
|
|