vibe-coding-master 0.7.50 → 0.8.1
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/dist/backend/adapters/filesystem.js +6 -0
- package/dist/backend/api/task-routes.js +20 -1
- package/dist/backend/server.js +4 -2
- package/dist/backend/services/auto-memory-service.js +111 -34
- package/dist/backend/services/claude-hook-service.js +5 -3
- package/dist/backend/services/gate-review-service.js +21 -2
- package/dist/backend/services/harness-feedback-service.js +53 -15
- package/dist/backend/services/message-service.js +10 -0
- package/dist/backend/services/runtime-recovery-service.js +9 -0
- package/dist/backend/services/translation-service.js +74 -3
- package/dist/backend/services/workflow-control-service.js +436 -75
- package/dist/backend/templates/handoff.js +14 -2
- package/dist/backend/templates/harness/architect-agent.js +6 -5
- package/dist/backend/templates/harness/architect-evidence-worker-agent.js +2 -0
- package/dist/backend/templates/harness/architect-validation-worker-agent.js +1 -1
- package/dist/backend/templates/harness/claude-root.js +3 -3
- package/dist/backend/templates/harness/coder-agent.js +1 -0
- package/dist/backend/templates/harness/harness-engineer-agent.js +14 -11
- package/dist/backend/templates/harness/memory-block.js +10 -0
- package/dist/backend/templates/harness/project-manager-agent.js +5 -1
- package/dist/backend/templates/harness/tester-agent.js +6 -0
- package/dist/backend/templates/harness/vcm-workflow-review-skill.js +17 -1
- package/dist/shared/validation/artifact-check.js +36 -3
- package/dist/shared/validation/artifact-contract.js +7 -0
- package/dist/shared/validation/artifact-registry.js +4 -1
- package/dist-frontend/assets/{index-DLsIPTvK.js → index-Dh7uVCmk.js} +1 -1
- package/dist-frontend/index.html +1 -1
- package/package.json +1 -1
- package/scripts/harness-tools/vcm-bash-guard +245 -21
|
@@ -41,6 +41,12 @@ export function createNodeFileSystemAdapter() {
|
|
|
41
41
|
}
|
|
42
42
|
});
|
|
43
43
|
},
|
|
44
|
+
async fileVersion(targetPath) {
|
|
45
|
+
return runFileOperation(async () => {
|
|
46
|
+
const stat = await fs.stat(targetPath, { bigint: true });
|
|
47
|
+
return `${stat.mtimeNs}:${stat.size}`;
|
|
48
|
+
});
|
|
49
|
+
},
|
|
44
50
|
async writeText(targetPath, content) {
|
|
45
51
|
await runFileOperation(async () => {
|
|
46
52
|
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
@@ -65,11 +65,17 @@ export function registerTaskRoutes(app, deps) {
|
|
|
65
65
|
taskSlug
|
|
66
66
|
}) ?? Promise.resolve(degradedWorkflowState(taskSlug))
|
|
67
67
|
]);
|
|
68
|
+
const displayedRoundState = await delayFlowPauseForTranslation(deps, {
|
|
69
|
+
repoRoot: project.repoRoot,
|
|
70
|
+
taskRepoRoot,
|
|
71
|
+
taskSlug,
|
|
72
|
+
roundState
|
|
73
|
+
});
|
|
68
74
|
return {
|
|
69
75
|
taskStatus,
|
|
70
76
|
messages,
|
|
71
77
|
orchestration,
|
|
72
|
-
roundState,
|
|
78
|
+
roundState: displayedRoundState,
|
|
73
79
|
workflowState,
|
|
74
80
|
architectRestart: deps.architectRestartService.getState(project.repoRoot, taskSlug),
|
|
75
81
|
roleStallWarning: deps.roleStallDetector.getWarning(project.repoRoot, taskSlug)
|
|
@@ -136,6 +142,19 @@ export function registerTaskRoutes(app, deps) {
|
|
|
136
142
|
return deps.taskCloseService.closeTask(project.repoRoot, request.params.taskSlug);
|
|
137
143
|
});
|
|
138
144
|
}
|
|
145
|
+
async function delayFlowPauseForTranslation(deps, input) {
|
|
146
|
+
if (!input.roundState.flowPause?.paused || !deps.translationService) {
|
|
147
|
+
return input.roundState;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const pending = await deps.translationService.shouldDelayFlowPauseNotification(input);
|
|
151
|
+
return pending ? { ...input.roundState, flowPause: undefined } : input.roundState;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// Translation-state failures must release, rather than suppress, the pause alert.
|
|
155
|
+
return input.roundState;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
139
158
|
function requireWarningId(value) {
|
|
140
159
|
if (typeof value === "string" && value.trim()) {
|
|
141
160
|
return value.trim();
|
package/dist/backend/server.js
CHANGED
|
@@ -137,7 +137,8 @@ export async function createServer(deps, options = {}) {
|
|
|
137
137
|
roundService: deps.roundService,
|
|
138
138
|
taskWorkflowService: deps.taskWorkflowService,
|
|
139
139
|
architectRestartService: deps.architectRestartService,
|
|
140
|
-
roleStallDetector: deps.roleStallDetector
|
|
140
|
+
roleStallDetector: deps.roleStallDetector,
|
|
141
|
+
translationService: deps.translationService
|
|
141
142
|
});
|
|
142
143
|
registerSessionRoutes(app, {
|
|
143
144
|
projectService: deps.projectService,
|
|
@@ -411,7 +412,8 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
411
412
|
taskService,
|
|
412
413
|
translationWorkerService,
|
|
413
414
|
architectRestartService,
|
|
414
|
-
roleContextRestartService
|
|
415
|
+
roleContextRestartService,
|
|
416
|
+
workflowControlService
|
|
415
417
|
});
|
|
416
418
|
const claudeHookService = createClaudeHookService({
|
|
417
419
|
projectService,
|
|
@@ -4,7 +4,7 @@ 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
|
-
import { readVcmMemoryBlock, replaceVcmMemoryBlock } from "../templates/harness/memory-block.js";
|
|
7
|
+
import { readVcmMemoryHostFrame, readVcmMemoryBlock, replaceVcmMemoryBlock } from "../templates/harness/memory-block.js";
|
|
8
8
|
import { ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH, architectPlanningCandidateSnapshotPath, memoryReviewRoleDraftPath, MEMORY_REVIEW_RUNS_ROOT, MEMORY_REVIEW_STATE_PATH } from "./memory-review-paths.js";
|
|
9
9
|
import { parseMemoryProposal, validateMemoryProposal } from "./memory-proposal-validation.js";
|
|
10
10
|
const MEMORY_FILE_DEFINITIONS = [
|
|
@@ -101,15 +101,20 @@ export function createAutoMemoryService(deps) {
|
|
|
101
101
|
await deps.fs.writeText(resolveRepoPath(taskRepoRoot, memoryRunHostFilePath(runId, definition.path)), await deps.fs.readText(resolveRepoPath(taskRepoRoot, definition.path)));
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
-
async function assertOnlyMemoryBlocksChanged(taskRepoRoot, runId
|
|
104
|
+
async function assertOnlyMemoryBlocksChanged(taskRepoRoot, runId) {
|
|
105
105
|
for (const definition of MEMORY_FILE_DEFINITIONS) {
|
|
106
106
|
const beforeHost = await deps.fs.readText(resolveRepoPath(taskRepoRoot, memoryRunHostFilePath(runId, definition.path)));
|
|
107
107
|
const currentHost = await deps.fs.readText(resolveRepoPath(taskRepoRoot, definition.path));
|
|
108
|
-
const
|
|
109
|
-
|
|
108
|
+
const beforeFrame = readVcmMemoryHostFrame(beforeHost);
|
|
109
|
+
const currentFrame = readVcmMemoryHostFrame(currentHost);
|
|
110
|
+
if (!beforeFrame || !currentFrame) {
|
|
111
|
+
throw missingMemoryBlockError(definition.path);
|
|
112
|
+
}
|
|
113
|
+
const difference = describeMemoryHostDifference(beforeFrame, currentFrame);
|
|
114
|
+
if (difference) {
|
|
110
115
|
throw new VcmError({
|
|
111
116
|
code: "MEMORY_REVIEW_SCOPE_CHANGED",
|
|
112
|
-
message: `Harness Engineer changed content outside the VCM memory block: ${definition.path}
|
|
117
|
+
message: `Harness Engineer changed content outside the VCM memory block: ${definition.path} (${difference}).`,
|
|
113
118
|
statusCode: 409,
|
|
114
119
|
hint: "Restore non-memory content, keep only the reviewed <VCM-memory> edit, and commit the correction."
|
|
115
120
|
});
|
|
@@ -302,6 +307,7 @@ export function createAutoMemoryService(deps) {
|
|
|
302
307
|
});
|
|
303
308
|
}
|
|
304
309
|
await writeRunMemoryHostSnapshot(taskRepoRoot, state.runId);
|
|
310
|
+
const existingEntries = collectExistingMemoryReviewEntries(beforeMemory);
|
|
305
311
|
const timestamp = now();
|
|
306
312
|
state.reviewPromptDispatchedAt = timestamp;
|
|
307
313
|
state.retrospectiveReportPath = retrospectiveReportPath;
|
|
@@ -309,12 +315,19 @@ export function createAutoMemoryService(deps) {
|
|
|
309
315
|
state.updatedAt = timestamp;
|
|
310
316
|
await persistActiveState(taskRepoRoot, state);
|
|
311
317
|
const runRoot = resolveRepoPath(taskRepoRoot, `${MEMORY_REVIEW_RUNS_ROOT}/${state.runId}`);
|
|
318
|
+
const existingEntriesPath = path.join(runRoot, "existing-entries.json");
|
|
319
|
+
await deps.fs.writeJsonAtomic(existingEntriesPath, {
|
|
320
|
+
version: 1,
|
|
321
|
+
runId: state.runId,
|
|
322
|
+
entries: existingEntries
|
|
323
|
+
});
|
|
312
324
|
const planningCandidatePath = await findPlanningCandidateSnapshot(taskRepoRoot, state.runId);
|
|
313
325
|
return {
|
|
314
326
|
runId: state.runId,
|
|
315
327
|
roleDraftsPath: path.join(runRoot, "drafts"),
|
|
316
328
|
currentMemoryPath: path.join(runRoot, "before"),
|
|
317
329
|
activeMemoryPaths: memoryPaths.map((memoryPath) => resolveRepoPath(taskRepoRoot, memoryPath)),
|
|
330
|
+
existingEntriesPath,
|
|
318
331
|
reviewResultPath: path.join(runRoot, "review-result.json"),
|
|
319
332
|
proposalCandidates,
|
|
320
333
|
...(planningCandidatePath
|
|
@@ -825,7 +838,7 @@ export function createAutoMemoryService(deps) {
|
|
|
825
838
|
}
|
|
826
839
|
const before = await readRunMemorySet(taskRepoRoot, state.runId, "before");
|
|
827
840
|
const after = await readMemorySet(taskRepoRoot);
|
|
828
|
-
await assertOnlyMemoryBlocksChanged(taskRepoRoot, state.runId
|
|
841
|
+
await assertOnlyMemoryBlocksChanged(taskRepoRoot, state.runId);
|
|
829
842
|
const memoryPaths = MEMORY_FILE_DEFINITIONS.map((definition) => definition.path);
|
|
830
843
|
const uncommittedMemoryDiff = await deps.git.getDiff(taskRepoRoot, "HEAD", null, memoryPaths);
|
|
831
844
|
if (uncommittedMemoryDiff.trim()) {
|
|
@@ -966,31 +979,35 @@ export function createAutoMemoryService(deps) {
|
|
|
966
979
|
statusCode: 409
|
|
967
980
|
});
|
|
968
981
|
}
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
982
|
+
const existingEntries = collectExistingMemoryReviewEntries(before);
|
|
983
|
+
const existingDecisions = result.decisions.filter((candidate) => candidate.source === "existing");
|
|
984
|
+
for (const decision of existingDecisions) {
|
|
985
|
+
const assigned = existingEntries.find((entry) => entry.itemId === decision.itemId);
|
|
986
|
+
if (!assigned) {
|
|
972
987
|
throw new VcmError({
|
|
973
988
|
code: "MEMORY_REVIEW_EXISTING_DECISION_UNKNOWN",
|
|
974
989
|
message: `review-result.json contains an unknown existing-memory decision: ${decision.itemId}`,
|
|
975
990
|
statusCode: 409
|
|
976
991
|
});
|
|
977
992
|
}
|
|
993
|
+
if (decision.target !== assigned.target || decision.entry !== assigned.entry) {
|
|
994
|
+
throw new VcmError({
|
|
995
|
+
code: "MEMORY_REVIEW_EXISTING_DECISION_MISMATCH",
|
|
996
|
+
message: `Existing-memory decision must copy the assigned target and entry exactly: ${decision.itemId}`,
|
|
997
|
+
statusCode: 409
|
|
998
|
+
});
|
|
999
|
+
}
|
|
978
1000
|
}
|
|
979
|
-
for (const
|
|
980
|
-
const
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
code: "MEMORY_REVIEW_EXISTING_DECISION_MISSING",
|
|
988
|
-
message: `review-result.json must contain exactly one decision for existing memory entry in ${definition.path}: ${entry}`,
|
|
989
|
-
statusCode: 409
|
|
990
|
-
});
|
|
991
|
-
}
|
|
992
|
-
validateExistingMemoryDecision(definition.path, entry, matches[0], after);
|
|
1001
|
+
for (const assigned of existingEntries) {
|
|
1002
|
+
const matches = existingDecisions.filter((decision) => decision.itemId === assigned.itemId);
|
|
1003
|
+
if (matches.length !== 1) {
|
|
1004
|
+
throw new VcmError({
|
|
1005
|
+
code: "MEMORY_REVIEW_EXISTING_DECISION_MISSING",
|
|
1006
|
+
message: `review-result.json must contain exactly one decision for existing memory item ${assigned.itemId} in ${assigned.memoryPath}.`,
|
|
1007
|
+
statusCode: 409
|
|
1008
|
+
});
|
|
993
1009
|
}
|
|
1010
|
+
validateExistingMemoryDecision(assigned.memoryPath, assigned.entry, matches[0], after);
|
|
994
1011
|
}
|
|
995
1012
|
const moveDecisions = result.decisions.filter((decision) => decision.decision === "move-to-durable-doc");
|
|
996
1013
|
if (moveDecisions.length !== result.durableDocAssignments.length) {
|
|
@@ -1002,7 +1019,6 @@ export function createAutoMemoryService(deps) {
|
|
|
1002
1019
|
}
|
|
1003
1020
|
const assignmentKeys = new Set();
|
|
1004
1021
|
for (const assignment of result.durableDocAssignments) {
|
|
1005
|
-
validateDurableDocAssignmentInput(assignment, after);
|
|
1006
1022
|
const key = `${assignment.sourceMemoryPath}\n${assignment.sourceEntry}\n${assignment.targetPath}`;
|
|
1007
1023
|
if (assignmentKeys.has(key)) {
|
|
1008
1024
|
throw new VcmError({
|
|
@@ -1022,6 +1038,7 @@ export function createAutoMemoryService(deps) {
|
|
|
1022
1038
|
statusCode: 409
|
|
1023
1039
|
});
|
|
1024
1040
|
}
|
|
1041
|
+
validateDurableDocAssignmentInput(assignment, matchingDecision.source, after);
|
|
1025
1042
|
}
|
|
1026
1043
|
return result;
|
|
1027
1044
|
}
|
|
@@ -1034,7 +1051,7 @@ export function createAutoMemoryService(deps) {
|
|
|
1034
1051
|
statusCode: 409
|
|
1035
1052
|
});
|
|
1036
1053
|
}
|
|
1037
|
-
const
|
|
1054
|
+
const afterContent = after[memoryTargetToPath(decision.target)];
|
|
1038
1055
|
if (candidate.operation === "remove") {
|
|
1039
1056
|
if (decision.decision !== "remove" && decision.decision !== "retain") {
|
|
1040
1057
|
throw new VcmError({
|
|
@@ -1043,14 +1060,14 @@ export function createAutoMemoryService(deps) {
|
|
|
1043
1060
|
statusCode: 409
|
|
1044
1061
|
});
|
|
1045
1062
|
}
|
|
1046
|
-
if (decision.decision === "remove" &&
|
|
1063
|
+
if (decision.decision === "remove" && memoryContainsReviewContent(afterContent, expectedEntry)) {
|
|
1047
1064
|
throw new VcmError({
|
|
1048
1065
|
code: "MEMORY_REVIEW_PROPOSAL_REMOVAL_MISMATCH",
|
|
1049
1066
|
message: `Accepted removal is still present for proposal ${candidate.id}.`,
|
|
1050
1067
|
statusCode: 409
|
|
1051
1068
|
});
|
|
1052
1069
|
}
|
|
1053
|
-
if (decision.decision === "retain" && !
|
|
1070
|
+
if (decision.decision === "retain" && !memoryContainsReviewContent(afterContent, expectedEntry)) {
|
|
1054
1071
|
throw new VcmError({
|
|
1055
1072
|
code: "MEMORY_REVIEW_PROPOSAL_RETAIN_MISMATCH",
|
|
1056
1073
|
message: `Rejected removal is missing from memory for proposal ${candidate.id}.`,
|
|
@@ -1067,7 +1084,7 @@ export function createAutoMemoryService(deps) {
|
|
|
1067
1084
|
});
|
|
1068
1085
|
}
|
|
1069
1086
|
if ((decision.decision === "keep-in-memory" || decision.decision === "keep-memory-reference")
|
|
1070
|
-
&& !
|
|
1087
|
+
&& !memoryContainsReviewContent(afterContent, decision.finalContent)) {
|
|
1071
1088
|
throw new VcmError({
|
|
1072
1089
|
code: "MEMORY_REVIEW_PROPOSAL_CONTENT_MISSING",
|
|
1073
1090
|
message: `Accepted proposal content is missing from memory for ${candidate.id}.`,
|
|
@@ -1076,7 +1093,7 @@ export function createAutoMemoryService(deps) {
|
|
|
1076
1093
|
}
|
|
1077
1094
|
}
|
|
1078
1095
|
function validateExistingMemoryDecision(memoryPath, entry, decision, after) {
|
|
1079
|
-
const afterEntries = new Set(
|
|
1096
|
+
const afterEntries = new Set(parseExistingMemoryEntries(after[memoryPath]));
|
|
1080
1097
|
if (decision.decision === "retain" && !afterEntries.has(entry)) {
|
|
1081
1098
|
throw new VcmError({
|
|
1082
1099
|
code: "MEMORY_REVIEW_RETAIN_MISMATCH",
|
|
@@ -1101,7 +1118,7 @@ export function createAutoMemoryService(deps) {
|
|
|
1101
1118
|
}
|
|
1102
1119
|
}
|
|
1103
1120
|
}
|
|
1104
|
-
function validateDurableDocAssignmentInput(assignment, after) {
|
|
1121
|
+
function validateDurableDocAssignmentInput(assignment, source, after) {
|
|
1105
1122
|
if (!MEMORY_FILE_DEFINITIONS.some((definition) => definition.path === assignment.sourceMemoryPath)) {
|
|
1106
1123
|
throw new VcmError({
|
|
1107
1124
|
code: "MEMORY_REVIEW_ASSIGNMENT_SOURCE_INVALID",
|
|
@@ -1109,7 +1126,10 @@ export function createAutoMemoryService(deps) {
|
|
|
1109
1126
|
statusCode: 409
|
|
1110
1127
|
});
|
|
1111
1128
|
}
|
|
1112
|
-
|
|
1129
|
+
const sourceStillPresent = source === "existing"
|
|
1130
|
+
? parseExistingMemoryEntries(after[assignment.sourceMemoryPath]).includes(assignment.sourceEntry)
|
|
1131
|
+
: memoryContainsReviewContent(after[assignment.sourceMemoryPath], assignment.sourceEntry);
|
|
1132
|
+
if (sourceStillPresent) {
|
|
1113
1133
|
throw new VcmError({
|
|
1114
1134
|
code: "MEMORY_REVIEW_ASSIGNMENT_SOURCE_NOT_REMOVED",
|
|
1115
1135
|
message: `Move-to-durable-doc must remove memory before assignment: ${assignment.sourceEntry}`,
|
|
@@ -1451,11 +1471,47 @@ function memoryTargetToPath(target) {
|
|
|
1451
1471
|
}
|
|
1452
1472
|
return definition.path;
|
|
1453
1473
|
}
|
|
1454
|
-
function
|
|
1474
|
+
function collectExistingMemoryReviewEntries(memory) {
|
|
1475
|
+
return MEMORY_FILE_DEFINITIONS.flatMap((definition) => {
|
|
1476
|
+
const target = memoryPathToTarget(definition.path);
|
|
1477
|
+
return parseExistingMemoryEntries(memory[definition.path]).map((entry, index) => ({
|
|
1478
|
+
itemId: `existing:${target}:${index + 1}:${sha256(entry).slice(0, 12)}`,
|
|
1479
|
+
target,
|
|
1480
|
+
memoryPath: definition.path,
|
|
1481
|
+
entry
|
|
1482
|
+
}));
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
function parseExistingMemoryEntries(content) {
|
|
1486
|
+
const normalized = content.replace(/\r\n?/g, "\n").trim();
|
|
1487
|
+
if (!normalized) {
|
|
1488
|
+
return [];
|
|
1489
|
+
}
|
|
1490
|
+
const lines = normalized.split("\n");
|
|
1491
|
+
const sectionStarts = lines
|
|
1492
|
+
.map((line, index) => (/^##(?:\s+|$)/.test(line) ? index : -1))
|
|
1493
|
+
.filter((index) => index >= 0);
|
|
1494
|
+
if (sectionStarts.length === 0) {
|
|
1495
|
+
return [normalized];
|
|
1496
|
+
}
|
|
1497
|
+
return sectionStarts.map((start, index) => {
|
|
1498
|
+
const end = sectionStarts[index + 1] ?? lines.length;
|
|
1499
|
+
return lines.slice(start, end).join("\n").trim();
|
|
1500
|
+
}).filter(Boolean);
|
|
1501
|
+
}
|
|
1502
|
+
function memoryContainsReviewContent(content, expected) {
|
|
1503
|
+
const normalizedExpected = expected.replace(/\r\n?/g, "\n").trim();
|
|
1504
|
+
if (!normalizedExpected) {
|
|
1505
|
+
return false;
|
|
1506
|
+
}
|
|
1507
|
+
if (normalizedExpected.includes("\n") || /^##(?:\s+|$)/.test(normalizedExpected)) {
|
|
1508
|
+
return parseExistingMemoryEntries(content).includes(normalizedExpected);
|
|
1509
|
+
}
|
|
1455
1510
|
return content
|
|
1456
|
-
.
|
|
1511
|
+
.replace(/\r\n?/g, "\n")
|
|
1512
|
+
.split("\n")
|
|
1457
1513
|
.map((line) => line.trim())
|
|
1458
|
-
.
|
|
1514
|
+
.includes(normalizedExpected);
|
|
1459
1515
|
}
|
|
1460
1516
|
function assertDurableDocPath(targetPath) {
|
|
1461
1517
|
const normalized = normalizeProjectRelativePath(targetPath);
|
|
@@ -1570,6 +1626,27 @@ function memoryRunFilePath(runId, snapshot, memoryPath) {
|
|
|
1570
1626
|
function memoryRunHostFilePath(runId, memoryPath) {
|
|
1571
1627
|
return `${MEMORY_REVIEW_RUNS_ROOT}/${runId}/host-before/${memoryPath}`;
|
|
1572
1628
|
}
|
|
1629
|
+
function describeMemoryHostDifference(before, current) {
|
|
1630
|
+
if (before.beforeBlock !== current.beforeBlock) {
|
|
1631
|
+
return describeTextDifference("before-block content", before.beforeBlock, current.beforeBlock);
|
|
1632
|
+
}
|
|
1633
|
+
if (before.afterBlock !== current.afterBlock) {
|
|
1634
|
+
return describeTextDifference("after-block content", before.afterBlock, current.afterBlock);
|
|
1635
|
+
}
|
|
1636
|
+
return undefined;
|
|
1637
|
+
}
|
|
1638
|
+
function describeTextDifference(label, before, current) {
|
|
1639
|
+
const limit = Math.min(before.length, current.length);
|
|
1640
|
+
let offset = 0;
|
|
1641
|
+
while (offset < limit && before[offset] === current[offset]) {
|
|
1642
|
+
offset += 1;
|
|
1643
|
+
}
|
|
1644
|
+
const excerptStart = Math.max(0, offset - 20);
|
|
1645
|
+
const excerptEnd = offset + 40;
|
|
1646
|
+
const snapshotExcerpt = JSON.stringify(before.slice(excerptStart, excerptEnd));
|
|
1647
|
+
const currentExcerpt = JSON.stringify(current.slice(excerptStart, excerptEnd));
|
|
1648
|
+
return `${label} differs at character ${offset}; snapshot=${snapshotExcerpt}; current=${currentExcerpt}`;
|
|
1649
|
+
}
|
|
1573
1650
|
function missingMemoryBlockError(filePath) {
|
|
1574
1651
|
return new VcmError({
|
|
1575
1652
|
code: "MEMORY_BLOCK_MISSING",
|
|
@@ -158,7 +158,8 @@ export function createClaudeHookService(deps) {
|
|
|
158
158
|
? await deps.harnessFeedbackService?.handleTaskRetrospectiveHook(context.project.repoRoot, {
|
|
159
159
|
taskSlug: activeTask.taskSlug,
|
|
160
160
|
eventName,
|
|
161
|
-
memoryReviewStatus: memoryState?.status ?? "idle"
|
|
161
|
+
memoryReviewStatus: memoryState?.status ?? "idle",
|
|
162
|
+
memoryReviewError: memoryState?.active?.error
|
|
162
163
|
})
|
|
163
164
|
: false;
|
|
164
165
|
if (memoryHandled || retrospectiveHandled) {
|
|
@@ -1097,7 +1098,8 @@ function asksUserQuestion(message) {
|
|
|
1097
1098
|
.split(/\r?\n/)
|
|
1098
1099
|
.map((line) => line.trim())
|
|
1099
1100
|
.filter(Boolean);
|
|
1100
|
-
return lines.some((line) => /[??]
|
|
1101
|
+
return lines.some((line) => /[??]/.test(line)
|
|
1101
1102
|
|| /^(?:please\s+(?:answer|choose|confirm|decide|provide|select|tell)|(?:can|could|do|does|is|are|should|will|would)\s+you\b)/i.test(line)
|
|
1102
|
-
||
|
|
1103
|
+
|| /\b(?:please\s+(?:reply|respond|choose|confirm|decide|provide|select|tell|give)|i\s+need\s+your\s+(?:decision|choice|confirmation|answer)|let\s+me\s+know|your\s+(?:decision|choice|confirmation|answer)\s+is\s+required|waiting\s+for\s+your)\b/i.test(line)
|
|
1104
|
+
|| /(?:请(?:回答|回复|选择|确认|决定|提供|告知|给出|说明)|你(?:是否|能否|要不要|可否)|是否需要你|需要你(?:选择|确认|决定|提供|告知)|需要您的(?:选择|确认|决定|回复)|等待您的(?:选择|确认|决定|回复)|告诉我|告知我)/.test(line));
|
|
1103
1105
|
}
|
|
@@ -836,6 +836,9 @@ export function createGateReviewService(deps) {
|
|
|
836
836
|
async skipReviewGate(repoRoot, taskSlug, gate, input) {
|
|
837
837
|
const context = await getContext(repoRoot, taskSlug);
|
|
838
838
|
assertExceptionReason(input.reason);
|
|
839
|
+
const exceptionInputHash = gate === "code-diff"
|
|
840
|
+
? undefined
|
|
841
|
+
: await tryComputeInputHash(deps, context.taskRepoRoot, gate);
|
|
839
842
|
const index = await withGateStateLock(context, async () => {
|
|
840
843
|
const timestamp = now();
|
|
841
844
|
const current = await loadIndex(deps.fs, context, timestamp);
|
|
@@ -850,6 +853,7 @@ export function createGateReviewService(deps) {
|
|
|
850
853
|
const next = applyGateState(current, gate, {
|
|
851
854
|
status: "skipped",
|
|
852
855
|
decision: undefined,
|
|
856
|
+
inputHash: exceptionInputHash ?? current.gates[gate].inputHash,
|
|
853
857
|
exceptionReason: input.reason,
|
|
854
858
|
error: undefined,
|
|
855
859
|
completedAt: timestamp,
|
|
@@ -867,6 +871,9 @@ export function createGateReviewService(deps) {
|
|
|
867
871
|
async overrideReviewGate(repoRoot, taskSlug, gate, input) {
|
|
868
872
|
const context = await getContext(repoRoot, taskSlug);
|
|
869
873
|
assertExceptionReason(input.reason);
|
|
874
|
+
const exceptionInputHash = gate === "code-diff"
|
|
875
|
+
? undefined
|
|
876
|
+
: await tryComputeInputHash(deps, context.taskRepoRoot, gate);
|
|
870
877
|
const index = await withGateStateLock(context, async () => {
|
|
871
878
|
const timestamp = now();
|
|
872
879
|
const current = await loadIndex(deps.fs, context, timestamp);
|
|
@@ -881,6 +888,7 @@ export function createGateReviewService(deps) {
|
|
|
881
888
|
const next = applyGateState(current, gate, {
|
|
882
889
|
status: "overridden",
|
|
883
890
|
decision: "approve",
|
|
891
|
+
inputHash: exceptionInputHash ?? current.gates[gate].inputHash,
|
|
884
892
|
exceptionReason: input.reason,
|
|
885
893
|
error: undefined,
|
|
886
894
|
completedAt: timestamp,
|
|
@@ -1319,8 +1327,11 @@ async function readCodeDiffEvidenceError(fs, taskRepoRoot, source) {
|
|
|
1319
1327
|
}
|
|
1320
1328
|
async function readCodeDiffValidationGateError(deps, context, index) {
|
|
1321
1329
|
const validationGate = index.gates["validation-adequacy"];
|
|
1322
|
-
if (validationGate.required
|
|
1323
|
-
|
|
1330
|
+
if (validationGate.required) {
|
|
1331
|
+
const passed = (validationGate.status === "completed" && validationGate.decision === "approve")
|
|
1332
|
+
|| validationGate.status === "skipped"
|
|
1333
|
+
|| validationGate.status === "overridden";
|
|
1334
|
+
if (!passed) {
|
|
1324
1335
|
return "code-diff requires the validation-adequacy Gate to complete successfully for the current Tester evidence.";
|
|
1325
1336
|
}
|
|
1326
1337
|
const currentValidationHash = await computeInputHash(deps, context.taskRepoRoot, "validation-adequacy");
|
|
@@ -1330,6 +1341,14 @@ async function readCodeDiffValidationGateError(deps, context, index) {
|
|
|
1330
1341
|
}
|
|
1331
1342
|
return undefined;
|
|
1332
1343
|
}
|
|
1344
|
+
async function tryComputeInputHash(deps, taskRepoRoot, gate) {
|
|
1345
|
+
try {
|
|
1346
|
+
return await computeInputHash(deps, taskRepoRoot, gate);
|
|
1347
|
+
}
|
|
1348
|
+
catch {
|
|
1349
|
+
return undefined;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1333
1352
|
async function readArchitectureEvidenceError(fs, taskRepoRoot) {
|
|
1334
1353
|
const relativePath = ".ai/vcm/handoffs/architecture-evidence.md";
|
|
1335
1354
|
const absolutePath = resolveRepoPath(taskRepoRoot, relativePath);
|
|
@@ -11,15 +11,18 @@ const TASK_RETROSPECTIVE_DIR = `${FEEDBACK_ROOT}/task-retrospectives`;
|
|
|
11
11
|
const LEGACY_STATE_PATH = `${FEEDBACK_ROOT}/state.json`;
|
|
12
12
|
export function createHarnessFeedbackService(deps) {
|
|
13
13
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
14
|
-
async function getState(repoRoot,
|
|
14
|
+
async function getState(repoRoot, activeTaskSlug) {
|
|
15
15
|
await cleanupLegacyState(repoRoot);
|
|
16
16
|
const pending = await listPendingFeedback(repoRoot);
|
|
17
|
+
const marker = activeTaskSlug
|
|
18
|
+
? await loadTaskRetrospectiveMarker(repoRoot, activeTaskSlug)
|
|
19
|
+
: undefined;
|
|
17
20
|
return {
|
|
18
21
|
version: 1,
|
|
19
22
|
status: pending.length > 0 ? "queued" : "idle",
|
|
20
23
|
queuedCount: pending.length,
|
|
21
24
|
pending,
|
|
22
|
-
warnings: []
|
|
25
|
+
warnings: marker?.status === "failed" && marker.error ? [marker.error] : []
|
|
23
26
|
};
|
|
24
27
|
}
|
|
25
28
|
async function sendPendingFeedback(repoRoot, input) {
|
|
@@ -143,27 +146,41 @@ export function createHarnessFeedbackService(deps) {
|
|
|
143
146
|
]
|
|
144
147
|
: ["Report is empty."];
|
|
145
148
|
const reportReady = reportErrors.length === 0;
|
|
146
|
-
if (!reportReady
|
|
149
|
+
if (!reportReady) {
|
|
147
150
|
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
148
151
|
...marker,
|
|
149
152
|
status: "failed",
|
|
150
153
|
failedAt: timestamp,
|
|
151
154
|
updatedAt: timestamp,
|
|
152
|
-
error:
|
|
153
|
-
? `Harness Engineer did not write a valid Task Harness Retrospective report: ${reportErrors.join(" ")}`
|
|
154
|
-
: "Task Harness Retrospective memory review failed."
|
|
155
|
+
error: `Harness Engineer did not write a valid Task Harness Retrospective report: ${reportErrors.join(" ")}`
|
|
155
156
|
});
|
|
156
157
|
return true;
|
|
157
158
|
}
|
|
158
|
-
|
|
159
|
+
const processedMarker = await consumeAcceptedFeedback(repoRoot, marker);
|
|
160
|
+
if (!processedMarker) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
if (processedMarker.memoryRunId && input.memoryReviewStatus === "failed") {
|
|
159
164
|
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
160
|
-
...
|
|
165
|
+
...processedMarker,
|
|
166
|
+
status: "failed",
|
|
167
|
+
failedAt: timestamp,
|
|
168
|
+
updatedAt: timestamp,
|
|
169
|
+
error: input.memoryReviewError
|
|
170
|
+
? `Task Harness Retrospective memory review failed: ${input.memoryReviewError}`
|
|
171
|
+
: "Task Harness Retrospective memory review failed. Open Harness Studio Memory to inspect the validation error."
|
|
172
|
+
});
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
if (processedMarker.memoryRunId && input.memoryReviewStatus === "documenting") {
|
|
176
|
+
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
177
|
+
...processedMarker,
|
|
161
178
|
status: "waiting-docs",
|
|
162
179
|
updatedAt: timestamp
|
|
163
180
|
});
|
|
164
181
|
return true;
|
|
165
182
|
}
|
|
166
|
-
await completeTaskRetrospective(repoRoot,
|
|
183
|
+
await completeTaskRetrospective(repoRoot, processedMarker);
|
|
167
184
|
return true;
|
|
168
185
|
}
|
|
169
186
|
async function completeWaitingTaskRetrospective(repoRoot, taskSlug, memoryStatus) {
|
|
@@ -189,6 +206,22 @@ export function createHarnessFeedbackService(deps) {
|
|
|
189
206
|
return true;
|
|
190
207
|
}
|
|
191
208
|
async function completeTaskRetrospective(repoRoot, marker) {
|
|
209
|
+
const timestamp = now();
|
|
210
|
+
const processedMarker = await consumeAcceptedFeedback(repoRoot, marker);
|
|
211
|
+
if (!processedMarker) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
215
|
+
...processedMarker,
|
|
216
|
+
status: "completed",
|
|
217
|
+
completedAt: timestamp,
|
|
218
|
+
updatedAt: timestamp
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
async function consumeAcceptedFeedback(repoRoot, marker) {
|
|
222
|
+
if (marker.feedbackProcessedAt) {
|
|
223
|
+
return marker;
|
|
224
|
+
}
|
|
192
225
|
const timestamp = now();
|
|
193
226
|
try {
|
|
194
227
|
await removeProcessedFeedback(repoRoot, marker.pendingFeedbackPaths ?? []);
|
|
@@ -201,14 +234,16 @@ export function createHarnessFeedbackService(deps) {
|
|
|
201
234
|
updatedAt: timestamp,
|
|
202
235
|
error: `VCM could not remove processed Harness Feedback: ${errorMessage(error)}`
|
|
203
236
|
});
|
|
204
|
-
return;
|
|
237
|
+
return undefined;
|
|
205
238
|
}
|
|
206
|
-
|
|
239
|
+
const processedMarker = {
|
|
207
240
|
...marker,
|
|
208
|
-
|
|
209
|
-
|
|
241
|
+
pendingFeedbackPaths: [],
|
|
242
|
+
feedbackProcessedAt: timestamp,
|
|
210
243
|
updatedAt: timestamp
|
|
211
|
-
}
|
|
244
|
+
};
|
|
245
|
+
await persistTaskRetrospectiveMarker(repoRoot, processedMarker);
|
|
246
|
+
return processedMarker;
|
|
212
247
|
}
|
|
213
248
|
async function assertHarnessEngineerAvailable(_repoRoot) {
|
|
214
249
|
return undefined;
|
|
@@ -320,6 +355,7 @@ export function createHarnessFeedbackService(deps) {
|
|
|
320
355
|
"Auto Memory Review:",
|
|
321
356
|
`Role drafts: ${memoryReview.roleDraftsPath}`,
|
|
322
357
|
`Current memory snapshot: ${memoryReview.currentMemoryPath}`,
|
|
358
|
+
`Existing memory entries: ${memoryReview.existingEntriesPath}`,
|
|
323
359
|
"Active memory files:",
|
|
324
360
|
...memoryReview.activeMemoryPaths.map((memoryPath) => `- ${memoryPath}`),
|
|
325
361
|
"Proposal candidates:",
|
|
@@ -338,7 +374,9 @@ export function createHarnessFeedbackService(deps) {
|
|
|
338
374
|
"",
|
|
339
375
|
"Review every memory candidate against final task evidence while performing this retrospective.",
|
|
340
376
|
"The snapshot files contain only the matching pre-review <VCM-memory> block content.",
|
|
341
|
-
"
|
|
377
|
+
"The existing memory entries file is the complete required decision set. It groups each ## section and its body as one semantic entry; legacy memory without ## sections is one entry.",
|
|
378
|
+
"Emit exactly one source=existing decision for every listed itemId. Copy its target and entry exactly; do not create decisions for headings, wrapped lines, or other text not listed there.",
|
|
379
|
+
"Review every listed existing entry against current code, documentation, and final task evidence before evaluating proposals.",
|
|
342
380
|
"Evaluate each proposal independently. Keep only verified, durable, reusable project knowledge; do not keep task narrative, temporary state, unverified conclusions, or Harness rules in memory.",
|
|
343
381
|
"For every existing entry and proposal, record why the decision is necessary, the impact if the knowledge is absent, the evidence checked, and whether a durable document is the correct source.",
|
|
344
382
|
"When the decision is move-to-durable-doc, remove the entry from memory now and add one durableDocAssignment. Do not wait for the durable document update before removing memory.",
|
|
@@ -211,6 +211,7 @@ export function createMessageService(deps) {
|
|
|
211
211
|
const messages = await readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
|
|
212
212
|
let clearedCount = 0;
|
|
213
213
|
if (input.clearRouteFiles) {
|
|
214
|
+
await deps.workflowControlService?.cancelPendingDispatch(toWorkflowContext(input));
|
|
214
215
|
for (const routeFile of await listPendingRouteFiles(input)) {
|
|
215
216
|
await deps.fs.writeText(resolveRepoPath(input.taskRepoRoot ?? input.repoRoot, routeFile.path), "");
|
|
216
217
|
clearedCount += 1;
|
|
@@ -225,6 +226,15 @@ export function createMessageService(deps) {
|
|
|
225
226
|
},
|
|
226
227
|
async deleteMessageHistory(input) {
|
|
227
228
|
return withTaskLock(taskLocks, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), async () => {
|
|
229
|
+
const workflowState = await deps.workflowControlService?.getState(toWorkflowContext(input));
|
|
230
|
+
if (workflowState?.pendingDispatch?.status === "dispatching") {
|
|
231
|
+
throw new VcmError({
|
|
232
|
+
code: "MESSAGE_HISTORY_DISPATCHING",
|
|
233
|
+
message: "Message history cannot be deleted while a PM workflow dispatch is awaiting target confirmation.",
|
|
234
|
+
statusCode: 409,
|
|
235
|
+
hint: "Wait for confirmation or use Mark All Done to cancel the pending route first."
|
|
236
|
+
});
|
|
237
|
+
}
|
|
228
238
|
const messagesPath = getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug);
|
|
229
239
|
const messages = await readLatestMessages(deps.fs, messagesPath);
|
|
230
240
|
await writeMessageSnapshots(deps.fs, messagesPath, []);
|
|
@@ -33,6 +33,15 @@ export function createRuntimeRecoveryService(deps) {
|
|
|
33
33
|
await recoverTaskSessions(taskRepoRoot, config.stateRoot, task.taskSlug, recoveredAt, context);
|
|
34
34
|
const roundRecovered = await recoverRound(taskRepoRoot, config.stateRoot, task.taskSlug, recoveredAt, context);
|
|
35
35
|
await recoverMessages(taskRepoRoot, config.stateRoot, task.taskSlug, recoveredAt, context);
|
|
36
|
+
const workflowRecovered = await deps.workflowControlService?.recoverTask({
|
|
37
|
+
taskRepoRoot,
|
|
38
|
+
stateRoot: config.stateRoot,
|
|
39
|
+
handoffDir: task.handoffDir,
|
|
40
|
+
taskSlug: task.taskSlug
|
|
41
|
+
});
|
|
42
|
+
if (workflowRecovered) {
|
|
43
|
+
context.changedPaths.add(path.join(config.stateRoot, "workflow-control.json"));
|
|
44
|
+
}
|
|
36
45
|
await recoverGateReview(taskRepoRoot, recoveredAt, context);
|
|
37
46
|
await recoverCoderWorkers(taskRepoRoot, task.taskSlug, context);
|
|
38
47
|
await deps.architectRestartService?.recoverTask(repoRoot, task.taskSlug);
|