vibe-coding-master 0.7.43 → 0.7.45

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 (54) hide show
  1. package/README.md +64 -39
  2. package/dist/backend/adapters/claude-adapter.js +2 -2
  3. package/dist/backend/adapters/codex-bridge-adapter.js +213 -0
  4. package/dist/backend/api/app-settings-routes.js +6 -6
  5. package/dist/backend/api/artifact-routes.js +3 -0
  6. package/dist/backend/api/harness-routes.js +16 -0
  7. package/dist/backend/api/task-routes.js +1 -0
  8. package/dist/backend/api/workflow-control-routes.js +12 -0
  9. package/dist/backend/cli/install-vcm-harness.js +21 -0
  10. package/dist/backend/server.js +11 -9
  11. package/dist/backend/services/app-settings-service.js +11 -11
  12. package/dist/backend/services/artifact-service.js +7 -30
  13. package/dist/backend/services/auto-memory-service.js +640 -12
  14. package/dist/backend/services/claude-hook-service.js +80 -2
  15. package/dist/backend/services/{ccr-integration-service.js → codex-bridge-integration-service.js} +71 -71
  16. package/dist/backend/services/gate-review-service.js +173 -74
  17. package/dist/backend/services/harness-feedback-service.js +76 -78
  18. package/dist/backend/services/harness-service.js +27 -3
  19. package/dist/backend/services/runtime-coordinator-service.js +2 -1
  20. package/dist/backend/services/session-service.js +16 -16
  21. package/dist/backend/services/status-service.js +1 -0
  22. package/dist/backend/services/translation-worker-service.js +19 -4
  23. package/dist/backend/services/usage-analytics-service.js +3 -3
  24. package/dist/backend/services/workflow-control-service.js +96 -12
  25. package/dist/backend/templates/handoff.js +44 -2
  26. package/dist/backend/templates/harness/architect-agent.js +13 -7
  27. package/dist/backend/templates/harness/architect-scaffold-worker-agent.js +1 -1
  28. package/dist/backend/templates/harness/check-scaffold-ledger.js +234 -10
  29. package/dist/backend/templates/harness/claude-root.js +3 -2
  30. package/dist/backend/templates/harness/coder-agent.js +8 -0
  31. package/dist/backend/templates/harness/gate-review.js +144 -49
  32. package/dist/backend/templates/harness/harness-engineer-agent.js +25 -10
  33. package/dist/backend/templates/harness/project-manager-agent.js +16 -10
  34. package/dist/backend/templates/harness/resolve-durable-doc-assignment.js +60 -0
  35. package/dist/backend/templates/harness/tester-agent.js +13 -0
  36. package/dist/backend/templates/harness/vcm-ask-user-skill.js +82 -0
  37. package/dist/backend/templates/harness/vcm-code-navigation-skill.js +7 -5
  38. package/dist/backend/templates/harness/vcm-task-state-skill.js +2 -2
  39. package/dist/backend/templates/harness/vcm-workflow-review-skill.js +1 -1
  40. package/dist/shared/types/session.js +28 -22
  41. package/dist/shared/types/workflow.js +1 -0
  42. package/dist/shared/validation/artifact-check.js +3 -3
  43. package/dist/shared/validation/artifact-contract.js +1 -1
  44. package/dist/shared/validation/artifact-registry.js +16 -0
  45. package/dist-frontend/assets/index-DDmygnV6.css +32 -0
  46. package/dist-frontend/assets/{index-VW9tYPP5.js → index-nAV6toi8.js} +47 -47
  47. package/dist-frontend/index.html +2 -2
  48. package/package.json +1 -1
  49. package/scripts/claude-plugins/vcm-lsp-bridge/.claude-plugin/plugin.json +21 -6
  50. package/scripts/{ccr-api-key-helper.mjs → codex-bridge-api-key-helper.mjs} +1 -1
  51. package/scripts/harness-tools/vcm-artifact +1 -2
  52. package/scripts/harness-tools/vcm-bash-guard +1 -1
  53. package/dist/backend/adapters/ccr-gateway-adapter.js +0 -172
  54. package/dist-frontend/assets/index-B0d4Z6ny.css +0 -32
@@ -10,6 +10,7 @@ const REVIEWER_AGENT_PATH = ".claude/agents/reviewer.md";
10
10
  const GATE_REVIEW_DIR = ".ai/vcm/gate-reviews";
11
11
  const REQUESTS_DIR = ".ai/vcm/gate-reviews/requests";
12
12
  const GATE_REVIEW_VERSION = 1;
13
+ const HARNESS_COMMIT_PREFIX = "[VCM Harness] ";
13
14
  const REVIEWER_ROLE = "reviewer";
14
15
  const DEFAULT_REPORT_POLL_INTERVAL_MS = 1000;
15
16
  const activeRuns = new Map();
@@ -107,6 +108,7 @@ export function createGateReviewService(deps) {
107
108
  taskSlug,
108
109
  taskRepoRoot,
109
110
  stateRoot: projectConfig.stateRoot,
111
+ handoffDir: task.handoffDir,
110
112
  config: loadRuntimeConfig(reviewSettings)
111
113
  };
112
114
  }
@@ -345,42 +347,21 @@ export function createGateReviewService(deps) {
345
347
  }
346
348
  }
347
349
  if (gate === "code-diff") {
348
- const prerequisiteError = await readCodeDiffPrerequisiteError(deps, context, index, codeDiffSource);
349
- if (prerequisiteError) {
350
- index = applyGateState(index, gate, {
351
- status: "failed",
352
- decision: undefined,
353
- error: prerequisiteError,
354
- exceptionReason: undefined,
355
- requestId: undefined,
356
- requestPath: undefined,
357
- inputHash: undefined,
358
- baseCommit: undefined,
359
- headCommit: undefined,
360
- commits: undefined,
361
- changedFiles: undefined,
362
- diffStat: undefined,
363
- codeDiffSource,
364
- codeDiffSources: codeDiffSource ? [codeDiffSource] : undefined,
365
- requestedAt: undefined,
366
- startedAt: undefined,
367
- completedAt: now(),
368
- callbackStatus: "not_sent",
369
- callbackError: undefined
370
- }, now(), true);
371
- await saveIndex(deps.fs, context.taskRepoRoot, index);
350
+ const evidenceError = await readCodeDiffEvidenceError(deps.fs, context.taskRepoRoot, codeDiffSource);
351
+ if (evidenceError) {
352
+ index = await recordCodeDiffStartFailure(index, context, gate, codeDiffSource, evidenceError);
372
353
  return {
373
354
  status: "failed_to_start",
374
355
  gate,
375
356
  record: index.gates[gate],
376
- message: prerequisiteError
357
+ message: evidenceError
377
358
  };
378
359
  }
379
360
  }
380
361
  const codeDiffInput = gate === "code-diff"
381
362
  ? await resolveCodeDiffInput(deps, context, record)
382
363
  : undefined;
383
- if (gate === "code-diff" && !codeDiffInput) {
364
+ if (gate === "code-diff" && (!codeDiffInput || codeDiffInput.commits.length === 0)) {
384
365
  index = applyGateState(index, gate, {
385
366
  status: "not_required",
386
367
  decision: undefined,
@@ -389,16 +370,16 @@ export function createGateReviewService(deps) {
389
370
  requestId: undefined,
390
371
  requestPath: undefined,
391
372
  inputHash: undefined,
392
- baseCommit: undefined,
393
- headCommit: undefined,
394
- commits: undefined,
395
- changedFiles: undefined,
396
- diffStat: undefined,
373
+ baseCommit: codeDiffInput?.baseCommit,
374
+ headCommit: codeDiffInput?.headCommit,
375
+ commits: codeDiffInput?.commits,
376
+ changedFiles: codeDiffInput?.changedFiles,
377
+ diffStat: codeDiffInput?.diffStat,
397
378
  codeDiffSource,
398
379
  codeDiffSources: codeDiffSource ? [codeDiffSource] : undefined,
399
380
  requestedAt: undefined,
400
381
  startedAt: undefined,
401
- completedAt: undefined,
382
+ completedAt: codeDiffInput ? now() : undefined,
402
383
  callbackStatus: "not_sent",
403
384
  callbackError: undefined
404
385
  }, now(), true);
@@ -407,11 +388,51 @@ export function createGateReviewService(deps) {
407
388
  status: "not_required",
408
389
  gate,
409
390
  record: index.gates[gate],
410
- message: "No new commits to review."
391
+ message: codeDiffInput ? "No non-Harness commits to review." : "No new commits to review."
411
392
  };
412
393
  }
413
- const codeDiffSources = gate === "code-diff" && codeDiffInput && codeDiffSource
414
- ? resolveCodeDiffSources(record, codeDiffInput, codeDiffSource)
394
+ if (gate === "code-diff") {
395
+ const prerequisiteError = await readCodeDiffValidationGateError(deps, context, index);
396
+ if (prerequisiteError) {
397
+ index = await recordCodeDiffStartFailure(index, context, gate, codeDiffSource, prerequisiteError, codeDiffInput);
398
+ return {
399
+ status: "failed_to_start",
400
+ gate,
401
+ record: index.gates[gate],
402
+ message: prerequisiteError
403
+ };
404
+ }
405
+ }
406
+ async function recordCodeDiffStartFailure(currentIndex, currentContext, currentGate, currentSource, message, currentCodeDiffInput) {
407
+ const failedIndex = applyGateState(currentIndex, currentGate, {
408
+ status: "failed",
409
+ decision: undefined,
410
+ error: message,
411
+ exceptionReason: undefined,
412
+ requestId: undefined,
413
+ requestPath: undefined,
414
+ inputHash: undefined,
415
+ baseCommit: currentCodeDiffInput?.baseCommit,
416
+ headCommit: currentCodeDiffInput?.headCommit,
417
+ commits: currentCodeDiffInput?.commits,
418
+ changedFiles: currentCodeDiffInput?.changedFiles,
419
+ diffStat: currentCodeDiffInput?.diffStat,
420
+ codeDiffSource: currentSource,
421
+ codeDiffSources: currentSource ? [currentSource] : undefined,
422
+ requestedAt: undefined,
423
+ startedAt: undefined,
424
+ completedAt: now(),
425
+ callbackStatus: "not_sent",
426
+ callbackError: undefined
427
+ }, now(), true);
428
+ await saveIndex(deps.fs, currentContext.taskRepoRoot, failedIndex);
429
+ return failedIndex;
430
+ }
431
+ const currentCodeDiffSources = gate === "code-diff" && codeDiffSource
432
+ ? await resolveWorkflowCodeDiffSources(deps, context, codeDiffSource)
433
+ : undefined;
434
+ const codeDiffSources = gate === "code-diff" && codeDiffInput && currentCodeDiffSources
435
+ ? resolveCodeDiffSources(record, codeDiffInput, currentCodeDiffSources)
415
436
  : undefined;
416
437
  const inputHash = await computeInputHash(deps, context.taskRepoRoot, gate, codeDiffInput, codeDiffSources);
417
438
  if (!options.force
@@ -1059,44 +1080,78 @@ async function resolveCodeDiffInput(deps, context, record) {
1059
1080
  return undefined;
1060
1081
  }
1061
1082
  const range = `${baseCommit}..${headCommit}`;
1062
- const commits = splitLines(await commandStdout(deps.runner, context.taskRepoRoot, [
1083
+ const allCommits = splitLines(await commandStdout(deps.runner, context.taskRepoRoot, [
1063
1084
  "log",
1064
1085
  "--oneline",
1065
1086
  "--reverse",
1066
1087
  range
1067
1088
  ]));
1068
- if (commits.length === 0) {
1089
+ if (allCommits.length === 0) {
1069
1090
  return undefined;
1070
1091
  }
1071
- const changedFiles = splitLines(await commandStdout(deps.runner, context.taskRepoRoot, [
1072
- "diff",
1073
- "--name-only",
1074
- "--find-renames",
1075
- range
1076
- ]));
1077
- const diffStat = await commandStdout(deps.runner, context.taskRepoRoot, [
1078
- "diff",
1079
- "--stat",
1080
- "--find-renames",
1081
- range
1082
- ]);
1083
- const diff = await commandStdout(deps.runner, context.taskRepoRoot, [
1084
- "diff",
1085
- "--binary",
1086
- "--find-renames",
1087
- range
1088
- ]);
1092
+ const commits = allCommits.filter((line) => !isHarnessCommitLine(line));
1093
+ const commitShas = commits.map(commitShaFromOneline);
1094
+ const changedFiles = new Set();
1095
+ const diffStats = [];
1096
+ const diffs = [];
1097
+ for (const commitSha of commitShas) {
1098
+ for (const changedFile of splitLines(await commandStdout(deps.runner, context.taskRepoRoot, [
1099
+ "show",
1100
+ "--format=",
1101
+ "--name-only",
1102
+ "--find-renames",
1103
+ commitSha
1104
+ ]))) {
1105
+ changedFiles.add(changedFile);
1106
+ }
1107
+ const stat = await commandStdout(deps.runner, context.taskRepoRoot, [
1108
+ "show",
1109
+ "--format=",
1110
+ "--stat",
1111
+ "--find-renames",
1112
+ commitSha
1113
+ ]);
1114
+ if (stat.trim()) {
1115
+ diffStats.push(stat.trim());
1116
+ }
1117
+ const patch = await commandStdout(deps.runner, context.taskRepoRoot, [
1118
+ "show",
1119
+ "--format=",
1120
+ "--binary",
1121
+ "--find-renames",
1122
+ commitSha
1123
+ ]);
1124
+ if (patch) {
1125
+ diffs.push(patch);
1126
+ }
1127
+ }
1128
+ const diffStat = diffStats.join("\n");
1129
+ const diff = diffs.join("\n");
1089
1130
  const diffHash = createHash("sha256").update(diff).digest("hex");
1090
1131
  return {
1091
1132
  baseCommit,
1092
1133
  headCommit,
1093
1134
  commits,
1094
- changedFiles,
1135
+ commitShas,
1136
+ changedFiles: [...changedFiles],
1095
1137
  diffStat,
1096
1138
  diffHash
1097
1139
  };
1098
1140
  }
1141
+ function commitShaFromOneline(line) {
1142
+ return line.split(/\s+/, 1)[0] ?? line;
1143
+ }
1144
+ function isHarnessCommitLine(line) {
1145
+ const separator = line.indexOf(" ");
1146
+ return separator >= 0 && line.slice(separator + 1).startsWith(HARNESS_COMMIT_PREFIX);
1147
+ }
1099
1148
  async function resolveCodeDiffBaseCommit(deps, context, record, headCommit) {
1149
+ if (record.gate === "code-diff"
1150
+ && record.status === "failed"
1151
+ && record.baseCommit
1152
+ && await isAncestor(deps.runner, context.taskRepoRoot, record.baseCommit, headCommit)) {
1153
+ return record.baseCommit;
1154
+ }
1100
1155
  if (record.gate === "code-diff"
1101
1156
  && record.status === "completed"
1102
1157
  && record.decision === "request_changes"
@@ -1111,6 +1166,12 @@ async function resolveCodeDiffBaseCommit(deps, context, record, headCommit) {
1111
1166
  && await isAncestor(deps.runner, context.taskRepoRoot, record.headCommit, headCommit)) {
1112
1167
  return record.headCommit;
1113
1168
  }
1169
+ if (record.gate === "code-diff"
1170
+ && record.status === "not_required"
1171
+ && record.headCommit
1172
+ && await isAncestor(deps.runner, context.taskRepoRoot, record.headCommit, headCommit)) {
1173
+ return record.headCommit;
1174
+ }
1114
1175
  const rootHead = (await commandStdout(deps.runner, context.repoRoot, ["rev-parse", "HEAD"])).trim();
1115
1176
  if (rootHead
1116
1177
  && rootHead !== headCommit
@@ -1165,7 +1226,7 @@ async function computeInputHash(deps, taskRepoRoot, gate, codeDiffInput, codeDif
1165
1226
  digest.update(coreArtifact);
1166
1227
  digest.update(await deps.fs.readText(resolveRepoPath(taskRepoRoot, coreArtifact)));
1167
1228
  }
1168
- const common = [
1229
+ const common = gate === "code-diff" ? [] : [
1169
1230
  "CLAUDE.md",
1170
1231
  ".claude/agents/architect.md",
1171
1232
  ".claude/agents/coder.md",
@@ -1189,10 +1250,6 @@ async function computeInputHash(deps, taskRepoRoot, gate, codeDiffInput, codeDif
1189
1250
  if (gate === "code-diff" && codeDiffInput) {
1190
1251
  digest.update("codeDiffSources");
1191
1252
  digest.update(codeDiffSources?.join("\n") ?? "<missing>");
1192
- digest.update("baseCommit");
1193
- digest.update(codeDiffInput.baseCommit);
1194
- digest.update("headCommit");
1195
- digest.update(codeDiffInput.headCommit);
1196
1253
  digest.update("commits");
1197
1254
  digest.update(codeDiffInput.commits.join("\n"));
1198
1255
  digest.update("changedFiles");
@@ -1295,11 +1352,14 @@ async function readValidationReportError(fs, taskRepoRoot) {
1295
1352
  }
1296
1353
  return undefined;
1297
1354
  }
1298
- async function readCodeDiffPrerequisiteError(deps, context, index, source) {
1299
- const reportError = await readValidationReportError(deps.fs, context.taskRepoRoot);
1355
+ async function readCodeDiffEvidenceError(fs, taskRepoRoot, source) {
1356
+ const reportError = await readValidationReportError(fs, taskRepoRoot);
1300
1357
  if (reportError) {
1301
1358
  return "code-diff requires completed Tester validation. " + reportError;
1302
1359
  }
1360
+ return readCodeDiffSourceArtifactError(fs, taskRepoRoot, source);
1361
+ }
1362
+ async function readCodeDiffValidationGateError(deps, context, index) {
1303
1363
  const validationGate = index.gates["validation-adequacy"];
1304
1364
  if (validationGate.required && validationGate.status !== "skipped" && validationGate.status !== "overridden") {
1305
1365
  if (validationGate.status !== "completed" || validationGate.decision !== "approve") {
@@ -1310,7 +1370,7 @@ async function readCodeDiffPrerequisiteError(deps, context, index, source) {
1310
1370
  return "code-diff requires a current validation-adequacy approval; code or test evidence changed after the recorded approval.";
1311
1371
  }
1312
1372
  }
1313
- return readCodeDiffSourceArtifactError(deps.fs, context.taskRepoRoot, source);
1373
+ return undefined;
1314
1374
  }
1315
1375
  async function readArchitectureEvidenceError(fs, taskRepoRoot) {
1316
1376
  const relativePath = ".ai/vcm/handoffs/architecture-evidence.md";
@@ -1399,17 +1459,16 @@ Use each captured snapshot as the immutable handoff or prior-Gate input for this
1399
1459
  Code Diff Input:
1400
1460
  This code-diff gate reviews the new commits from one PM route flow, not the whole task and not one terminal turn.
1401
1461
  Code sources: ${codeDiffSources?.join(" -> ") ?? "<missing>"}
1402
- Base commit: ${codeDiffInput.baseCommit}
1403
- Head commit: ${codeDiffInput.headCommit}
1404
- Commits:
1462
+ Reviewable commits:
1405
1463
  ${codeDiffInput.commits.map((line) => `- ${line}`).join("\n")}
1406
1464
  Changed files:
1407
1465
  ${codeDiffInput.changedFiles.length > 0 ? codeDiffInput.changedFiles.map((line) => `- ${line}`).join("\n") : "- <none>"}
1408
1466
  Diff commands:
1409
- - git diff --stat --find-renames ${codeDiffInput.baseCommit}..${codeDiffInput.headCommit}
1410
- - git diff --find-renames ${codeDiffInput.baseCommit}..${codeDiffInput.headCommit}
1411
- - git show --stat --oneline <commit>
1412
- Review only this commit range.`
1467
+ ${codeDiffInput.commitShas.flatMap((commitSha) => [
1468
+ `- git show --stat --oneline ${commitSha}`,
1469
+ `- git show --find-renames ${commitSha}`
1470
+ ]).join("\n")}
1471
+ Review only these commits.`
1413
1472
  : "";
1414
1473
  return `[VCM GATE REVIEW]
1415
1474
  Task: ${context.taskSlug}
@@ -1763,18 +1822,58 @@ function getSourceArtifacts(gate, codeDiffSources) {
1763
1822
  ...(codeDiffSources ?? []).flatMap((source) => CODE_DIFF_SOURCE_ARTIFACTS[source])
1764
1823
  ])];
1765
1824
  }
1766
- function resolveCodeDiffSources(record, codeDiffInput, currentSource) {
1825
+ function resolveCodeDiffSources(record, codeDiffInput, currentSources) {
1767
1826
  const continuingRecordedRange = record.baseCommit === codeDiffInput.baseCommit
1768
1827
  && ((record.status === "completed" && record.decision === "request_changes")
1769
1828
  || record.status === "failed");
1770
1829
  if (!continuingRecordedRange) {
1771
- return [currentSource];
1830
+ return currentSources;
1772
1831
  }
1773
1832
  return [...new Set([
1774
1833
  ...(normalizeCodeDiffSources(record.codeDiffSources, record.codeDiffSource) ?? []),
1775
- currentSource
1834
+ ...currentSources
1776
1835
  ])];
1777
1836
  }
1837
+ async function resolveWorkflowCodeDiffSources(deps, context, currentSource) {
1838
+ if (!deps.workflowControlService) {
1839
+ return [currentSource];
1840
+ }
1841
+ try {
1842
+ const workflowContext = {
1843
+ taskRepoRoot: context.taskRepoRoot,
1844
+ stateRoot: context.stateRoot,
1845
+ handoffDir: context.handoffDir,
1846
+ taskSlug: context.taskSlug
1847
+ };
1848
+ const [state, progress] = await Promise.all([
1849
+ deps.workflowControlService.getState(workflowContext),
1850
+ deps.workflowControlService.getProgress(workflowContext)
1851
+ ]);
1852
+ const startedAtSequence = state.flowRun?.startedAtSequence;
1853
+ if (state.warnings.length > 0 || startedAtSequence === undefined) {
1854
+ return [currentSource];
1855
+ }
1856
+ const sources = [];
1857
+ for (const entry of progress.history) {
1858
+ if (entry.sequence < startedAtSequence)
1859
+ continue;
1860
+ if (entry.flow === "code-change" && entry.targetRole === "coder") {
1861
+ sources.push("coder");
1862
+ }
1863
+ else if (entry.flow === "architect-debug" && entry.targetRole === "architect") {
1864
+ sources.push("architect-debug");
1865
+ }
1866
+ else if (entry.flow === "architecture-diagnosis" && entry.targetRole === "architect") {
1867
+ sources.push("architect-diagnosis");
1868
+ }
1869
+ }
1870
+ sources.push(currentSource);
1871
+ return [...new Set(sources)];
1872
+ }
1873
+ catch {
1874
+ return [currentSource];
1875
+ }
1876
+ }
1778
1877
  function normalizeCodeDiffSources(sources, source) {
1779
1878
  const normalized = Array.isArray(sources) ? sources.filter(isCodeDiffSource) : [];
1780
1879
  if (normalized.length === 0 && isCodeDiffSource(source)) {
@@ -4,7 +4,6 @@ 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 { getRetrospectiveReportErrors } from "./artifact-service.js";
8
7
  const FEEDBACK_ROOT = ".ai/vcm/harness-feedback";
9
8
  const PENDING_DIR = `${FEEDBACK_ROOT}/pending`;
10
9
  const TASK_RETROSPECTIVE_DIR = `${FEEDBACK_ROOT}/task-retrospectives`;
@@ -143,7 +142,7 @@ export function createHarnessFeedbackService(deps) {
143
142
  ]
144
143
  : ["Report is empty."];
145
144
  const reportReady = reportErrors.length === 0;
146
- if (!reportReady || (marker.memoryRunId && !input.memoryReviewSucceeded)) {
145
+ if (!reportReady || (marker.memoryRunId && input.memoryReviewStatus === "failed")) {
147
146
  await persistTaskRetrospectiveMarker(repoRoot, {
148
147
  ...marker,
149
148
  status: "failed",
@@ -155,6 +154,41 @@ export function createHarnessFeedbackService(deps) {
155
154
  });
156
155
  return true;
157
156
  }
157
+ if (marker.memoryRunId && input.memoryReviewStatus === "documenting") {
158
+ await persistTaskRetrospectiveMarker(repoRoot, {
159
+ ...marker,
160
+ status: "waiting-docs",
161
+ updatedAt: timestamp
162
+ });
163
+ return true;
164
+ }
165
+ await completeTaskRetrospective(repoRoot, marker);
166
+ return true;
167
+ }
168
+ async function completeWaitingTaskRetrospective(repoRoot, taskSlug, memoryStatus) {
169
+ const marker = await loadTaskRetrospectiveMarker(repoRoot, taskSlug);
170
+ if (!marker || marker.status !== "waiting-docs") {
171
+ return false;
172
+ }
173
+ if (memoryStatus === "documenting") {
174
+ return true;
175
+ }
176
+ if (memoryStatus === "failed") {
177
+ const timestamp = now();
178
+ await persistTaskRetrospectiveMarker(repoRoot, {
179
+ ...marker,
180
+ status: "failed",
181
+ failedAt: timestamp,
182
+ updatedAt: timestamp,
183
+ error: "Task Harness Retrospective durable-document assignment failed."
184
+ });
185
+ return true;
186
+ }
187
+ await completeTaskRetrospective(repoRoot, marker);
188
+ return true;
189
+ }
190
+ async function completeTaskRetrospective(repoRoot, marker) {
191
+ const timestamp = now();
158
192
  try {
159
193
  await removeProcessedFeedback(repoRoot, marker.pendingFeedbackPaths ?? []);
160
194
  }
@@ -166,7 +200,7 @@ export function createHarnessFeedbackService(deps) {
166
200
  updatedAt: timestamp,
167
201
  error: `VCM could not remove processed Harness Feedback: ${errorMessage(error)}`
168
202
  });
169
- return true;
203
+ return;
170
204
  }
171
205
  await persistTaskRetrospectiveMarker(repoRoot, {
172
206
  ...marker,
@@ -174,7 +208,6 @@ export function createHarnessFeedbackService(deps) {
174
208
  completedAt: timestamp,
175
209
  updatedAt: timestamp
176
210
  });
177
- return true;
178
211
  }
179
212
  async function assertHarnessEngineerAvailable(_repoRoot) {
180
213
  return undefined;
@@ -285,6 +318,16 @@ export function createHarnessFeedbackService(deps) {
285
318
  `Current memory snapshot: ${memoryReview.currentMemoryPath}`,
286
319
  "Active memory files:",
287
320
  ...memoryReview.activeMemoryPaths.map((memoryPath) => `- ${memoryPath}`),
321
+ "Proposal candidates:",
322
+ ...(memoryReview.proposalCandidates.length > 0
323
+ ? memoryReview.proposalCandidates.map((candidate) => [
324
+ candidate.id,
325
+ `source=${candidate.source}`,
326
+ `operation=${candidate.operation}`,
327
+ `target=${candidate.target}`,
328
+ `entry=${candidate.content ?? candidate.existing ?? "none"}`
329
+ ].join(" | "))
330
+ : ["none"]),
288
331
  ...(memoryReview.planningCandidatePath
289
332
  ? [`Architect planning-session candidate: ${memoryReview.planningCandidatePath}`]
290
333
  : []),
@@ -292,88 +335,30 @@ export function createHarnessFeedbackService(deps) {
292
335
  "Review every memory candidate against final task evidence while performing this retrospective.",
293
336
  "The snapshot files contain only the matching pre-review <VCM-memory> block content.",
294
337
  "Before evaluating proposals, review every substantive entry in every current memory snapshot against current code, documentation, and final task evidence.",
295
- "For each existing entry, decide retain, update, remove, or move-to-durable-doc. Record the decision reason, the impact of removing it, and whether memory or a durable document is the correct source.",
296
- "Complete this full existing-memory review even when every proposal says no-change.",
297
- "Then evaluate every proposal item independently. Do not accept or reject a whole role draft as one decision.",
298
- "For every Add or Update candidate, independently explain why the memory is necessary, what fails if it is absent, and why memory or a durable document is the correct destination.",
299
- "Do not copy the proposer rationale as the review. Verify it against current code, durable documentation, and final task evidence.",
300
- "Keep only verified, durable, reusable project knowledge. Merge duplicates and keep role-specific knowledge in the matching role file.",
301
- "Do not record task narrative, temporary state, unverified conclusions, or Harness rules in memory.",
302
- "Final content must be one exact line written to the selected active <VCM-memory> block. Use none when the decision does not keep memory.",
303
- "For keep-in-memory use Durable doc disposition: memory. For keep-memory-reference use memory-reference. For move-to-durable-doc use durable-doc.",
304
- "Existing-memory Target must be shared or the exact role name. Decision must be retain, update, remove, or move-to-durable-doc.",
305
- "Durable doc disposition must be memory, durable-doc, or memory-reference. Use Durable doc path: none with memory and an actual path with the other dispositions.",
306
- "Do not keep full content in memory when durable-doc is correct. Use memory-reference only when an ongoing role needs the document pointer.",
307
- "Use none as the complete Existing Memory Decisions body only when no substantive existing memory entry exists.",
308
- "Apply the reviewed result directly to the <VCM-memory> blocks in the listed active memory files. Do not change any content outside those blocks.",
309
- "If memory changes, commit only the changed active memory files before ending the turn. Use commit message: chore: update VCM memory. If memory is unchanged, do not create a commit.",
310
- "Use this exact block in the retrospective report and replace each option or placeholder with one allowed value or a concise summary:",
338
+ "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.",
339
+ "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.",
340
+ "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.",
341
+ "Apply the reviewed result directly to the listed <VCM-memory> blocks. Do not change content outside those blocks.",
342
+ "If memory changes, commit only the changed active memory files with message [VCM Harness] Update VCM memory. If memory is unchanged, do not create a commit.",
343
+ `Write the complete machine-readable review to: ${memoryReview.reviewResultPath}`,
344
+ "The JSON root must be: {\"version\":1,\"runId\":\"<assigned run id>\",\"memoryCommit\":\"<full commit or none>\",\"decisions\":[],\"durableDocAssignments\":[]}.",
345
+ "Each decision must contain itemId, source (existing|proposal), target, entry, decision, reason, impactIfAbsent, evidence (non-empty array), finalContent, and durableDocPath.",
346
+ "Existing decisions use retain|update|remove|move-to-durable-doc. Add or Update proposal decisions use keep-in-memory|keep-memory-reference|move-to-durable-doc|reject; Remove proposal decisions use remove|retain. Proposal itemId must equal the assigned candidate ID.",
347
+ "Each durableDocAssignment must contain sourceMemoryPath, sourceEntry, targetPath, content, reason, and evidence (non-empty array). Use a project-relative Markdown target outside .ai/vcm.",
348
+ "In the retrospective report, include only this memory summary:",
311
349
  "",
312
350
  "## Memory Review",
313
- "Existing memory reviewed: complete",
314
- "",
315
- "### Proposal Decisions",
316
- ...(memoryReview.proposalCandidates.length > 0
317
- ? memoryReview.proposalCandidates.flatMap(renderMemoryProposalDecisionTemplate)
318
- : ["none"]),
319
- "",
320
- "### Existing Memory Decisions",
321
- "#### Item 1",
322
- "Target: shared",
323
- "Existing: <exact existing memory entry>",
324
- "Decision: retain",
325
- "Reason: <why this decision is correct>",
326
- "Impact if removed: <specific future role or task failure>",
327
- "Durable doc disposition: memory",
328
- "Durable doc path: none",
329
- "Evidence: <current code, durable documentation, or final task evidence>",
330
- "",
331
- "### Existing Memory Changes",
332
- "- retained: <summary or none>",
333
- "- updated: <summary or none>",
334
- "- removed: <summary or none>",
335
- "",
336
- "Reviewed memory set: complete"
351
+ "Memory commit: <full commit or none>",
352
+ `Review result: ${memoryReview.reviewResultPath}`,
353
+ "Durable document assignments: <count>"
337
354
  ]
338
355
  : []),
339
356
  "",
340
357
  `Write the analysis to Result Path: ${resolveRepoPath(repoRoot, analysisPath)}`,
341
- `Submit with: .ai/tools/vcm-artifact retrospective-report --file <candidate> --path ${analysisPath} --mode final`,
342
- "End your turn after VCM accepts the result."
358
+ "Write the report directly to that path. Do not use vcm-artifact.",
359
+ "End your turn after the report is complete."
343
360
  ].join("\n");
344
361
  }
345
- function renderMemoryProposalDecisionTemplate(candidate) {
346
- if (candidate.operation === "remove") {
347
- return [
348
- `#### Candidate ${candidate.id}`,
349
- `Source: ${candidate.source}`,
350
- "Operation: remove",
351
- `Target: ${candidate.target}`,
352
- `Existing: ${candidate.existing}`,
353
- "Decision: remove|retain",
354
- "Reason: <why the proposed removal should be applied or rejected>",
355
- "Evidence checked: <current code, durable documentation, or final task evidence>",
356
- ""
357
- ];
358
- }
359
- return [
360
- `#### Candidate ${candidate.id}`,
361
- `Source: ${candidate.source}`,
362
- `Operation: ${candidate.operation}`,
363
- `Target: ${candidate.target}`,
364
- `Candidate: ${candidate.content}`,
365
- "Decision: keep-in-memory|keep-memory-reference|move-to-durable-doc|reject",
366
- "Final target: shared|project-manager|architect|coder|tester|reviewer|harness-engineer|none",
367
- "Why memory is necessary: <independent reason, or why it is not necessary>",
368
- "Impact if absent: <specific impact, or why no durable impact exists>",
369
- "Durable doc disposition: memory|durable-doc|memory-reference",
370
- "Durable doc analysis: <why this destination is correct>",
371
- "Durable doc path: <none or a project-relative durable doc path>",
372
- "Evidence checked: <current code, durable documentation, or final task evidence>",
373
- "Final content: <exact one-line reviewed-memory content or none>",
374
- ""
375
- ];
376
- }
377
362
  function buildPendingFeedbackPrompt(repoRoot, feedbackPath) {
378
363
  return [
379
364
  "[VCM Harness Feedback]",
@@ -431,6 +416,7 @@ export function createHarnessFeedbackService(deps) {
431
416
  sendPendingFeedback,
432
417
  startTaskRetrospective,
433
418
  handleTaskRetrospectiveHook,
419
+ completeWaitingTaskRetrospective,
434
420
  assertHarnessEngineerAvailable
435
421
  };
436
422
  }
@@ -495,6 +481,18 @@ function assertPendingFeedbackPath(feedbackPath) {
495
481
  throw new Error(`Invalid assigned Harness Feedback path: ${feedbackPath}.`);
496
482
  }
497
483
  }
484
+ function getRetrospectiveReportErrors(content) {
485
+ const errors = [];
486
+ if (!/^# Task Harness Retrospective(?::\s*.+)?\s*$/m.test(content)) {
487
+ errors.push("Retrospective report requires '# Task Harness Retrospective: <task>'.");
488
+ }
489
+ for (const heading of ["Findings", "Feedback Dispositions", "Recommended Harness Changes", "VCM Issue Drafts"]) {
490
+ if (!new RegExp(`^## ${heading}\\s*$`, "m").test(content)) {
491
+ errors.push(`Missing required section: ${heading}.`);
492
+ }
493
+ }
494
+ return errors;
495
+ }
498
496
  function parseSimpleMetadata(content) {
499
497
  const result = {};
500
498
  for (const line of content.split(/\r?\n/).slice(0, 80)) {