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.
Files changed (29) hide show
  1. package/dist/backend/adapters/filesystem.js +6 -0
  2. package/dist/backend/api/task-routes.js +20 -1
  3. package/dist/backend/server.js +4 -2
  4. package/dist/backend/services/auto-memory-service.js +111 -34
  5. package/dist/backend/services/claude-hook-service.js +5 -3
  6. package/dist/backend/services/gate-review-service.js +21 -2
  7. package/dist/backend/services/harness-feedback-service.js +53 -15
  8. package/dist/backend/services/message-service.js +10 -0
  9. package/dist/backend/services/runtime-recovery-service.js +9 -0
  10. package/dist/backend/services/translation-service.js +74 -3
  11. package/dist/backend/services/workflow-control-service.js +436 -75
  12. package/dist/backend/templates/handoff.js +14 -2
  13. package/dist/backend/templates/harness/architect-agent.js +6 -5
  14. package/dist/backend/templates/harness/architect-evidence-worker-agent.js +2 -0
  15. package/dist/backend/templates/harness/architect-validation-worker-agent.js +1 -1
  16. package/dist/backend/templates/harness/claude-root.js +3 -3
  17. package/dist/backend/templates/harness/coder-agent.js +1 -0
  18. package/dist/backend/templates/harness/harness-engineer-agent.js +14 -11
  19. package/dist/backend/templates/harness/memory-block.js +10 -0
  20. package/dist/backend/templates/harness/project-manager-agent.js +5 -1
  21. package/dist/backend/templates/harness/tester-agent.js +6 -0
  22. package/dist/backend/templates/harness/vcm-workflow-review-skill.js +17 -1
  23. package/dist/shared/validation/artifact-check.js +36 -3
  24. package/dist/shared/validation/artifact-contract.js +7 -0
  25. package/dist/shared/validation/artifact-registry.js +4 -1
  26. package/dist-frontend/assets/{index-DLsIPTvK.js → index-Dh7uVCmk.js} +1 -1
  27. package/dist-frontend/index.html +1 -1
  28. package/package.json +1 -1
  29. 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();
@@ -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, currentMemory) {
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 expectedHost = replaceVcmMemoryBlock(beforeHost, currentMemory[definition.path]);
109
- if (currentHost !== expectedHost) {
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, after);
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
- for (const decision of result.decisions.filter((candidate) => candidate.source === "existing")) {
970
- const memoryPath = memoryTargetToPath(decision.target);
971
- if (!substantiveMemoryEntries(before[memoryPath]).includes(decision.entry)) {
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 definition of MEMORY_FILE_DEFINITIONS) {
980
- const target = memoryPathToTarget(definition.path);
981
- for (const entry of substantiveMemoryEntries(before[definition.path])) {
982
- const matches = result.decisions.filter((decision) => decision.source === "existing"
983
- && decision.target === target
984
- && decision.entry === entry);
985
- if (matches.length !== 1) {
986
- throw new VcmError({
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 afterEntries = new Set(substantiveMemoryEntries(after[memoryTargetToPath(decision.target)]));
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" && afterEntries.has(expectedEntry)) {
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" && !afterEntries.has(expectedEntry)) {
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
- && !afterEntries.has(decision.finalContent)) {
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(substantiveMemoryEntries(after[memoryPath]));
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
- if (substantiveMemoryEntries(after[assignment.sourceMemoryPath]).includes(assignment.sourceEntry)) {
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 substantiveMemoryEntries(content) {
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
- .split(/\r?\n/)
1511
+ .replace(/\r\n?/g, "\n")
1512
+ .split("\n")
1457
1513
  .map((line) => line.trim())
1458
- .filter(Boolean);
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) => /[??]["')\]}】”’]*$/.test(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
- || /^(?:请(?:回答|选择|确认|决定|提供|告知)|你(?:是否|能否|要不要|可否)|是否需要你|需要你(?:选择|确认|决定|提供|告知))/.test(line));
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 && validationGate.status !== "skipped" && validationGate.status !== "overridden") {
1323
- if (validationGate.status !== "completed" || validationGate.decision !== "approve") {
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, _activeTaskSlug) {
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 || (marker.memoryRunId && input.memoryReviewStatus === "failed")) {
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: !reportReady
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
- if (marker.memoryRunId && input.memoryReviewStatus === "documenting") {
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
- ...marker,
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, marker);
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
- await persistTaskRetrospectiveMarker(repoRoot, {
239
+ const processedMarker = {
207
240
  ...marker,
208
- status: "completed",
209
- completedAt: timestamp,
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
- "Before evaluating proposals, review every substantive entry in every current memory snapshot against current code, documentation, and final task evidence.",
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);