vibe-coding-master 0.6.22 → 0.7.0
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/README.md +37 -3
- package/dist/backend/adapters/filesystem.js +8 -0
- package/dist/backend/api/harness-routes.js +54 -0
- package/dist/backend/api/runtime-state-routes.js +6 -2
- package/dist/backend/api/task-routes.js +2 -32
- package/dist/backend/cli/install-vcm-harness.js +1 -1
- package/dist/backend/gateway/gateway-service.js +4 -37
- package/dist/backend/server.js +50 -15
- package/dist/backend/services/app-settings-service.js +1 -0
- package/dist/backend/services/auto-memory-service.js +760 -0
- package/dist/backend/services/claude-hook-service.js +108 -2
- package/dist/backend/services/claude-transcript-reply.js +81 -1
- package/dist/backend/services/harness-service.js +1 -1
- package/dist/backend/services/runtime-coordinator-service.js +69 -1
- package/dist/backend/services/runtime-recovery-service.js +6 -0
- package/dist/backend/services/session-service.js +3 -0
- package/dist/backend/services/task-close-service.js +88 -0
- package/dist/backend/services/task-service.js +152 -35
- package/dist/backend/services/turn-reconciler-service.js +122 -0
- package/dist/backend/templates/harness/architect-agent.js +3 -0
- package/dist/backend/templates/harness/claude-root.js +3 -1
- package/dist/backend/templates/harness/coder-agent.js +3 -0
- package/dist/backend/templates/harness/gate-review.js +3 -0
- package/dist/backend/templates/harness/harness-engineer-agent.js +32 -8
- package/dist/backend/templates/harness/project-manager-agent.js +3 -0
- package/dist/backend/templates/harness/role-memory.js +9 -0
- package/dist/backend/templates/harness/tester-agent.js +3 -0
- package/dist/shared/types/memory.js +8 -0
- package/dist-frontend/assets/{index-DmSHDyiQ.css → index-C2QzumXk.css} +1 -1
- package/dist-frontend/assets/index-e8Tqa8Qh.js +97 -0
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/dist-frontend/assets/index-DYBg_qYS.js +0 -96
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { checkMarkdownArtifact, readArtifactSectionValue } from "../../shared/validation/artifact-check.js";
|
|
4
|
+
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
5
|
+
import { VcmError } from "../errors.js";
|
|
6
|
+
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
7
|
+
const MEMORY_ROOT = ".ai/vcm/memory";
|
|
8
|
+
const MEMORY_REVIEW_ROOT = ".ai/vcm/memory-review";
|
|
9
|
+
const MEMORY_REVIEW_RUNS_ROOT = `${MEMORY_REVIEW_ROOT}/runs`;
|
|
10
|
+
const MEMORY_REVIEW_STATE_PATH = `${MEMORY_REVIEW_ROOT}/state.json`;
|
|
11
|
+
const MEMORY_FILE_DEFINITIONS = [
|
|
12
|
+
{ path: `${MEMORY_ROOT}/shared.md`, title: "Shared Memory" },
|
|
13
|
+
{ path: `${MEMORY_ROOT}/roles/project-manager.md`, title: "Project Manager Memory", role: "project-manager" },
|
|
14
|
+
{ path: `${MEMORY_ROOT}/roles/architect.md`, title: "Architect Memory", role: "architect" },
|
|
15
|
+
{ path: `${MEMORY_ROOT}/roles/coder.md`, title: "Coder Memory", role: "coder" },
|
|
16
|
+
{ path: `${MEMORY_ROOT}/roles/tester.md`, title: "Tester Memory", role: "tester" },
|
|
17
|
+
{ path: `${MEMORY_ROOT}/roles/gate-reviewer.md`, title: "Gate Reviewer Memory", role: "gate-reviewer" },
|
|
18
|
+
{ path: `${MEMORY_ROOT}/roles/harness-engineer.md`, title: "Harness Engineer Memory", role: "harness-engineer" }
|
|
19
|
+
];
|
|
20
|
+
export function createAutoMemoryService(deps) {
|
|
21
|
+
const now = deps.now ?? (() => new Date().toISOString());
|
|
22
|
+
async function readMemorySet(repoRoot) {
|
|
23
|
+
const memory = {};
|
|
24
|
+
for (const definition of MEMORY_FILE_DEFINITIONS) {
|
|
25
|
+
memory[definition.path] = await deps.fs.readText(resolveRepoPath(repoRoot, definition.path));
|
|
26
|
+
}
|
|
27
|
+
return memory;
|
|
28
|
+
}
|
|
29
|
+
async function writeMemorySet(repoRoot, memory) {
|
|
30
|
+
assertCompleteMemorySet(memory);
|
|
31
|
+
for (const definition of MEMORY_FILE_DEFINITIONS) {
|
|
32
|
+
const targetPath = resolveRepoPath(repoRoot, definition.path);
|
|
33
|
+
const content = ensureTrailingNewline(memory[definition.path]);
|
|
34
|
+
if (deps.fs.writeTextAtomic) {
|
|
35
|
+
await deps.fs.writeTextAtomic(targetPath, content);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
await deps.fs.writeText(targetPath, content);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function applyMemorySet(baseRepoRoot, taskRepoRoot, memory) {
|
|
43
|
+
await writeMemorySet(baseRepoRoot, memory);
|
|
44
|
+
await writeMemorySet(taskRepoRoot, memory);
|
|
45
|
+
}
|
|
46
|
+
async function writeRunMemorySet(taskRepoRoot, runId, snapshot, memory) {
|
|
47
|
+
assertCompleteMemorySet(memory);
|
|
48
|
+
for (const definition of MEMORY_FILE_DEFINITIONS) {
|
|
49
|
+
await deps.fs.writeText(resolveRepoPath(taskRepoRoot, memoryRunFilePath(runId, snapshot, definition.path)), ensureTrailingNewline(memory[definition.path]));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function readRunMemorySet(taskRepoRoot, runId, snapshot) {
|
|
53
|
+
const memory = {};
|
|
54
|
+
for (const definition of MEMORY_FILE_DEFINITIONS) {
|
|
55
|
+
const memoryPath = resolveRepoPath(taskRepoRoot, memoryRunFilePath(runId, snapshot, definition.path));
|
|
56
|
+
if (!(await deps.fs.pathExists(memoryPath))) {
|
|
57
|
+
throw new Error(`Missing memory review output: ${definition.path}`);
|
|
58
|
+
}
|
|
59
|
+
memory[definition.path] = await deps.fs.readText(memoryPath);
|
|
60
|
+
}
|
|
61
|
+
return memory;
|
|
62
|
+
}
|
|
63
|
+
async function getState(baseRepoRoot, taskRepoRoot) {
|
|
64
|
+
await ensureTaskMemorySnapshot(deps.fs, baseRepoRoot, taskRepoRoot);
|
|
65
|
+
const [active, runs, files] = await Promise.all([
|
|
66
|
+
loadActiveState(taskRepoRoot),
|
|
67
|
+
listRuns(taskRepoRoot),
|
|
68
|
+
listMemoryFiles(taskRepoRoot)
|
|
69
|
+
]);
|
|
70
|
+
return {
|
|
71
|
+
version: 1,
|
|
72
|
+
status: active?.status ?? "idle",
|
|
73
|
+
files,
|
|
74
|
+
runs,
|
|
75
|
+
...(active ? { active: toActiveReview(active) } : {}),
|
|
76
|
+
warnings: []
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
async function reconcileTask(input) {
|
|
80
|
+
await ensureTaskMemorySnapshot(deps.fs, input.baseRepoRoot, input.taskRepoRoot);
|
|
81
|
+
const active = await loadActiveState(input.taskRepoRoot);
|
|
82
|
+
if (active?.status === "collecting") {
|
|
83
|
+
await dispatchCurrentDraft(input.baseRepoRoot, input.taskRepoRoot, active);
|
|
84
|
+
return getState(input.baseRepoRoot, input.taskRepoRoot);
|
|
85
|
+
}
|
|
86
|
+
if (active?.status === "reviewing") {
|
|
87
|
+
await dispatchHarnessReview(input.baseRepoRoot, input.taskRepoRoot, active);
|
|
88
|
+
return getState(input.baseRepoRoot, input.taskRepoRoot);
|
|
89
|
+
}
|
|
90
|
+
if (active || !input.roundReady) {
|
|
91
|
+
return getState(input.baseRepoRoot, input.taskRepoRoot);
|
|
92
|
+
}
|
|
93
|
+
const preferences = await deps.appSettings.getPreferences();
|
|
94
|
+
if (!preferences.autoMemoryEnabled) {
|
|
95
|
+
return getState(input.baseRepoRoot, input.taskRepoRoot);
|
|
96
|
+
}
|
|
97
|
+
if (deps.isHarnessEngineerAvailable && !(await deps.isHarnessEngineerAvailable(input.baseRepoRoot))) {
|
|
98
|
+
return getState(input.baseRepoRoot, input.taskRepoRoot);
|
|
99
|
+
}
|
|
100
|
+
const finalAcceptanceHash = await readAcceptedFinalAcceptanceHash(input.taskRepoRoot, input.handoffDir);
|
|
101
|
+
if (!finalAcceptanceHash) {
|
|
102
|
+
return getState(input.baseRepoRoot, input.taskRepoRoot);
|
|
103
|
+
}
|
|
104
|
+
if (await hasCompletedRunForFinalAcceptance(input.taskRepoRoot, finalAcceptanceHash)) {
|
|
105
|
+
return getState(input.baseRepoRoot, input.taskRepoRoot);
|
|
106
|
+
}
|
|
107
|
+
const gateSettings = await deps.appSettings.getGateReviewSettings(input.baseRepoRoot, input.taskSlug);
|
|
108
|
+
const roles = ["project-manager", "architect", "coder", "tester"];
|
|
109
|
+
if (gateSettings.enabled) {
|
|
110
|
+
roles.push("gate-reviewer");
|
|
111
|
+
}
|
|
112
|
+
const timestamp = now();
|
|
113
|
+
const runId = createRunId(timestamp, "auto");
|
|
114
|
+
const drafts = roles.map((role) => ({
|
|
115
|
+
role,
|
|
116
|
+
path: `${MEMORY_REVIEW_RUNS_ROOT}/${runId}/drafts/${role}.md`,
|
|
117
|
+
status: "pending"
|
|
118
|
+
}));
|
|
119
|
+
const state = {
|
|
120
|
+
version: 1,
|
|
121
|
+
runId,
|
|
122
|
+
taskSlug: input.taskSlug,
|
|
123
|
+
status: "collecting",
|
|
124
|
+
finalAcceptanceHash,
|
|
125
|
+
createdAt: timestamp,
|
|
126
|
+
updatedAt: timestamp,
|
|
127
|
+
drafts
|
|
128
|
+
};
|
|
129
|
+
const before = await readMemorySet(input.baseRepoRoot);
|
|
130
|
+
await writeRunMemorySet(input.taskRepoRoot, runId, "before", before);
|
|
131
|
+
await writeRunMemorySet(input.taskRepoRoot, runId, "after", before);
|
|
132
|
+
await persistRun(input.taskRepoRoot, {
|
|
133
|
+
version: 1,
|
|
134
|
+
runId,
|
|
135
|
+
taskSlug: input.taskSlug,
|
|
136
|
+
source: "auto",
|
|
137
|
+
status: "collecting",
|
|
138
|
+
createdAt: timestamp,
|
|
139
|
+
updatedAt: timestamp,
|
|
140
|
+
finalAcceptanceHash,
|
|
141
|
+
beforeHashes: hashMemorySet(before)
|
|
142
|
+
});
|
|
143
|
+
await persistActiveState(input.taskRepoRoot, state);
|
|
144
|
+
await dispatchCurrentDraft(input.baseRepoRoot, input.taskRepoRoot, state);
|
|
145
|
+
return getState(input.baseRepoRoot, input.taskRepoRoot);
|
|
146
|
+
}
|
|
147
|
+
async function getTaskRetrospectiveReadiness(input) {
|
|
148
|
+
const preferences = await deps.appSettings.getPreferences();
|
|
149
|
+
if (!preferences.autoMemoryEnabled) {
|
|
150
|
+
return { ready: true, disposition: "disabled" };
|
|
151
|
+
}
|
|
152
|
+
const finalAcceptanceHash = await readAcceptedFinalAcceptanceHash(input.taskRepoRoot, input.handoffDir);
|
|
153
|
+
if (!finalAcceptanceHash) {
|
|
154
|
+
return { ready: true, disposition: "not-applicable" };
|
|
155
|
+
}
|
|
156
|
+
const active = await loadActiveState(input.taskRepoRoot);
|
|
157
|
+
if (active) {
|
|
158
|
+
const disposition = active.status;
|
|
159
|
+
return {
|
|
160
|
+
ready: false,
|
|
161
|
+
disposition,
|
|
162
|
+
reason: disposition === "failed"
|
|
163
|
+
? "Auto Memory failed for this task. Retry it before Task Harness Retrospective."
|
|
164
|
+
: `Auto Memory is ${disposition} for this task.`
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (await hasCompletedRunForFinalAcceptance(input.taskRepoRoot, finalAcceptanceHash)) {
|
|
168
|
+
return { ready: true, disposition: "completed" };
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
ready: false,
|
|
172
|
+
disposition: "pending",
|
|
173
|
+
reason: "Auto Memory must complete for this Final Acceptance before Task Harness Retrospective."
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
async function assertTaskRetrospectiveReady(input) {
|
|
177
|
+
const readiness = await getTaskRetrospectiveReadiness(input);
|
|
178
|
+
if (readiness.ready) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
throw new VcmError({
|
|
182
|
+
code: "TASK_MEMORY_REVIEW_NOT_READY",
|
|
183
|
+
message: "Task Harness Retrospective must run after Auto Memory.",
|
|
184
|
+
statusCode: 409,
|
|
185
|
+
hint: readiness.reason
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
async function isRoleMemoryTurn(taskRepoRoot, role) {
|
|
189
|
+
const state = await loadActiveState(taskRepoRoot);
|
|
190
|
+
const draft = state?.status === "collecting" ? currentDraft(state) : undefined;
|
|
191
|
+
return draft?.role === role && draft.status === "running";
|
|
192
|
+
}
|
|
193
|
+
async function handleRoleHook(input) {
|
|
194
|
+
const state = await loadActiveState(input.taskRepoRoot);
|
|
195
|
+
const draft = state?.status === "collecting" ? currentDraft(state) : undefined;
|
|
196
|
+
if (!state || !draft || draft.role !== input.role) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
if (input.eventName === "UserPromptSubmit" || input.eventName === "PostCompact") {
|
|
200
|
+
if (draft.status !== "running") {
|
|
201
|
+
draft.status = "running";
|
|
202
|
+
state.updatedAt = now();
|
|
203
|
+
await persistActiveState(input.taskRepoRoot, state);
|
|
204
|
+
}
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
if (input.eventName === "StopFailure") {
|
|
208
|
+
await failReview(input.taskRepoRoot, state, `${input.role} memory draft turn failed.`);
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
if (input.eventName !== "Stop") {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
const draftAbsolutePath = resolveRepoPath(input.taskRepoRoot, draft.path);
|
|
215
|
+
if (!(await deps.fs.pathExists(draftAbsolutePath))) {
|
|
216
|
+
await failReview(input.taskRepoRoot, state, `${input.role} did not write the required memory draft: ${draft.path}`);
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
const content = (await deps.fs.readText(draftAbsolutePath)).trim();
|
|
220
|
+
if (!content || !/^Decision:\s*(update|no-change)\s*$/im.test(content)) {
|
|
221
|
+
await failReview(input.taskRepoRoot, state, `${input.role} memory draft is missing a valid Decision: update or Decision: no-change field.`);
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
draft.status = "completed";
|
|
225
|
+
state.updatedAt = now();
|
|
226
|
+
const next = currentDraft(state);
|
|
227
|
+
if (next) {
|
|
228
|
+
await persistActiveState(input.taskRepoRoot, state);
|
|
229
|
+
await dispatchCurrentDraft(input.baseRepoRoot, input.taskRepoRoot, state);
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
state.status = "reviewing";
|
|
233
|
+
await persistActiveState(input.taskRepoRoot, state);
|
|
234
|
+
await updateRunStatus(input.taskRepoRoot, state.runId, "reviewing", state.updatedAt);
|
|
235
|
+
await dispatchHarnessReview(input.baseRepoRoot, input.taskRepoRoot, state);
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
async function handleHarnessEngineerHook(input) {
|
|
239
|
+
const state = await loadActiveState(input.taskRepoRoot);
|
|
240
|
+
if (state?.status === "reviewing" && !state.reviewPromptDispatchedAt) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
if (!state || state.status !== "reviewing") {
|
|
244
|
+
if (input.eventName === "Stop") {
|
|
245
|
+
await captureHarnessEngineerChanges(input.baseRepoRoot, input.taskRepoRoot, input.taskSlug);
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
if (input.eventName === "UserPromptSubmit" || input.eventName === "PostCompact") {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
if (input.eventName === "StopFailure") {
|
|
253
|
+
await failReview(input.taskRepoRoot, state, "Harness Engineer memory review turn failed.");
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
if (input.eventName === "Stop") {
|
|
257
|
+
await applyReviewedMemory(input.baseRepoRoot, input.taskRepoRoot, state);
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
async function getFile(baseRepoRoot, taskRepoRoot, filePath) {
|
|
263
|
+
await ensureTaskMemorySnapshot(deps.fs, baseRepoRoot, taskRepoRoot);
|
|
264
|
+
const definition = requireMemoryFileDefinition(filePath);
|
|
265
|
+
const content = await deps.fs.readText(resolveRepoPath(taskRepoRoot, definition.path));
|
|
266
|
+
return {
|
|
267
|
+
path: definition.path,
|
|
268
|
+
title: definition.title,
|
|
269
|
+
...("role" in definition ? { role: definition.role } : {}),
|
|
270
|
+
sizeBytes: Buffer.byteLength(content, "utf8"),
|
|
271
|
+
content,
|
|
272
|
+
editable: true
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
async function updateFile(baseRepoRoot, taskRepoRoot, taskSlug, filePath, content) {
|
|
276
|
+
await assertNoActiveReview(taskRepoRoot);
|
|
277
|
+
const definition = requireMemoryFileDefinition(filePath);
|
|
278
|
+
const before = await readMemorySet(baseRepoRoot);
|
|
279
|
+
const normalizedContent = ensureTrailingNewline(content);
|
|
280
|
+
if (before[definition.path] === normalizedContent) {
|
|
281
|
+
return getState(baseRepoRoot, taskRepoRoot);
|
|
282
|
+
}
|
|
283
|
+
const after = { ...before, [definition.path]: normalizedContent };
|
|
284
|
+
await createAppliedRun(baseRepoRoot, taskRepoRoot, taskSlug, "user", before, after);
|
|
285
|
+
return getState(baseRepoRoot, taskRepoRoot);
|
|
286
|
+
}
|
|
287
|
+
async function revertRun(baseRepoRoot, taskRepoRoot, runId) {
|
|
288
|
+
await assertNoActiveReview(taskRepoRoot);
|
|
289
|
+
const run = await readRun(taskRepoRoot, requireSafeRunId(runId));
|
|
290
|
+
if (run.status !== "applied" || run.revertedAt || !run.afterHashes) {
|
|
291
|
+
throw new VcmError({
|
|
292
|
+
code: "MEMORY_RUN_NOT_REVERTIBLE",
|
|
293
|
+
message: `Memory review run cannot be reverted: ${runId}`,
|
|
294
|
+
statusCode: 409
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
const current = await readMemorySet(baseRepoRoot);
|
|
298
|
+
if (!sameHashes(hashMemorySet(current), run.afterHashes)) {
|
|
299
|
+
throw new VcmError({
|
|
300
|
+
code: "MEMORY_RUN_CHANGED",
|
|
301
|
+
message: "Current memory has changed since this review run was applied.",
|
|
302
|
+
statusCode: 409,
|
|
303
|
+
hint: "Review the current memory and edit it manually instead of overwriting newer changes."
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
const before = await readRunMemorySet(taskRepoRoot, run.runId, "before");
|
|
307
|
+
await writeMemorySet(baseRepoRoot, before);
|
|
308
|
+
await writeMemorySet(taskRepoRoot, before);
|
|
309
|
+
const timestamp = now();
|
|
310
|
+
await persistRun(taskRepoRoot, {
|
|
311
|
+
...run,
|
|
312
|
+
status: "reverted",
|
|
313
|
+
revertedAt: timestamp,
|
|
314
|
+
updatedAt: timestamp
|
|
315
|
+
});
|
|
316
|
+
return getState(baseRepoRoot, taskRepoRoot);
|
|
317
|
+
}
|
|
318
|
+
async function retryFailedReview(baseRepoRoot, taskRepoRoot) {
|
|
319
|
+
const state = await loadActiveState(taskRepoRoot);
|
|
320
|
+
if (!state || state.status !== "failed") {
|
|
321
|
+
throw new VcmError({
|
|
322
|
+
code: "MEMORY_REVIEW_NOT_FAILED",
|
|
323
|
+
message: "There is no failed Auto Memory review to retry.",
|
|
324
|
+
statusCode: 409
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
await clearActiveState(taskRepoRoot);
|
|
328
|
+
return getState(baseRepoRoot, taskRepoRoot);
|
|
329
|
+
}
|
|
330
|
+
async function assertHarnessEngineerAvailable(taskRepoRoot) {
|
|
331
|
+
const state = await loadActiveState(taskRepoRoot);
|
|
332
|
+
if (!state || state.status === "failed") {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
throw new VcmError({
|
|
336
|
+
code: "HARNESS_ENGINEER_MEMORY_ACTIVE",
|
|
337
|
+
message: "Harness Engineer is reserved for the active Auto Memory review.",
|
|
338
|
+
statusCode: 409,
|
|
339
|
+
hint: "Wait for the memory review to finish before starting another Harness Engineer workflow."
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
async function assertNoActiveReview(taskRepoRoot) {
|
|
343
|
+
const state = await loadActiveState(taskRepoRoot);
|
|
344
|
+
if (!state) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
throw new VcmError({
|
|
348
|
+
code: "MEMORY_REVIEW_ACTIVE",
|
|
349
|
+
message: "Memory cannot be edited while an Auto Memory review is active.",
|
|
350
|
+
statusCode: 409,
|
|
351
|
+
hint: state.status === "failed"
|
|
352
|
+
? "Retry the failed review before editing memory."
|
|
353
|
+
: "Wait for the active memory review to finish."
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
async function dispatchCurrentDraft(baseRepoRoot, taskRepoRoot, state) {
|
|
357
|
+
const draft = currentDraft(state);
|
|
358
|
+
if (!draft || draft.status !== "pending") {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
const session = await ensureWorkflowRoleSession(baseRepoRoot, state.taskSlug, draft.role);
|
|
363
|
+
if (session.activityStatus === "running") {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
draft.status = "running";
|
|
367
|
+
state.updatedAt = now();
|
|
368
|
+
await persistActiveState(taskRepoRoot, state);
|
|
369
|
+
await submitTerminalInput(deps.runtime, session.id, buildRoleDraftPrompt(taskRepoRoot, state, draft));
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
await failReview(taskRepoRoot, state, `Unable to start ${draft.role} memory draft: ${errorMessage(error)}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
async function dispatchHarnessReview(baseRepoRoot, taskRepoRoot, state) {
|
|
376
|
+
if (state.status !== "reviewing" || state.reviewPromptDispatchedAt) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (deps.isHarnessEngineerAvailable && !(await deps.isHarnessEngineerAvailable(baseRepoRoot))) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
try {
|
|
383
|
+
const existing = await deps.sessionService.getProjectHarnessEngineerSession(baseRepoRoot);
|
|
384
|
+
if (existing?.activityStatus === "running") {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const session = await deps.sessionService.ensureProjectHarnessEngineerSession(baseRepoRoot, {
|
|
388
|
+
taskSlug: state.taskSlug,
|
|
389
|
+
cols: 120,
|
|
390
|
+
rows: 32
|
|
391
|
+
});
|
|
392
|
+
if (session.status !== "running" || session.activityStatus === "running" || !deps.runtime.getSession(session.id)) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
state.reviewPromptDispatchedAt = now();
|
|
396
|
+
state.updatedAt = state.reviewPromptDispatchedAt;
|
|
397
|
+
await persistActiveState(taskRepoRoot, state);
|
|
398
|
+
await submitTerminalInput(deps.runtime, session.id, buildHarnessReviewPrompt(taskRepoRoot, state));
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
await failReview(taskRepoRoot, state, `Unable to start Harness Engineer memory review: ${errorMessage(error)}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
async function ensureWorkflowRoleSession(baseRepoRoot, taskSlug, role) {
|
|
405
|
+
const existing = await deps.sessionService.getRoleSession(baseRepoRoot, taskSlug, role);
|
|
406
|
+
if (existing?.status === "running" && deps.runtime.getSession(existing.id)) {
|
|
407
|
+
return existing;
|
|
408
|
+
}
|
|
409
|
+
const preferences = await deps.appSettings.getPreferences();
|
|
410
|
+
const options = preferences.launchTemplate.roles[role];
|
|
411
|
+
if (existing?.claudeSessionId) {
|
|
412
|
+
return deps.sessionService.resumeRoleSession(baseRepoRoot, taskSlug, role, options);
|
|
413
|
+
}
|
|
414
|
+
return deps.sessionService.startRoleSession(baseRepoRoot, taskSlug, role, options);
|
|
415
|
+
}
|
|
416
|
+
async function applyReviewedMemory(baseRepoRoot, taskRepoRoot, state) {
|
|
417
|
+
try {
|
|
418
|
+
const before = await readRunMemorySet(taskRepoRoot, state.runId, "before");
|
|
419
|
+
const after = await readRunMemorySet(taskRepoRoot, state.runId, "after");
|
|
420
|
+
assertCompleteMemorySet(after);
|
|
421
|
+
await applyMemorySet(baseRepoRoot, taskRepoRoot, after);
|
|
422
|
+
const timestamp = now();
|
|
423
|
+
const diff = renderMemoryDiff(before, after);
|
|
424
|
+
const run = await readRun(taskRepoRoot, state.runId);
|
|
425
|
+
await persistRun(taskRepoRoot, {
|
|
426
|
+
...run,
|
|
427
|
+
status: "applied",
|
|
428
|
+
updatedAt: timestamp,
|
|
429
|
+
appliedAt: timestamp,
|
|
430
|
+
afterHashes: hashMemorySet(after),
|
|
431
|
+
diff
|
|
432
|
+
});
|
|
433
|
+
await deps.fs.writeText(resolveRepoPath(taskRepoRoot, `${MEMORY_REVIEW_RUNS_ROOT}/${state.runId}/applied.patch`), diff);
|
|
434
|
+
await clearActiveState(taskRepoRoot);
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
await failReview(taskRepoRoot, state, `Harness Engineer memory result could not be applied: ${errorMessage(error)}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
async function captureHarnessEngineerChanges(baseRepoRoot, taskRepoRoot, taskSlug) {
|
|
441
|
+
await ensureTaskMemorySnapshot(deps.fs, baseRepoRoot, taskRepoRoot);
|
|
442
|
+
const [before, after] = await Promise.all([
|
|
443
|
+
readMemorySet(baseRepoRoot),
|
|
444
|
+
readMemorySet(taskRepoRoot)
|
|
445
|
+
]);
|
|
446
|
+
if (sameHashes(hashMemorySet(before), hashMemorySet(after))) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
await createAppliedRun(baseRepoRoot, taskRepoRoot, taskSlug, "harness-engineer", before, after);
|
|
450
|
+
}
|
|
451
|
+
async function createAppliedRun(baseRepoRoot, taskRepoRoot, taskSlug, source, before, after) {
|
|
452
|
+
assertCompleteMemorySet(after);
|
|
453
|
+
const timestamp = now();
|
|
454
|
+
const runId = createRunId(timestamp, source);
|
|
455
|
+
await writeRunMemorySet(taskRepoRoot, runId, "before", before);
|
|
456
|
+
await writeRunMemorySet(taskRepoRoot, runId, "after", after);
|
|
457
|
+
await applyMemorySet(baseRepoRoot, taskRepoRoot, after);
|
|
458
|
+
const diff = renderMemoryDiff(before, after);
|
|
459
|
+
await persistRun(taskRepoRoot, {
|
|
460
|
+
version: 1,
|
|
461
|
+
runId,
|
|
462
|
+
taskSlug,
|
|
463
|
+
source,
|
|
464
|
+
status: "applied",
|
|
465
|
+
createdAt: timestamp,
|
|
466
|
+
updatedAt: timestamp,
|
|
467
|
+
appliedAt: timestamp,
|
|
468
|
+
beforeHashes: hashMemorySet(before),
|
|
469
|
+
afterHashes: hashMemorySet(after),
|
|
470
|
+
diff
|
|
471
|
+
});
|
|
472
|
+
await deps.fs.writeText(resolveRepoPath(taskRepoRoot, `${MEMORY_REVIEW_RUNS_ROOT}/${runId}/applied.patch`), diff);
|
|
473
|
+
}
|
|
474
|
+
async function failReview(taskRepoRoot, state, message) {
|
|
475
|
+
const timestamp = now();
|
|
476
|
+
state.status = "failed";
|
|
477
|
+
state.error = message;
|
|
478
|
+
state.updatedAt = timestamp;
|
|
479
|
+
await persistActiveState(taskRepoRoot, state);
|
|
480
|
+
const run = await readRun(taskRepoRoot, state.runId);
|
|
481
|
+
await persistRun(taskRepoRoot, {
|
|
482
|
+
...run,
|
|
483
|
+
status: "failed",
|
|
484
|
+
failedAt: timestamp,
|
|
485
|
+
updatedAt: timestamp,
|
|
486
|
+
error: message,
|
|
487
|
+
diff: run.diff ?? ""
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
async function listRuns(taskRepoRoot) {
|
|
491
|
+
const runsRoot = resolveRepoPath(taskRepoRoot, MEMORY_REVIEW_RUNS_ROOT);
|
|
492
|
+
if (!(await deps.fs.pathExists(runsRoot))) {
|
|
493
|
+
return [];
|
|
494
|
+
}
|
|
495
|
+
const runIds = await deps.fs.readDir(runsRoot);
|
|
496
|
+
const runs = [];
|
|
497
|
+
for (const runId of runIds) {
|
|
498
|
+
const metadataPath = resolveRepoPath(taskRepoRoot, `${MEMORY_REVIEW_RUNS_ROOT}/${runId}/run.json`);
|
|
499
|
+
if (!(await deps.fs.pathExists(metadataPath))) {
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const run = await deps.fs.readJson(metadataPath);
|
|
503
|
+
if (run.status === "collecting" || run.status === "reviewing") {
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
runs.push({
|
|
507
|
+
runId: run.runId,
|
|
508
|
+
taskSlug: run.taskSlug,
|
|
509
|
+
source: run.source,
|
|
510
|
+
status: run.revertedAt ? "reverted" : run.status,
|
|
511
|
+
createdAt: run.createdAt,
|
|
512
|
+
appliedAt: run.appliedAt,
|
|
513
|
+
failedAt: run.failedAt,
|
|
514
|
+
revertedAt: run.revertedAt,
|
|
515
|
+
finalAcceptanceHash: run.finalAcceptanceHash,
|
|
516
|
+
diff: run.diff ?? "",
|
|
517
|
+
canRevert: run.status === "applied" && !run.revertedAt && Boolean(run.afterHashes),
|
|
518
|
+
error: run.error
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
return runs.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
|
|
522
|
+
}
|
|
523
|
+
async function listMemoryFiles(taskRepoRoot) {
|
|
524
|
+
const files = [];
|
|
525
|
+
for (const definition of MEMORY_FILE_DEFINITIONS) {
|
|
526
|
+
const content = await deps.fs.readText(resolveRepoPath(taskRepoRoot, definition.path));
|
|
527
|
+
files.push({
|
|
528
|
+
path: definition.path,
|
|
529
|
+
title: definition.title,
|
|
530
|
+
...("role" in definition ? { role: definition.role } : {}),
|
|
531
|
+
sizeBytes: Buffer.byteLength(content, "utf8")
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
return files;
|
|
535
|
+
}
|
|
536
|
+
async function hasCompletedRunForFinalAcceptance(taskRepoRoot, finalAcceptanceHash) {
|
|
537
|
+
const runs = await listRuns(taskRepoRoot);
|
|
538
|
+
return runs.some((run) => run.finalAcceptanceHash === finalAcceptanceHash && (run.status === "applied" || run.status === "reverted"));
|
|
539
|
+
}
|
|
540
|
+
async function loadActiveState(taskRepoRoot) {
|
|
541
|
+
const statePath = resolveRepoPath(taskRepoRoot, MEMORY_REVIEW_STATE_PATH);
|
|
542
|
+
if (!(await deps.fs.pathExists(statePath))) {
|
|
543
|
+
return undefined;
|
|
544
|
+
}
|
|
545
|
+
const state = await deps.fs.readJson(statePath);
|
|
546
|
+
return state?.version === 1 && state.runId ? state : undefined;
|
|
547
|
+
}
|
|
548
|
+
async function persistActiveState(taskRepoRoot, state) {
|
|
549
|
+
await deps.fs.writeJsonAtomic(resolveRepoPath(taskRepoRoot, MEMORY_REVIEW_STATE_PATH), state);
|
|
550
|
+
}
|
|
551
|
+
async function clearActiveState(taskRepoRoot) {
|
|
552
|
+
await deps.fs.removePath?.(resolveRepoPath(taskRepoRoot, MEMORY_REVIEW_STATE_PATH), { force: true });
|
|
553
|
+
}
|
|
554
|
+
async function readRun(taskRepoRoot, runId) {
|
|
555
|
+
const runPath = resolveRepoPath(taskRepoRoot, `${MEMORY_REVIEW_RUNS_ROOT}/${requireSafeRunId(runId)}/run.json`);
|
|
556
|
+
if (!(await deps.fs.pathExists(runPath))) {
|
|
557
|
+
throw new VcmError({
|
|
558
|
+
code: "MEMORY_RUN_MISSING",
|
|
559
|
+
message: `Memory review run does not exist: ${runId}`,
|
|
560
|
+
statusCode: 404
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
return deps.fs.readJson(runPath);
|
|
564
|
+
}
|
|
565
|
+
async function persistRun(taskRepoRoot, run) {
|
|
566
|
+
await deps.fs.writeJsonAtomic(resolveRepoPath(taskRepoRoot, `${MEMORY_REVIEW_RUNS_ROOT}/${run.runId}/run.json`), run);
|
|
567
|
+
}
|
|
568
|
+
async function updateRunStatus(taskRepoRoot, runId, status, timestamp) {
|
|
569
|
+
const run = await readRun(taskRepoRoot, runId);
|
|
570
|
+
await persistRun(taskRepoRoot, { ...run, status, updatedAt: timestamp });
|
|
571
|
+
}
|
|
572
|
+
async function readAcceptedFinalAcceptanceHash(taskRepoRoot, handoffDir) {
|
|
573
|
+
const finalAcceptancePath = path.posix.join(handoffDir, "final-acceptance.md");
|
|
574
|
+
const absolutePath = resolveRepoPath(taskRepoRoot, finalAcceptancePath);
|
|
575
|
+
if (!(await deps.fs.pathExists(absolutePath))) {
|
|
576
|
+
return undefined;
|
|
577
|
+
}
|
|
578
|
+
const content = await deps.fs.readText(absolutePath);
|
|
579
|
+
const check = checkMarkdownArtifact("final-acceptance", finalAcceptancePath, content);
|
|
580
|
+
const decision = readArtifactSectionValue(content, "Decision")?.toLowerCase();
|
|
581
|
+
if (check.status !== "ok" || (decision !== "accepted" && decision !== "accepted-with-known-risks")) {
|
|
582
|
+
return undefined;
|
|
583
|
+
}
|
|
584
|
+
return `sha256:${sha256(content)}`;
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
ensureTaskSnapshot(baseRepoRoot, taskRepoRoot) {
|
|
588
|
+
return ensureTaskMemorySnapshot(deps.fs, baseRepoRoot, taskRepoRoot);
|
|
589
|
+
},
|
|
590
|
+
reconcileTask,
|
|
591
|
+
getState,
|
|
592
|
+
getTaskRetrospectiveReadiness,
|
|
593
|
+
assertTaskRetrospectiveReady,
|
|
594
|
+
getFile,
|
|
595
|
+
updateFile,
|
|
596
|
+
revertRun,
|
|
597
|
+
retryFailedReview,
|
|
598
|
+
isRoleMemoryTurn,
|
|
599
|
+
handleRoleHook,
|
|
600
|
+
handleHarnessEngineerHook,
|
|
601
|
+
assertHarnessEngineerAvailable
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
export async function ensureTaskMemorySnapshot(fs, baseRepoRoot, taskRepoRoot) {
|
|
605
|
+
for (const definition of MEMORY_FILE_DEFINITIONS) {
|
|
606
|
+
const canonicalPath = resolveRepoPath(baseRepoRoot, definition.path);
|
|
607
|
+
if (!(await fs.pathExists(canonicalPath))) {
|
|
608
|
+
await fs.writeText(canonicalPath, renderDefaultMemoryFile(definition.title));
|
|
609
|
+
}
|
|
610
|
+
const snapshotPath = resolveRepoPath(taskRepoRoot, definition.path);
|
|
611
|
+
if (!(await fs.pathExists(snapshotPath))) {
|
|
612
|
+
await fs.writeText(snapshotPath, await fs.readText(canonicalPath));
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function renderDefaultMemoryFile(title) {
|
|
617
|
+
return `# ${title}\n\nNo accumulated project memory yet.\n`;
|
|
618
|
+
}
|
|
619
|
+
function currentDraft(state) {
|
|
620
|
+
return state.drafts.find((draft) => draft.status !== "completed");
|
|
621
|
+
}
|
|
622
|
+
function toActiveReview(state) {
|
|
623
|
+
return {
|
|
624
|
+
runId: state.runId,
|
|
625
|
+
taskSlug: state.taskSlug,
|
|
626
|
+
status: state.status,
|
|
627
|
+
finalAcceptanceHash: state.finalAcceptanceHash,
|
|
628
|
+
createdAt: state.createdAt,
|
|
629
|
+
updatedAt: state.updatedAt,
|
|
630
|
+
currentRole: state.status === "collecting" ? currentDraft(state)?.role : undefined,
|
|
631
|
+
drafts: state.drafts,
|
|
632
|
+
error: state.error
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
function buildRoleDraftPrompt(taskRepoRoot, state, draft) {
|
|
636
|
+
return [
|
|
637
|
+
"[VCM Auto Memory Draft]",
|
|
638
|
+
"",
|
|
639
|
+
"The task passed Final Acceptance. Propose only durable, reusable memory supported by this task's evidence.",
|
|
640
|
+
`Task worktree: ${taskRepoRoot}`,
|
|
641
|
+
`Current shared memory: ${resolveRepoPath(taskRepoRoot, `${MEMORY_ROOT}/shared.md`)}`,
|
|
642
|
+
`Current role memory: ${resolveRepoPath(taskRepoRoot, `${MEMORY_ROOT}/roles/${draft.role}.md`)}`,
|
|
643
|
+
`Write the draft to: ${resolveRepoPath(taskRepoRoot, draft.path)}`,
|
|
644
|
+
"",
|
|
645
|
+
"Use this structure:",
|
|
646
|
+
"# Memory Draft",
|
|
647
|
+
"Decision: update | no-change",
|
|
648
|
+
"## Add",
|
|
649
|
+
"## Update",
|
|
650
|
+
"## Remove",
|
|
651
|
+
"## Evidence",
|
|
652
|
+
"",
|
|
653
|
+
"Do not edit memory files, route messages, or handoff artifacts. Do not record task narrative, temporary state, unverified conclusions, or rules that belong in the harness.",
|
|
654
|
+
"End the turn after writing the draft."
|
|
655
|
+
].join("\n");
|
|
656
|
+
}
|
|
657
|
+
function buildHarnessReviewPrompt(taskRepoRoot, state) {
|
|
658
|
+
const runRoot = resolveRepoPath(taskRepoRoot, `${MEMORY_REVIEW_RUNS_ROOT}/${state.runId}`);
|
|
659
|
+
return [
|
|
660
|
+
"[VCM Auto Memory Review]",
|
|
661
|
+
"",
|
|
662
|
+
"Review the role drafts and task evidence, then produce the complete next memory set.",
|
|
663
|
+
`Task worktree: ${taskRepoRoot}`,
|
|
664
|
+
`Role drafts: ${path.join(runRoot, "drafts")}`,
|
|
665
|
+
`Current memory snapshot: ${path.join(runRoot, "before")}`,
|
|
666
|
+
`Write the complete reviewed memory set to: ${path.join(runRoot, "after")}`,
|
|
667
|
+
"",
|
|
668
|
+
"Keep only verified, durable, reusable project knowledge. Merge duplicates, remove stale entries, and keep role-specific knowledge in the matching role file.",
|
|
669
|
+
"Memory is context, not authority. Keep harness rules and task-specific narrative out of memory.",
|
|
670
|
+
"Do not edit product code, harness files, the canonical base-repository memory, or review metadata.",
|
|
671
|
+
"All existing files already exist in the after directory. Edit those files in place and end the turn when review is complete."
|
|
672
|
+
].join("\n");
|
|
673
|
+
}
|
|
674
|
+
function requireMemoryFileDefinition(filePath) {
|
|
675
|
+
const normalized = filePath.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
676
|
+
const definition = MEMORY_FILE_DEFINITIONS.find((candidate) => candidate.path === normalized);
|
|
677
|
+
if (!definition) {
|
|
678
|
+
throw new VcmError({
|
|
679
|
+
code: "MEMORY_FILE_INVALID",
|
|
680
|
+
message: `Memory file is not managed by VCM: ${filePath}`,
|
|
681
|
+
statusCode: 400
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
return definition;
|
|
685
|
+
}
|
|
686
|
+
function requireSafeRunId(runId) {
|
|
687
|
+
const normalized = runId.trim();
|
|
688
|
+
if (!normalized || !/^[A-Za-z0-9._-]+$/.test(normalized)) {
|
|
689
|
+
throw new VcmError({
|
|
690
|
+
code: "MEMORY_RUN_INVALID",
|
|
691
|
+
message: "Memory review run id is invalid.",
|
|
692
|
+
statusCode: 400
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
return normalized;
|
|
696
|
+
}
|
|
697
|
+
function memoryRunFilePath(runId, snapshot, memoryPath) {
|
|
698
|
+
return `${MEMORY_REVIEW_RUNS_ROOT}/${runId}/${snapshot}/${memoryPath.slice(`${MEMORY_ROOT}/`.length)}`;
|
|
699
|
+
}
|
|
700
|
+
function assertCompleteMemorySet(memory) {
|
|
701
|
+
for (const definition of MEMORY_FILE_DEFINITIONS) {
|
|
702
|
+
if (typeof memory[definition.path] !== "string") {
|
|
703
|
+
throw new Error(`Missing reviewed memory file: ${definition.path}`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
function hashMemorySet(memory) {
|
|
708
|
+
return Object.fromEntries(MEMORY_FILE_DEFINITIONS.map((definition) => [
|
|
709
|
+
definition.path,
|
|
710
|
+
sha256(memory[definition.path] ?? "")
|
|
711
|
+
]));
|
|
712
|
+
}
|
|
713
|
+
function sameHashes(left, right) {
|
|
714
|
+
return MEMORY_FILE_DEFINITIONS.every((definition) => left[definition.path] === right[definition.path]);
|
|
715
|
+
}
|
|
716
|
+
function renderMemoryDiff(before, after) {
|
|
717
|
+
const sections = [];
|
|
718
|
+
for (const definition of MEMORY_FILE_DEFINITIONS) {
|
|
719
|
+
const oldContent = before[definition.path] ?? "";
|
|
720
|
+
const newContent = after[definition.path] ?? "";
|
|
721
|
+
if (oldContent === newContent) {
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
const oldLines = oldContent.replace(/\n$/, "").split("\n");
|
|
725
|
+
const newLines = newContent.replace(/\n$/, "").split("\n");
|
|
726
|
+
let prefix = 0;
|
|
727
|
+
while (prefix < oldLines.length && prefix < newLines.length && oldLines[prefix] === newLines[prefix]) {
|
|
728
|
+
prefix += 1;
|
|
729
|
+
}
|
|
730
|
+
let suffix = 0;
|
|
731
|
+
while (suffix < oldLines.length - prefix
|
|
732
|
+
&& suffix < newLines.length - prefix
|
|
733
|
+
&& oldLines[oldLines.length - 1 - suffix] === newLines[newLines.length - 1 - suffix]) {
|
|
734
|
+
suffix += 1;
|
|
735
|
+
}
|
|
736
|
+
const removed = oldLines.slice(prefix, oldLines.length - suffix).map((line) => `-${line}`);
|
|
737
|
+
const added = newLines.slice(prefix, newLines.length - suffix).map((line) => `+${line}`);
|
|
738
|
+
sections.push([
|
|
739
|
+
`--- ${definition.path}`,
|
|
740
|
+
`+++ ${definition.path}`,
|
|
741
|
+
`@@ line ${prefix + 1} @@`,
|
|
742
|
+
...removed,
|
|
743
|
+
...added
|
|
744
|
+
].join("\n"));
|
|
745
|
+
}
|
|
746
|
+
return sections.length > 0 ? `${sections.join("\n\n")}\n` : "No memory changes.\n";
|
|
747
|
+
}
|
|
748
|
+
function createRunId(timestamp, source) {
|
|
749
|
+
const suffix = sha256(`${timestamp}:${source}:${Math.random()}`).slice(0, 8);
|
|
750
|
+
return `${timestamp.replace(/[^0-9]/g, "").slice(0, 17)}-${source}-${suffix}`;
|
|
751
|
+
}
|
|
752
|
+
function ensureTrailingNewline(content) {
|
|
753
|
+
return content.endsWith("\n") ? content : `${content}\n`;
|
|
754
|
+
}
|
|
755
|
+
function sha256(content) {
|
|
756
|
+
return createHash("sha256").update(content).digest("hex");
|
|
757
|
+
}
|
|
758
|
+
function errorMessage(error) {
|
|
759
|
+
return error instanceof Error ? error.message : String(error);
|
|
760
|
+
}
|