vibe-coding-master 0.7.4 → 0.7.6
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 +19 -16
- package/dist/backend/adapters/git-adapter.js +15 -0
- package/dist/backend/api/artifact-routes.js +3 -0
- package/dist/backend/api/harness-routes.js +50 -27
- package/dist/backend/api/runtime-state-routes.js +7 -3
- package/dist/backend/api/task-routes.js +36 -4
- package/dist/backend/api/translation-routes.js +11 -2
- package/dist/backend/api/translation-worker-routes.js +37 -9
- package/dist/backend/cli/install-vcm-harness.js +40 -2
- package/dist/backend/gateway/gateway-service.js +34 -17
- package/dist/backend/server.js +12 -3
- package/dist/backend/services/artifact-service.js +5 -1
- package/dist/backend/services/auto-memory-service.js +156 -81
- package/dist/backend/services/claude-hook-service.js +50 -35
- package/dist/backend/services/command-dispatcher.js +1 -1
- package/dist/backend/services/gate-review-service.js +335 -31
- package/dist/backend/services/harness-feedback-service.js +19 -8
- package/dist/backend/services/harness-service.js +112 -34
- package/dist/backend/services/message-service.js +39 -2
- package/dist/backend/services/round-service.js +10 -121
- package/dist/backend/services/runtime-coordinator-service.js +18 -10
- package/dist/backend/services/runtime-recovery-service.js +1 -2
- package/dist/backend/services/session-service.js +36 -98
- package/dist/backend/services/status-service.js +1 -0
- package/dist/backend/services/task-close-service.js +12 -27
- package/dist/backend/services/task-workflow-service.js +228 -0
- package/dist/backend/services/translation-worker-service.js +14 -7
- package/dist/backend/templates/handoff.js +128 -1
- package/dist/backend/templates/harness/architect-agent.js +85 -22
- package/dist/backend/templates/harness/claude-root.js +25 -29
- package/dist/backend/templates/harness/coder-agent.js +5 -7
- package/dist/backend/templates/harness/coder-worker-agent.js +3 -3
- package/dist/backend/templates/harness/gate-review.js +292 -65
- package/dist/backend/templates/harness/harness-engineer-agent.js +8 -8
- package/dist/backend/templates/harness/memory-block.js +69 -0
- package/dist/backend/templates/harness/project-known-issues.js +1 -0
- package/dist/backend/templates/harness/project-manager-agent.js +217 -75
- package/dist/backend/templates/harness/role-memory.js +9 -12
- package/dist/backend/templates/harness/tester-agent.js +8 -4
- package/dist/backend/templates/harness/vcm-architecture-interview-skill.js +82 -0
- package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +4 -3
- package/dist/backend/templates/harness/vcm-harness-bootstrap-skill.js +14 -3
- package/dist/backend/templates/harness/vcm-propose-memory-skill.js +2 -2
- package/dist/backend/templates/harness/vcm-route-message-skill.js +5 -0
- package/dist/backend/templates/harness/vcm-task-state-skill.js +110 -0
- package/dist/backend/templates/message-envelope.js +1 -1
- package/dist/shared/constants.js +0 -10
- package/dist/shared/types/workflow.js +1 -0
- package/dist/shared/validation/artifact-check.js +41 -1
- package/dist-frontend/assets/index-BO2AuF-q.js +97 -0
- package/dist-frontend/assets/index-C2etsYlK.css +32 -0
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/scripts/harness-tools/check-durable-docs +298 -0
- package/scripts/verify-package.mjs +1 -0
- package/dist-frontend/assets/index-DCb-S6Ls.css +0 -32
- package/dist-frontend/assets/index-NTlycxx9.js +0 -97
|
@@ -4,25 +4,30 @@ import { checkMarkdownArtifact, readArtifactSectionValue } from "../../shared/va
|
|
|
4
4
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
5
5
|
import { VcmError } from "../errors.js";
|
|
6
6
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
7
|
-
|
|
7
|
+
import { readVcmMemoryBlock, replaceVcmMemoryBlock } from "../templates/harness/memory-block.js";
|
|
8
8
|
const MEMORY_REVIEW_ROOT = ".ai/vcm/memory-review";
|
|
9
9
|
const MEMORY_REVIEW_RUNS_ROOT = `${MEMORY_REVIEW_ROOT}/runs`;
|
|
10
10
|
const MEMORY_REVIEW_STATE_PATH = `${MEMORY_REVIEW_ROOT}/state.json`;
|
|
11
11
|
const MEMORY_FILE_DEFINITIONS = [
|
|
12
|
-
{ path:
|
|
13
|
-
{ path:
|
|
14
|
-
{ path:
|
|
15
|
-
{ path:
|
|
16
|
-
{ path:
|
|
17
|
-
{ path:
|
|
18
|
-
{ path:
|
|
12
|
+
{ path: "CLAUDE.md", title: "Shared Memory" },
|
|
13
|
+
{ path: ".claude/agents/project-manager.md", title: "Project Manager Memory", role: "project-manager" },
|
|
14
|
+
{ path: ".claude/agents/architect.md", title: "Architect Memory", role: "architect" },
|
|
15
|
+
{ path: ".claude/agents/coder.md", title: "Coder Memory", role: "coder" },
|
|
16
|
+
{ path: ".claude/agents/tester.md", title: "Tester Memory", role: "tester" },
|
|
17
|
+
{ path: ".claude/agents/gate-reviewer.md", title: "Gate Reviewer Memory", role: "gate-reviewer" },
|
|
18
|
+
{ path: ".claude/agents/harness-engineer.md", title: "Harness Engineer Memory", role: "harness-engineer" }
|
|
19
19
|
];
|
|
20
20
|
export function createAutoMemoryService(deps) {
|
|
21
21
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
22
22
|
async function readMemorySet(repoRoot) {
|
|
23
23
|
const memory = {};
|
|
24
24
|
for (const definition of MEMORY_FILE_DEFINITIONS) {
|
|
25
|
-
|
|
25
|
+
const hostContent = await deps.fs.readText(resolveRepoPath(repoRoot, definition.path));
|
|
26
|
+
const blockContent = readVcmMemoryBlock(hostContent);
|
|
27
|
+
if (blockContent === undefined) {
|
|
28
|
+
throw missingMemoryBlockError(definition.path);
|
|
29
|
+
}
|
|
30
|
+
memory[definition.path] = blockContent;
|
|
26
31
|
}
|
|
27
32
|
return memory;
|
|
28
33
|
}
|
|
@@ -30,7 +35,8 @@ export function createAutoMemoryService(deps) {
|
|
|
30
35
|
assertCompleteMemorySet(memory);
|
|
31
36
|
for (const definition of MEMORY_FILE_DEFINITIONS) {
|
|
32
37
|
const targetPath = resolveRepoPath(repoRoot, definition.path);
|
|
33
|
-
const
|
|
38
|
+
const hostContent = await deps.fs.readText(targetPath);
|
|
39
|
+
const content = replaceVcmMemoryBlock(hostContent, memory[definition.path]);
|
|
34
40
|
if (deps.fs.writeTextAtomic) {
|
|
35
41
|
await deps.fs.writeTextAtomic(targetPath, content);
|
|
36
42
|
}
|
|
@@ -39,9 +45,40 @@ export function createAutoMemoryService(deps) {
|
|
|
39
45
|
}
|
|
40
46
|
}
|
|
41
47
|
}
|
|
42
|
-
async function
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
async function applyAndCommitMemorySet(taskRepoRoot, before, after, commitMessage) {
|
|
49
|
+
assertCompleteMemorySet(after);
|
|
50
|
+
const changedPaths = MEMORY_FILE_DEFINITIONS
|
|
51
|
+
.filter((definition) => before[definition.path] !== after[definition.path])
|
|
52
|
+
.map((definition) => definition.path);
|
|
53
|
+
if (changedPaths.length === 0) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const current = await readMemorySet(taskRepoRoot);
|
|
57
|
+
if (!sameHashes(hashMemorySet(current), hashMemorySet(before))) {
|
|
58
|
+
throw new VcmError({
|
|
59
|
+
code: "MEMORY_TARGET_CHANGED",
|
|
60
|
+
message: "VCM memory changed while the review was in progress.",
|
|
61
|
+
statusCode: 409,
|
|
62
|
+
hint: "Review the current memory, then retry the memory review."
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const existingDiff = await deps.git.getDiff(taskRepoRoot, "HEAD", null, changedPaths);
|
|
66
|
+
if (existingDiff.trim()) {
|
|
67
|
+
throw new VcmError({
|
|
68
|
+
code: "MEMORY_HOST_FILE_DIRTY",
|
|
69
|
+
message: "A file containing VCM memory already has uncommitted changes.",
|
|
70
|
+
statusCode: 409,
|
|
71
|
+
hint: "Commit or discard the existing host-file changes before applying memory."
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
await writeMemorySet(taskRepoRoot, after);
|
|
75
|
+
try {
|
|
76
|
+
await deps.git.commitPaths(taskRepoRoot, commitMessage, changedPaths);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
await writeMemorySet(taskRepoRoot, before);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
45
82
|
}
|
|
46
83
|
async function writeRunMemorySet(taskRepoRoot, runId, snapshot, memory) {
|
|
47
84
|
assertCompleteMemorySet(memory);
|
|
@@ -61,8 +98,7 @@ export function createAutoMemoryService(deps) {
|
|
|
61
98
|
return memory;
|
|
62
99
|
}
|
|
63
100
|
async function getState(baseRepoRoot, taskRepoRoot) {
|
|
64
|
-
|
|
65
|
-
let [active, runs, files] = await Promise.all([
|
|
101
|
+
let [active, runs, memoryFiles] = await Promise.all([
|
|
66
102
|
loadActiveState(taskRepoRoot),
|
|
67
103
|
listRuns(taskRepoRoot),
|
|
68
104
|
listMemoryFiles(taskRepoRoot)
|
|
@@ -75,14 +111,13 @@ export function createAutoMemoryService(deps) {
|
|
|
75
111
|
return {
|
|
76
112
|
version: 1,
|
|
77
113
|
status: active?.status ?? "idle",
|
|
78
|
-
files,
|
|
114
|
+
files: memoryFiles.files,
|
|
79
115
|
runs,
|
|
80
116
|
...(active ? { active: toActiveReview(active) } : {}),
|
|
81
|
-
warnings:
|
|
117
|
+
warnings: memoryFiles.warnings
|
|
82
118
|
};
|
|
83
119
|
}
|
|
84
120
|
async function reconcileTask(input) {
|
|
85
|
-
await ensureTaskMemorySnapshot(deps.fs, input.baseRepoRoot, input.taskRepoRoot);
|
|
86
121
|
const active = await loadActiveState(input.taskRepoRoot);
|
|
87
122
|
const preferences = await deps.appSettings.getPreferences();
|
|
88
123
|
if (!preferences.autoMemoryEnabled) {
|
|
@@ -91,6 +126,7 @@ export function createAutoMemoryService(deps) {
|
|
|
91
126
|
}
|
|
92
127
|
return getState(input.baseRepoRoot, input.taskRepoRoot);
|
|
93
128
|
}
|
|
129
|
+
await assertMemoryBlocksInstalled(deps.fs, input.taskRepoRoot);
|
|
94
130
|
if (active?.status === "collecting") {
|
|
95
131
|
await dispatchCurrentDraft(input.baseRepoRoot, input.taskRepoRoot, active);
|
|
96
132
|
return getState(input.baseRepoRoot, input.taskRepoRoot);
|
|
@@ -135,7 +171,7 @@ export function createAutoMemoryService(deps) {
|
|
|
135
171
|
updatedAt: timestamp,
|
|
136
172
|
drafts
|
|
137
173
|
};
|
|
138
|
-
const before = await readMemorySet(input.
|
|
174
|
+
const before = await readMemorySet(input.taskRepoRoot);
|
|
139
175
|
await writeRunMemorySet(input.taskRepoRoot, runId, "before", before);
|
|
140
176
|
await writeRunMemorySet(input.taskRepoRoot, runId, "after", before);
|
|
141
177
|
await persistRun(input.taskRepoRoot, {
|
|
@@ -188,7 +224,7 @@ export function createAutoMemoryService(deps) {
|
|
|
188
224
|
async function isRoleMemoryTurn(taskRepoRoot, role) {
|
|
189
225
|
const state = await loadActiveState(taskRepoRoot);
|
|
190
226
|
const draft = state?.status === "collecting" ? currentDraft(state) : undefined;
|
|
191
|
-
return draft?.role === role && draft.status === "
|
|
227
|
+
return draft?.role === role && draft.status === "dispatched";
|
|
192
228
|
}
|
|
193
229
|
async function handleRoleHook(input) {
|
|
194
230
|
const state = await loadActiveState(input.taskRepoRoot);
|
|
@@ -204,11 +240,6 @@ export function createAutoMemoryService(deps) {
|
|
|
204
240
|
return false;
|
|
205
241
|
}
|
|
206
242
|
if (input.eventName === "UserPromptSubmit" || input.eventName === "PostCompact") {
|
|
207
|
-
if (draft.status !== "running") {
|
|
208
|
-
draft.status = "running";
|
|
209
|
-
state.updatedAt = now();
|
|
210
|
-
await persistActiveState(input.taskRepoRoot, state);
|
|
211
|
-
}
|
|
212
243
|
return true;
|
|
213
244
|
}
|
|
214
245
|
if (input.eventName === "StopFailure") {
|
|
@@ -265,15 +296,19 @@ export function createAutoMemoryService(deps) {
|
|
|
265
296
|
return true;
|
|
266
297
|
}
|
|
267
298
|
if (input.eventName === "Stop") {
|
|
268
|
-
await applyReviewedMemory(input.
|
|
299
|
+
await applyReviewedMemory(input.taskRepoRoot, state);
|
|
269
300
|
return true;
|
|
270
301
|
}
|
|
271
302
|
return true;
|
|
272
303
|
}
|
|
273
|
-
async function getFile(
|
|
274
|
-
await
|
|
304
|
+
async function getFile(_baseRepoRoot, taskRepoRoot, filePath) {
|
|
305
|
+
await assertMemoryBlocksInstalled(deps.fs, taskRepoRoot);
|
|
275
306
|
const definition = requireMemoryFileDefinition(filePath);
|
|
276
|
-
const
|
|
307
|
+
const hostContent = await deps.fs.readText(resolveRepoPath(taskRepoRoot, definition.path));
|
|
308
|
+
const content = readVcmMemoryBlock(hostContent);
|
|
309
|
+
if (content === undefined) {
|
|
310
|
+
throw missingMemoryBlockError(definition.path);
|
|
311
|
+
}
|
|
277
312
|
return {
|
|
278
313
|
path: definition.path,
|
|
279
314
|
title: definition.title,
|
|
@@ -286,13 +321,13 @@ export function createAutoMemoryService(deps) {
|
|
|
286
321
|
async function updateFile(baseRepoRoot, taskRepoRoot, taskSlug, filePath, content) {
|
|
287
322
|
await assertNoActiveReview(taskRepoRoot);
|
|
288
323
|
const definition = requireMemoryFileDefinition(filePath);
|
|
289
|
-
const before = await readMemorySet(
|
|
324
|
+
const before = await readMemorySet(taskRepoRoot);
|
|
290
325
|
const normalizedContent = ensureTrailingNewline(content);
|
|
291
326
|
if (before[definition.path] === normalizedContent) {
|
|
292
327
|
return getState(baseRepoRoot, taskRepoRoot);
|
|
293
328
|
}
|
|
294
329
|
const after = { ...before, [definition.path]: normalizedContent };
|
|
295
|
-
await createAppliedRun(
|
|
330
|
+
await createAppliedRun(taskRepoRoot, taskSlug, "user", before, after);
|
|
296
331
|
return getState(baseRepoRoot, taskRepoRoot);
|
|
297
332
|
}
|
|
298
333
|
async function revertRun(baseRepoRoot, taskRepoRoot, runId) {
|
|
@@ -305,7 +340,7 @@ export function createAutoMemoryService(deps) {
|
|
|
305
340
|
statusCode: 409
|
|
306
341
|
});
|
|
307
342
|
}
|
|
308
|
-
const current = await readMemorySet(
|
|
343
|
+
const current = await readMemorySet(taskRepoRoot);
|
|
309
344
|
if (!sameHashes(hashMemorySet(current), run.afterHashes)) {
|
|
310
345
|
throw new VcmError({
|
|
311
346
|
code: "MEMORY_RUN_CHANGED",
|
|
@@ -315,8 +350,7 @@ export function createAutoMemoryService(deps) {
|
|
|
315
350
|
});
|
|
316
351
|
}
|
|
317
352
|
const before = await readRunMemorySet(taskRepoRoot, run.runId, "before");
|
|
318
|
-
await
|
|
319
|
-
await writeMemorySet(taskRepoRoot, before);
|
|
353
|
+
await applyAndCommitMemorySet(taskRepoRoot, current, before, "chore: revert VCM memory");
|
|
320
354
|
const timestamp = now();
|
|
321
355
|
await persistRun(taskRepoRoot, {
|
|
322
356
|
...run,
|
|
@@ -382,7 +416,7 @@ export function createAutoMemoryService(deps) {
|
|
|
382
416
|
if (session.activityStatus === "running") {
|
|
383
417
|
return;
|
|
384
418
|
}
|
|
385
|
-
draft.status = "
|
|
419
|
+
draft.status = "dispatched";
|
|
386
420
|
state.updatedAt = now();
|
|
387
421
|
await persistActiveState(taskRepoRoot, state);
|
|
388
422
|
await submitTerminalInput(deps.runtime, session.id, buildRoleDraftPrompt(taskRepoRoot, state, draft));
|
|
@@ -399,15 +433,16 @@ export function createAutoMemoryService(deps) {
|
|
|
399
433
|
return;
|
|
400
434
|
}
|
|
401
435
|
try {
|
|
402
|
-
const existing = await deps.sessionService.
|
|
436
|
+
const existing = await deps.sessionService.getRoleSession(baseRepoRoot, state.taskSlug, "harness-engineer");
|
|
403
437
|
if (existing?.activityStatus === "running") {
|
|
404
438
|
return;
|
|
405
439
|
}
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
440
|
+
const input = { cols: 120, rows: 32 };
|
|
441
|
+
const session = existing?.status === "running"
|
|
442
|
+
? existing
|
|
443
|
+
: existing?.claudeSessionId
|
|
444
|
+
? await deps.sessionService.resumeRoleSession(baseRepoRoot, state.taskSlug, "harness-engineer", input)
|
|
445
|
+
: await deps.sessionService.startRoleSession(baseRepoRoot, state.taskSlug, "harness-engineer", input);
|
|
411
446
|
if (session.status !== "running" || session.activityStatus === "running" || !deps.runtime.getSession(session.id)) {
|
|
412
447
|
return;
|
|
413
448
|
}
|
|
@@ -432,12 +467,12 @@ export function createAutoMemoryService(deps) {
|
|
|
432
467
|
}
|
|
433
468
|
return deps.sessionService.startRoleSession(baseRepoRoot, taskSlug, role, options);
|
|
434
469
|
}
|
|
435
|
-
async function applyReviewedMemory(
|
|
470
|
+
async function applyReviewedMemory(taskRepoRoot, state) {
|
|
436
471
|
try {
|
|
437
472
|
const before = await readRunMemorySet(taskRepoRoot, state.runId, "before");
|
|
438
473
|
const after = await readRunMemorySet(taskRepoRoot, state.runId, "after");
|
|
439
474
|
assertCompleteMemorySet(after);
|
|
440
|
-
await
|
|
475
|
+
await applyAndCommitMemorySet(taskRepoRoot, before, after, "chore: update VCM memory");
|
|
441
476
|
const timestamp = now();
|
|
442
477
|
const diff = renderMemoryDiff(before, after);
|
|
443
478
|
const run = await readRun(taskRepoRoot, state.runId);
|
|
@@ -456,28 +491,34 @@ export function createAutoMemoryService(deps) {
|
|
|
456
491
|
await failReview(taskRepoRoot, state, `Harness Engineer memory result could not be applied: ${errorMessage(error)}`);
|
|
457
492
|
}
|
|
458
493
|
}
|
|
459
|
-
async function createAppliedRun(
|
|
494
|
+
async function createAppliedRun(taskRepoRoot, taskSlug, source, before, after) {
|
|
460
495
|
assertCompleteMemorySet(after);
|
|
461
496
|
const timestamp = now();
|
|
462
497
|
const runId = createRunId(timestamp, source);
|
|
463
498
|
await writeRunMemorySet(taskRepoRoot, runId, "before", before);
|
|
464
499
|
await writeRunMemorySet(taskRepoRoot, runId, "after", after);
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
500
|
+
try {
|
|
501
|
+
await applyAndCommitMemorySet(taskRepoRoot, before, after, "chore: update VCM memory");
|
|
502
|
+
const diff = renderMemoryDiff(before, after);
|
|
503
|
+
await persistRun(taskRepoRoot, {
|
|
504
|
+
version: 1,
|
|
505
|
+
runId,
|
|
506
|
+
taskSlug,
|
|
507
|
+
source,
|
|
508
|
+
status: "applied",
|
|
509
|
+
createdAt: timestamp,
|
|
510
|
+
updatedAt: timestamp,
|
|
511
|
+
appliedAt: timestamp,
|
|
512
|
+
beforeHashes: hashMemorySet(before),
|
|
513
|
+
afterHashes: hashMemorySet(after),
|
|
514
|
+
diff
|
|
515
|
+
});
|
|
516
|
+
await deps.fs.writeText(resolveRepoPath(taskRepoRoot, `${MEMORY_REVIEW_RUNS_ROOT}/${runId}/applied.patch`), diff);
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
await deps.fs.removePath?.(resolveRepoPath(taskRepoRoot, `${MEMORY_REVIEW_RUNS_ROOT}/${runId}`), { recursive: true, force: true });
|
|
520
|
+
throw error;
|
|
521
|
+
}
|
|
481
522
|
}
|
|
482
523
|
async function failReview(taskRepoRoot, state, message) {
|
|
483
524
|
const timestamp = now();
|
|
@@ -531,8 +572,21 @@ export function createAutoMemoryService(deps) {
|
|
|
531
572
|
}
|
|
532
573
|
async function listMemoryFiles(taskRepoRoot) {
|
|
533
574
|
const files = [];
|
|
575
|
+
const warnings = [];
|
|
534
576
|
for (const definition of MEMORY_FILE_DEFINITIONS) {
|
|
535
|
-
|
|
577
|
+
let content = "";
|
|
578
|
+
const hostPath = resolveRepoPath(taskRepoRoot, definition.path);
|
|
579
|
+
try {
|
|
580
|
+
if (await deps.fs.pathExists(hostPath)) {
|
|
581
|
+
content = readVcmMemoryBlock(await deps.fs.readText(hostPath)) ?? "";
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
warnings.push(`${definition.path}: ${errorMessage(error)}`);
|
|
586
|
+
}
|
|
587
|
+
if (!content) {
|
|
588
|
+
warnings.push(`${definition.path}: VCM memory block is missing. Refresh the VCM Harness.`);
|
|
589
|
+
}
|
|
536
590
|
files.push({
|
|
537
591
|
path: definition.path,
|
|
538
592
|
title: definition.title,
|
|
@@ -540,7 +594,7 @@ export function createAutoMemoryService(deps) {
|
|
|
540
594
|
sizeBytes: Buffer.byteLength(content, "utf8")
|
|
541
595
|
});
|
|
542
596
|
}
|
|
543
|
-
return files;
|
|
597
|
+
return { files, warnings: [...new Set(warnings)] };
|
|
544
598
|
}
|
|
545
599
|
async function hasCompletedRunForFinalAcceptance(taskRepoRoot, finalAcceptanceHash) {
|
|
546
600
|
return Boolean(await findCompletedRunForFinalAcceptance(taskRepoRoot, finalAcceptanceHash));
|
|
@@ -554,8 +608,22 @@ export function createAutoMemoryService(deps) {
|
|
|
554
608
|
if (!(await deps.fs.pathExists(statePath))) {
|
|
555
609
|
return undefined;
|
|
556
610
|
}
|
|
557
|
-
const
|
|
558
|
-
|
|
611
|
+
const stored = await deps.fs.readJson(statePath);
|
|
612
|
+
if (stored?.version !== 1 || !stored.runId) {
|
|
613
|
+
return undefined;
|
|
614
|
+
}
|
|
615
|
+
const migrated = stored.drafts.some((draft) => draft.status === "running");
|
|
616
|
+
const state = {
|
|
617
|
+
...stored,
|
|
618
|
+
drafts: stored.drafts.map((draft) => ({
|
|
619
|
+
...draft,
|
|
620
|
+
status: draft.status === "running" ? "dispatched" : draft.status
|
|
621
|
+
}))
|
|
622
|
+
};
|
|
623
|
+
if (migrated) {
|
|
624
|
+
await persistActiveState(taskRepoRoot, state);
|
|
625
|
+
}
|
|
626
|
+
return state;
|
|
559
627
|
}
|
|
560
628
|
async function persistActiveState(taskRepoRoot, state) {
|
|
561
629
|
await deps.fs.writeJsonAtomic(resolveRepoPath(taskRepoRoot, MEMORY_REVIEW_STATE_PATH), state);
|
|
@@ -600,9 +668,6 @@ export function createAutoMemoryService(deps) {
|
|
|
600
668
|
return `sha256:${sha256(content)}`;
|
|
601
669
|
}
|
|
602
670
|
return {
|
|
603
|
-
ensureTaskSnapshot(baseRepoRoot, taskRepoRoot) {
|
|
604
|
-
return ensureTaskMemorySnapshot(deps.fs, baseRepoRoot, taskRepoRoot);
|
|
605
|
-
},
|
|
606
671
|
reconcileTask,
|
|
607
672
|
getState,
|
|
608
673
|
getTaskRetrospectiveReadiness,
|
|
@@ -616,21 +681,18 @@ export function createAutoMemoryService(deps) {
|
|
|
616
681
|
assertHarnessEngineerAvailable
|
|
617
682
|
};
|
|
618
683
|
}
|
|
619
|
-
export async function
|
|
684
|
+
export async function assertMemoryBlocksInstalled(fs, taskRepoRoot) {
|
|
620
685
|
for (const definition of MEMORY_FILE_DEFINITIONS) {
|
|
621
|
-
const
|
|
622
|
-
if (!(await fs.pathExists(
|
|
623
|
-
|
|
686
|
+
const hostPath = resolveRepoPath(taskRepoRoot, definition.path);
|
|
687
|
+
if (!(await fs.pathExists(hostPath))) {
|
|
688
|
+
throw missingMemoryBlockError(definition.path);
|
|
624
689
|
}
|
|
625
|
-
const
|
|
626
|
-
if (
|
|
627
|
-
|
|
690
|
+
const content = await fs.readText(hostPath);
|
|
691
|
+
if (readVcmMemoryBlock(content) === undefined) {
|
|
692
|
+
throw missingMemoryBlockError(definition.path);
|
|
628
693
|
}
|
|
629
694
|
}
|
|
630
695
|
}
|
|
631
|
-
function renderDefaultMemoryFile(title) {
|
|
632
|
-
return `# ${title}\n\nNo accumulated project memory yet.\n`;
|
|
633
|
-
}
|
|
634
696
|
function currentDraft(state) {
|
|
635
697
|
return state.drafts.find((draft) => draft.status !== "completed");
|
|
636
698
|
}
|
|
@@ -649,13 +711,17 @@ function toActiveReview(state) {
|
|
|
649
711
|
};
|
|
650
712
|
}
|
|
651
713
|
function buildRoleDraftPrompt(taskRepoRoot, state, draft) {
|
|
714
|
+
const roleDefinition = MEMORY_FILE_DEFINITIONS.find((definition) => "role" in definition && definition.role === draft.role);
|
|
715
|
+
if (!roleDefinition) {
|
|
716
|
+
throw new Error(`Missing memory definition for role: ${draft.role}`);
|
|
717
|
+
}
|
|
652
718
|
return [
|
|
653
719
|
"[VCM Task Harness Review: Memory Proposal]",
|
|
654
720
|
"",
|
|
655
721
|
"Use the vcm-propose-memory skill to submit the assigned proposal.",
|
|
656
722
|
`Task worktree: ${taskRepoRoot}`,
|
|
657
|
-
`Current shared memory: ${resolveRepoPath(taskRepoRoot,
|
|
658
|
-
`Current role memory: ${resolveRepoPath(taskRepoRoot,
|
|
723
|
+
`Current shared memory block: ${resolveRepoPath(taskRepoRoot, "CLAUDE.md")}`,
|
|
724
|
+
`Current role memory block: ${resolveRepoPath(taskRepoRoot, roleDefinition.path)}`,
|
|
659
725
|
`Write the draft to: ${resolveRepoPath(taskRepoRoot, draft.path)}`,
|
|
660
726
|
"",
|
|
661
727
|
"End the turn after writing the draft."
|
|
@@ -672,9 +738,10 @@ function buildHarnessReviewPrompt(taskRepoRoot, state) {
|
|
|
672
738
|
`Current memory snapshot: ${path.join(runRoot, "before")}`,
|
|
673
739
|
`Write the complete reviewed memory set to: ${path.join(runRoot, "after")}`,
|
|
674
740
|
"",
|
|
741
|
+
"Each snapshot file contains only the matching <VCM-memory> block content, not the full host file.",
|
|
675
742
|
"Keep only verified, durable, reusable project knowledge. Merge duplicates, remove stale entries, and keep role-specific knowledge in the matching role file.",
|
|
676
743
|
"Do not record task narrative, temporary state, unverified conclusions, or Harness rules.",
|
|
677
|
-
"Do not edit product code, harness files,
|
|
744
|
+
"Do not edit product code, active harness files, active memory blocks, or review metadata.",
|
|
678
745
|
"All existing files already exist in the after directory. Edit those files in place and end the turn when review is complete."
|
|
679
746
|
].join("\n");
|
|
680
747
|
}
|
|
@@ -702,7 +769,15 @@ function requireSafeRunId(runId) {
|
|
|
702
769
|
return normalized;
|
|
703
770
|
}
|
|
704
771
|
function memoryRunFilePath(runId, snapshot, memoryPath) {
|
|
705
|
-
return `${MEMORY_REVIEW_RUNS_ROOT}/${runId}/${snapshot}/${memoryPath
|
|
772
|
+
return `${MEMORY_REVIEW_RUNS_ROOT}/${runId}/${snapshot}/${memoryPath}`;
|
|
773
|
+
}
|
|
774
|
+
function missingMemoryBlockError(filePath) {
|
|
775
|
+
return new VcmError({
|
|
776
|
+
code: "MEMORY_BLOCK_MISSING",
|
|
777
|
+
message: `VCM memory block is missing from ${filePath}.`,
|
|
778
|
+
statusCode: 409,
|
|
779
|
+
hint: "Refresh the VCM Harness in the active task worktree before using Auto Memory."
|
|
780
|
+
});
|
|
706
781
|
}
|
|
707
782
|
function assertCompleteMemorySet(memory) {
|
|
708
783
|
for (const definition of MEMORY_FILE_DEFINITIONS) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { isGateReviewerRoleName, isHarnessEngineerToolRoleName, isTranslatorToolRoleName,
|
|
1
|
+
import { isGateReviewerRoleName, isHarnessEngineerToolRoleName, isTranslatorToolRoleName, isVcmRoleName } from "../../shared/constants.js";
|
|
2
2
|
import { VcmError } from "../errors.js";
|
|
3
|
-
import {
|
|
3
|
+
import { readTranscriptTurnEvidence } from "./claude-transcript-reply.js";
|
|
4
4
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
5
5
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
6
6
|
const MAX_ROLE_RETRY_ATTEMPTS = 20;
|
|
@@ -74,12 +74,22 @@ export function createClaudeHookService(deps) {
|
|
|
74
74
|
async function processTranslatorHook(input) {
|
|
75
75
|
const eventName = parseHookEvent(input.event.hook_event_name);
|
|
76
76
|
const context = await getTranslatorHookContext();
|
|
77
|
-
const session =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
const session = input.taskSlug === "__project__"
|
|
78
|
+
? await deps.sessionService.recordProjectTranslatorHookEvent(context.project.repoRoot, {
|
|
79
|
+
eventName,
|
|
80
|
+
sessionId: stringOrUndefined(input.event.session_id),
|
|
81
|
+
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
82
|
+
cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd)
|
|
83
|
+
})
|
|
84
|
+
: await deps.sessionService.recordRoleHookEvent(context.project.repoRoot, {
|
|
85
|
+
taskSlug: input.taskSlug,
|
|
86
|
+
role: input.role,
|
|
87
|
+
eventName,
|
|
88
|
+
sessionId: stringOrUndefined(input.event.session_id),
|
|
89
|
+
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
90
|
+
cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd),
|
|
91
|
+
allowSessionMismatch: true
|
|
92
|
+
});
|
|
83
93
|
await deps.translationWorkerService?.handleTranslatorHook(context.project.repoRoot, eventName, input.taskSlug);
|
|
84
94
|
return {
|
|
85
95
|
ok: true,
|
|
@@ -93,15 +103,26 @@ export function createClaudeHookService(deps) {
|
|
|
93
103
|
async function processHarnessEngineerHook(input) {
|
|
94
104
|
const eventName = parseHookEvent(input.event.hook_event_name);
|
|
95
105
|
const context = await getProjectToolHookContext("Harness Engineer");
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
106
|
+
const projectScoped = input.taskSlug === "__project_harness_engineer__";
|
|
107
|
+
const session = projectScoped
|
|
108
|
+
? await deps.sessionService.recordProjectHarnessEngineerHookEvent(context.project.repoRoot, {
|
|
109
|
+
eventName,
|
|
110
|
+
sessionId: stringOrUndefined(input.event.session_id),
|
|
111
|
+
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
112
|
+
cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd)
|
|
113
|
+
})
|
|
114
|
+
: await deps.sessionService.recordRoleHookEvent(context.project.repoRoot, {
|
|
115
|
+
taskSlug: input.taskSlug,
|
|
116
|
+
role: input.role,
|
|
117
|
+
eventName,
|
|
118
|
+
sessionId: stringOrUndefined(input.event.session_id),
|
|
119
|
+
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
120
|
+
cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd),
|
|
121
|
+
allowSessionMismatch: true
|
|
122
|
+
});
|
|
102
123
|
const activeTask = deps.autoMemoryService
|
|
103
124
|
? (await deps.taskService.listTasks(context.project.repoRoot))
|
|
104
|
-
.find((task) => task.cleanupStatus !== "cleaned")
|
|
125
|
+
.find((task) => task.cleanupStatus !== "cleaned" && (projectScoped || task.taskSlug === input.taskSlug))
|
|
105
126
|
: undefined;
|
|
106
127
|
const memoryHandled = activeTask
|
|
107
128
|
? await deps.autoMemoryService?.handleHarnessEngineerHook({
|
|
@@ -221,13 +242,13 @@ export function createClaudeHookService(deps) {
|
|
|
221
242
|
throwUnsupportedEvent(eventName);
|
|
222
243
|
}
|
|
223
244
|
const context = await getHookContext(input);
|
|
224
|
-
if (await isDuplicateCompletedStop(context, input)) {
|
|
225
|
-
return completedHookResult(input, eventName);
|
|
226
|
-
}
|
|
227
245
|
const memoryResult = await processAutoMemoryRoleHook(input, context, eventName);
|
|
228
246
|
if (memoryResult) {
|
|
229
247
|
return memoryResult;
|
|
230
248
|
}
|
|
249
|
+
if (await isDuplicateCompletedStop(context, input)) {
|
|
250
|
+
return completedHookResult(input, eventName);
|
|
251
|
+
}
|
|
231
252
|
await clearStopFailureRecoveryState(context, input.role);
|
|
232
253
|
if (options.allowBlock && deps.jobGuard) {
|
|
233
254
|
const verdict = await deps.jobGuard.evaluateStop({
|
|
@@ -381,7 +402,6 @@ export function createClaudeHookService(deps) {
|
|
|
381
402
|
cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd)
|
|
382
403
|
});
|
|
383
404
|
if (boundToTask) {
|
|
384
|
-
const userFacingReply = await captureUserFacingReply(eventName, input.role, session);
|
|
385
405
|
await deps.roundService.recordClaudeHookEvent({
|
|
386
406
|
repoRoot: context.project.repoRoot,
|
|
387
407
|
stateRepoRoot: context.taskRepoRoot,
|
|
@@ -389,7 +409,6 @@ export function createClaudeHookService(deps) {
|
|
|
389
409
|
taskSlug: context.taskSlug,
|
|
390
410
|
role: input.role,
|
|
391
411
|
eventName,
|
|
392
|
-
...(userFacingReply ? { userFacingReply } : {}),
|
|
393
412
|
...(options.settleGuard
|
|
394
413
|
? {
|
|
395
414
|
settleGuard: async () => {
|
|
@@ -459,6 +478,17 @@ export function createClaudeHookService(deps) {
|
|
|
459
478
|
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
460
479
|
cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd)
|
|
461
480
|
});
|
|
481
|
+
const boundToTask = await isHookSessionBoundToTask(context, input.role);
|
|
482
|
+
if (boundToTask && eventName !== "PostCompact") {
|
|
483
|
+
await deps.roundService.recordClaudeHookEvent({
|
|
484
|
+
repoRoot: context.project.repoRoot,
|
|
485
|
+
stateRepoRoot: context.taskRepoRoot,
|
|
486
|
+
stateRoot: context.config.stateRoot,
|
|
487
|
+
taskSlug: context.taskSlug,
|
|
488
|
+
role: input.role,
|
|
489
|
+
eventName
|
|
490
|
+
});
|
|
491
|
+
}
|
|
462
492
|
await deps.autoMemoryService.handleRoleHook({
|
|
463
493
|
baseRepoRoot: context.project.repoRoot,
|
|
464
494
|
taskRepoRoot: context.taskRepoRoot,
|
|
@@ -755,21 +785,6 @@ function parseHookEvent(value) {
|
|
|
755
785
|
hint: "VCM accepts UserPromptSubmit, Stop, StopFailure, and PostCompact hooks only."
|
|
756
786
|
});
|
|
757
787
|
}
|
|
758
|
-
// On a user-facing role's Stop, best-effort capture its last user-facing turn
|
|
759
|
-
// text to seed the await-user pause message. Capture failure is non-fatal and
|
|
760
|
-
// must never block turn-end, so a missing/failed read just omits the field.
|
|
761
|
-
async function captureUserFacingReply(eventName, role, session) {
|
|
762
|
-
if (eventName !== "Stop" || !isUserFacingRole(role) || !session) {
|
|
763
|
-
return undefined;
|
|
764
|
-
}
|
|
765
|
-
try {
|
|
766
|
-
const reply = await readLatestRoleTurnReply(session);
|
|
767
|
-
return reply ? { text: reply.text, truncated: reply.truncated } : undefined;
|
|
768
|
-
}
|
|
769
|
-
catch {
|
|
770
|
-
return undefined;
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
788
|
function throwUnsupportedEvent(eventName) {
|
|
774
789
|
throw new VcmError({
|
|
775
790
|
code: "HOOK_EVENT_UNSUPPORTED",
|
|
@@ -26,7 +26,7 @@ export function createCommandDispatcher(deps) {
|
|
|
26
26
|
hint: `Start the ${input.role} session before sending a role command.`
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
|
-
const instruction = `
|
|
29
|
+
const instruction = `[VCM ROLE COMMAND]\nRead and execute the role command at: ${commandPath}\n[/VCM ROLE COMMAND]`;
|
|
30
30
|
await submitTerminalInput(deps.runtime, session.id, instruction);
|
|
31
31
|
return {
|
|
32
32
|
taskSlug: input.taskSlug,
|