vibe-coding-master 0.7.49 → 0.8.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 +4 -2
- 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 +79 -28
- 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 +32 -7
- package/dist/backend/services/task-service.js +13 -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 -6
- 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 +2 -1
- package/dist/backend/templates/harness/harness-engineer-agent.js +14 -11
- 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-long-running-validation-skill.js +1 -4
- 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
package/README.md
CHANGED
|
@@ -124,8 +124,9 @@ If you want VCM app state to survive container rebuilds, set:
|
|
|
124
124
|
1. Start VCM with `vcm`.
|
|
125
125
|
2. Open the GUI.
|
|
126
126
|
3. In `Repository`, enter a local Git repository path and click `Connect`.
|
|
127
|
-
4. Create or select a task in the `Task` section.
|
|
128
|
-
|
|
127
|
+
4. Create or select a task in the `Task` section. Before creating a new task,
|
|
128
|
+
VCM fast-forward pulls the connected branch when it has an upstream, then
|
|
129
|
+
creates the task branch and worktree from the updated `HEAD`.
|
|
129
130
|
5. In `VCM Harness`, initialize or update fixed harness files if VCM reports
|
|
130
131
|
pending changes. Harness changes are written to the active task worktree.
|
|
131
132
|
6. If bootstrap is incomplete, open Harness Studio and run bootstrap through
|
|
@@ -632,6 +633,7 @@ Make sure:
|
|
|
632
633
|
|
|
633
634
|
- the repository is a Git repository
|
|
634
635
|
- the connected base repo is clean
|
|
636
|
+
- the connected branch can be fast-forward pulled when it has an upstream
|
|
635
637
|
- no other task is currently active for this project
|
|
636
638
|
- the derived `feature/<task>` branch does not already exist
|
|
637
639
|
- the derived `.claude/worktrees/<task>` directory does not already exist
|
|
@@ -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,
|
|
@@ -302,6 +302,7 @@ export function createAutoMemoryService(deps) {
|
|
|
302
302
|
});
|
|
303
303
|
}
|
|
304
304
|
await writeRunMemoryHostSnapshot(taskRepoRoot, state.runId);
|
|
305
|
+
const existingEntries = collectExistingMemoryReviewEntries(beforeMemory);
|
|
305
306
|
const timestamp = now();
|
|
306
307
|
state.reviewPromptDispatchedAt = timestamp;
|
|
307
308
|
state.retrospectiveReportPath = retrospectiveReportPath;
|
|
@@ -309,12 +310,19 @@ export function createAutoMemoryService(deps) {
|
|
|
309
310
|
state.updatedAt = timestamp;
|
|
310
311
|
await persistActiveState(taskRepoRoot, state);
|
|
311
312
|
const runRoot = resolveRepoPath(taskRepoRoot, `${MEMORY_REVIEW_RUNS_ROOT}/${state.runId}`);
|
|
313
|
+
const existingEntriesPath = path.join(runRoot, "existing-entries.json");
|
|
314
|
+
await deps.fs.writeJsonAtomic(existingEntriesPath, {
|
|
315
|
+
version: 1,
|
|
316
|
+
runId: state.runId,
|
|
317
|
+
entries: existingEntries
|
|
318
|
+
});
|
|
312
319
|
const planningCandidatePath = await findPlanningCandidateSnapshot(taskRepoRoot, state.runId);
|
|
313
320
|
return {
|
|
314
321
|
runId: state.runId,
|
|
315
322
|
roleDraftsPath: path.join(runRoot, "drafts"),
|
|
316
323
|
currentMemoryPath: path.join(runRoot, "before"),
|
|
317
324
|
activeMemoryPaths: memoryPaths.map((memoryPath) => resolveRepoPath(taskRepoRoot, memoryPath)),
|
|
325
|
+
existingEntriesPath,
|
|
318
326
|
reviewResultPath: path.join(runRoot, "review-result.json"),
|
|
319
327
|
proposalCandidates,
|
|
320
328
|
...(planningCandidatePath
|
|
@@ -966,31 +974,35 @@ export function createAutoMemoryService(deps) {
|
|
|
966
974
|
statusCode: 409
|
|
967
975
|
});
|
|
968
976
|
}
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
977
|
+
const existingEntries = collectExistingMemoryReviewEntries(before);
|
|
978
|
+
const existingDecisions = result.decisions.filter((candidate) => candidate.source === "existing");
|
|
979
|
+
for (const decision of existingDecisions) {
|
|
980
|
+
const assigned = existingEntries.find((entry) => entry.itemId === decision.itemId);
|
|
981
|
+
if (!assigned) {
|
|
972
982
|
throw new VcmError({
|
|
973
983
|
code: "MEMORY_REVIEW_EXISTING_DECISION_UNKNOWN",
|
|
974
984
|
message: `review-result.json contains an unknown existing-memory decision: ${decision.itemId}`,
|
|
975
985
|
statusCode: 409
|
|
976
986
|
});
|
|
977
987
|
}
|
|
988
|
+
if (decision.target !== assigned.target || decision.entry !== assigned.entry) {
|
|
989
|
+
throw new VcmError({
|
|
990
|
+
code: "MEMORY_REVIEW_EXISTING_DECISION_MISMATCH",
|
|
991
|
+
message: `Existing-memory decision must copy the assigned target and entry exactly: ${decision.itemId}`,
|
|
992
|
+
statusCode: 409
|
|
993
|
+
});
|
|
994
|
+
}
|
|
978
995
|
}
|
|
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);
|
|
996
|
+
for (const assigned of existingEntries) {
|
|
997
|
+
const matches = existingDecisions.filter((decision) => decision.itemId === assigned.itemId);
|
|
998
|
+
if (matches.length !== 1) {
|
|
999
|
+
throw new VcmError({
|
|
1000
|
+
code: "MEMORY_REVIEW_EXISTING_DECISION_MISSING",
|
|
1001
|
+
message: `review-result.json must contain exactly one decision for existing memory item ${assigned.itemId} in ${assigned.memoryPath}.`,
|
|
1002
|
+
statusCode: 409
|
|
1003
|
+
});
|
|
993
1004
|
}
|
|
1005
|
+
validateExistingMemoryDecision(assigned.memoryPath, assigned.entry, matches[0], after);
|
|
994
1006
|
}
|
|
995
1007
|
const moveDecisions = result.decisions.filter((decision) => decision.decision === "move-to-durable-doc");
|
|
996
1008
|
if (moveDecisions.length !== result.durableDocAssignments.length) {
|
|
@@ -1002,7 +1014,6 @@ export function createAutoMemoryService(deps) {
|
|
|
1002
1014
|
}
|
|
1003
1015
|
const assignmentKeys = new Set();
|
|
1004
1016
|
for (const assignment of result.durableDocAssignments) {
|
|
1005
|
-
validateDurableDocAssignmentInput(assignment, after);
|
|
1006
1017
|
const key = `${assignment.sourceMemoryPath}\n${assignment.sourceEntry}\n${assignment.targetPath}`;
|
|
1007
1018
|
if (assignmentKeys.has(key)) {
|
|
1008
1019
|
throw new VcmError({
|
|
@@ -1022,6 +1033,7 @@ export function createAutoMemoryService(deps) {
|
|
|
1022
1033
|
statusCode: 409
|
|
1023
1034
|
});
|
|
1024
1035
|
}
|
|
1036
|
+
validateDurableDocAssignmentInput(assignment, matchingDecision.source, after);
|
|
1025
1037
|
}
|
|
1026
1038
|
return result;
|
|
1027
1039
|
}
|
|
@@ -1034,7 +1046,7 @@ export function createAutoMemoryService(deps) {
|
|
|
1034
1046
|
statusCode: 409
|
|
1035
1047
|
});
|
|
1036
1048
|
}
|
|
1037
|
-
const
|
|
1049
|
+
const afterContent = after[memoryTargetToPath(decision.target)];
|
|
1038
1050
|
if (candidate.operation === "remove") {
|
|
1039
1051
|
if (decision.decision !== "remove" && decision.decision !== "retain") {
|
|
1040
1052
|
throw new VcmError({
|
|
@@ -1043,14 +1055,14 @@ export function createAutoMemoryService(deps) {
|
|
|
1043
1055
|
statusCode: 409
|
|
1044
1056
|
});
|
|
1045
1057
|
}
|
|
1046
|
-
if (decision.decision === "remove" &&
|
|
1058
|
+
if (decision.decision === "remove" && memoryContainsReviewContent(afterContent, expectedEntry)) {
|
|
1047
1059
|
throw new VcmError({
|
|
1048
1060
|
code: "MEMORY_REVIEW_PROPOSAL_REMOVAL_MISMATCH",
|
|
1049
1061
|
message: `Accepted removal is still present for proposal ${candidate.id}.`,
|
|
1050
1062
|
statusCode: 409
|
|
1051
1063
|
});
|
|
1052
1064
|
}
|
|
1053
|
-
if (decision.decision === "retain" && !
|
|
1065
|
+
if (decision.decision === "retain" && !memoryContainsReviewContent(afterContent, expectedEntry)) {
|
|
1054
1066
|
throw new VcmError({
|
|
1055
1067
|
code: "MEMORY_REVIEW_PROPOSAL_RETAIN_MISMATCH",
|
|
1056
1068
|
message: `Rejected removal is missing from memory for proposal ${candidate.id}.`,
|
|
@@ -1067,7 +1079,7 @@ export function createAutoMemoryService(deps) {
|
|
|
1067
1079
|
});
|
|
1068
1080
|
}
|
|
1069
1081
|
if ((decision.decision === "keep-in-memory" || decision.decision === "keep-memory-reference")
|
|
1070
|
-
&& !
|
|
1082
|
+
&& !memoryContainsReviewContent(afterContent, decision.finalContent)) {
|
|
1071
1083
|
throw new VcmError({
|
|
1072
1084
|
code: "MEMORY_REVIEW_PROPOSAL_CONTENT_MISSING",
|
|
1073
1085
|
message: `Accepted proposal content is missing from memory for ${candidate.id}.`,
|
|
@@ -1076,7 +1088,7 @@ export function createAutoMemoryService(deps) {
|
|
|
1076
1088
|
}
|
|
1077
1089
|
}
|
|
1078
1090
|
function validateExistingMemoryDecision(memoryPath, entry, decision, after) {
|
|
1079
|
-
const afterEntries = new Set(
|
|
1091
|
+
const afterEntries = new Set(parseExistingMemoryEntries(after[memoryPath]));
|
|
1080
1092
|
if (decision.decision === "retain" && !afterEntries.has(entry)) {
|
|
1081
1093
|
throw new VcmError({
|
|
1082
1094
|
code: "MEMORY_REVIEW_RETAIN_MISMATCH",
|
|
@@ -1101,7 +1113,7 @@ export function createAutoMemoryService(deps) {
|
|
|
1101
1113
|
}
|
|
1102
1114
|
}
|
|
1103
1115
|
}
|
|
1104
|
-
function validateDurableDocAssignmentInput(assignment, after) {
|
|
1116
|
+
function validateDurableDocAssignmentInput(assignment, source, after) {
|
|
1105
1117
|
if (!MEMORY_FILE_DEFINITIONS.some((definition) => definition.path === assignment.sourceMemoryPath)) {
|
|
1106
1118
|
throw new VcmError({
|
|
1107
1119
|
code: "MEMORY_REVIEW_ASSIGNMENT_SOURCE_INVALID",
|
|
@@ -1109,7 +1121,10 @@ export function createAutoMemoryService(deps) {
|
|
|
1109
1121
|
statusCode: 409
|
|
1110
1122
|
});
|
|
1111
1123
|
}
|
|
1112
|
-
|
|
1124
|
+
const sourceStillPresent = source === "existing"
|
|
1125
|
+
? parseExistingMemoryEntries(after[assignment.sourceMemoryPath]).includes(assignment.sourceEntry)
|
|
1126
|
+
: memoryContainsReviewContent(after[assignment.sourceMemoryPath], assignment.sourceEntry);
|
|
1127
|
+
if (sourceStillPresent) {
|
|
1113
1128
|
throw new VcmError({
|
|
1114
1129
|
code: "MEMORY_REVIEW_ASSIGNMENT_SOURCE_NOT_REMOVED",
|
|
1115
1130
|
message: `Move-to-durable-doc must remove memory before assignment: ${assignment.sourceEntry}`,
|
|
@@ -1451,11 +1466,47 @@ function memoryTargetToPath(target) {
|
|
|
1451
1466
|
}
|
|
1452
1467
|
return definition.path;
|
|
1453
1468
|
}
|
|
1454
|
-
function
|
|
1469
|
+
function collectExistingMemoryReviewEntries(memory) {
|
|
1470
|
+
return MEMORY_FILE_DEFINITIONS.flatMap((definition) => {
|
|
1471
|
+
const target = memoryPathToTarget(definition.path);
|
|
1472
|
+
return parseExistingMemoryEntries(memory[definition.path]).map((entry, index) => ({
|
|
1473
|
+
itemId: `existing:${target}:${index + 1}:${sha256(entry).slice(0, 12)}`,
|
|
1474
|
+
target,
|
|
1475
|
+
memoryPath: definition.path,
|
|
1476
|
+
entry
|
|
1477
|
+
}));
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
function parseExistingMemoryEntries(content) {
|
|
1481
|
+
const normalized = content.replace(/\r\n?/g, "\n").trim();
|
|
1482
|
+
if (!normalized) {
|
|
1483
|
+
return [];
|
|
1484
|
+
}
|
|
1485
|
+
const lines = normalized.split("\n");
|
|
1486
|
+
const sectionStarts = lines
|
|
1487
|
+
.map((line, index) => (/^##(?:\s+|$)/.test(line) ? index : -1))
|
|
1488
|
+
.filter((index) => index >= 0);
|
|
1489
|
+
if (sectionStarts.length === 0) {
|
|
1490
|
+
return [normalized];
|
|
1491
|
+
}
|
|
1492
|
+
return sectionStarts.map((start, index) => {
|
|
1493
|
+
const end = sectionStarts[index + 1] ?? lines.length;
|
|
1494
|
+
return lines.slice(start, end).join("\n").trim();
|
|
1495
|
+
}).filter(Boolean);
|
|
1496
|
+
}
|
|
1497
|
+
function memoryContainsReviewContent(content, expected) {
|
|
1498
|
+
const normalizedExpected = expected.replace(/\r\n?/g, "\n").trim();
|
|
1499
|
+
if (!normalizedExpected) {
|
|
1500
|
+
return false;
|
|
1501
|
+
}
|
|
1502
|
+
if (normalizedExpected.includes("\n") || /^##(?:\s+|$)/.test(normalizedExpected)) {
|
|
1503
|
+
return parseExistingMemoryEntries(content).includes(normalizedExpected);
|
|
1504
|
+
}
|
|
1455
1505
|
return content
|
|
1456
|
-
.
|
|
1506
|
+
.replace(/\r\n?/g, "\n")
|
|
1507
|
+
.split("\n")
|
|
1457
1508
|
.map((line) => line.trim())
|
|
1458
|
-
.
|
|
1509
|
+
.includes(normalizedExpected);
|
|
1459
1510
|
}
|
|
1460
1511
|
function assertDurableDocPath(targetPath) {
|
|
1461
1512
|
const normalized = normalizeProjectRelativePath(targetPath);
|
|
@@ -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,8 +33,17 @@ 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
|
-
await
|
|
46
|
+
await recoverCoderWorkers(taskRepoRoot, task.taskSlug, context);
|
|
38
47
|
await deps.architectRestartService?.recoverTask(repoRoot, task.taskSlug);
|
|
39
48
|
await deps.roleContextRestartService?.recoverTask(repoRoot, task.taskSlug);
|
|
40
49
|
if ((roundRecovered || task.status === "running") && !hasLiveTaskSession(task.taskSlug)) {
|
|
@@ -238,16 +247,32 @@ export function createRuntimeRecoveryService(deps) {
|
|
|
238
247
|
});
|
|
239
248
|
context.changedPaths.add(relativePath);
|
|
240
249
|
}
|
|
241
|
-
async function
|
|
242
|
-
|
|
243
|
-
if (!(await deps.fs.pathExists(absolutePath))) {
|
|
250
|
+
async function recoverCoderWorkers(taskRepoRoot, taskSlug, context) {
|
|
251
|
+
if (hasLiveRoundRole(taskSlug, "coder") || !deps.fs.removePath) {
|
|
244
252
|
return;
|
|
245
253
|
}
|
|
246
|
-
|
|
254
|
+
const tasksPath = path.join(taskRepoRoot, CODER_WORKERS_RUNTIME_DIR, "tasks");
|
|
255
|
+
if (!(await deps.fs.pathExists(tasksPath))) {
|
|
247
256
|
return;
|
|
248
257
|
}
|
|
249
|
-
await deps.fs.
|
|
250
|
-
|
|
258
|
+
for (const entry of await deps.fs.readDir(tasksPath)) {
|
|
259
|
+
if (!entry.endsWith(".json")) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const absolutePath = path.join(tasksPath, entry);
|
|
263
|
+
let state;
|
|
264
|
+
try {
|
|
265
|
+
state = await deps.fs.readJson(absolutePath);
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (state.status !== "running" || state.handled === true) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
await deps.fs.removePath(absolutePath, { force: true });
|
|
274
|
+
context.changedPaths.add(path.posix.join(CODER_WORKERS_RUNTIME_DIR, "tasks", entry));
|
|
275
|
+
}
|
|
251
276
|
}
|
|
252
277
|
async function recoverHarnessBootstrap(repoRoot, _timestamp, context) {
|
|
253
278
|
const absolutePath = path.join(repoRoot, BOOTSTRAP_SESSION_PATH);
|
|
@@ -53,6 +53,19 @@ export function createTaskService(deps) {
|
|
|
53
53
|
hint: `Commit, stash, or discard these changes before creating a task worktree: ${baseVisibleChanges.slice(0, 12).join(", ")}`
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
|
+
const upstreamBranch = await deps.git.getUpstreamBranch(repoRoot);
|
|
57
|
+
if (upstreamBranch) {
|
|
58
|
+
await deps.git.pullFastForward(repoRoot);
|
|
59
|
+
const postPullVisibleChanges = await getBaseRepoVisibleChanges(deps.git, repoRoot);
|
|
60
|
+
if (postPullVisibleChanges.length > 0) {
|
|
61
|
+
throw new VcmError({
|
|
62
|
+
code: "BASE_REPO_DIRTY",
|
|
63
|
+
message: "The connected repository has Git-visible changes after pulling its upstream branch.",
|
|
64
|
+
statusCode: 409,
|
|
65
|
+
hint: `Commit, stash, or discard these changes before creating a task worktree: ${postPullVisibleChanges.slice(0, 12).join(", ")}`
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
56
69
|
const timestamp = now();
|
|
57
70
|
await deps.fs.ensureDir(path.dirname(worktreePath));
|
|
58
71
|
await deps.git.createWorktree({
|