vibe-coding-master 0.9.2 → 0.9.4

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 (40) hide show
  1. package/README.md +3 -2
  2. package/dist/backend/adapters/command-runner.js +1 -0
  3. package/dist/backend/adapters/filesystem.js +5 -2
  4. package/dist/backend/api/artifact-routes.js +0 -3
  5. package/dist/backend/api/task-routes.js +0 -1
  6. package/dist/backend/cli/install-vcm-harness.js +6 -0
  7. package/dist/backend/server.js +2 -0
  8. package/dist/backend/services/artifact-service.js +1 -16
  9. package/dist/backend/services/auto-memory-service.js +101 -16
  10. package/dist/backend/services/claude-hook-service.js +31 -12
  11. package/dist/backend/services/docs-sync-check.js +32 -0
  12. package/dist/backend/services/gate-review-service.js +10 -2
  13. package/dist/backend/services/harness-feedback-service.js +21 -3
  14. package/dist/backend/services/lsp-plugin.js +6 -2
  15. package/dist/backend/services/managed-artifact-validation.js +2 -1
  16. package/dist/backend/services/runtime-coordinator-service.js +1 -1
  17. package/dist/backend/services/session-service.js +1 -1
  18. package/dist/backend/services/status-service.js +0 -1
  19. package/dist/backend/services/translation-worker-service.js +44 -6
  20. package/dist/backend/services/workflow-control-service.js +103 -165
  21. package/dist/backend/templates/handoff.js +2 -48
  22. package/dist/backend/templates/harness/architect-agent.js +42 -23
  23. package/dist/backend/templates/harness/claude-root.js +9 -9
  24. package/dist/backend/templates/harness/coder-agent.js +0 -1
  25. package/dist/backend/templates/harness/gate-review.js +55 -12
  26. package/dist/backend/templates/harness/project-manager-agent.js +33 -27
  27. package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +13 -14
  28. package/dist/backend/templates/harness/vcm-long-running-validation-skill.js +2 -2
  29. package/dist/backend/templates/harness/vcm-workflow-review-skill.js +2 -2
  30. package/dist/shared/types/gate-review.js +4 -0
  31. package/dist/shared/types/workflow.js +0 -1
  32. package/dist/shared/validation/artifact-check.js +1 -27
  33. package/dist/shared/validation/artifact-contract.js +0 -2
  34. package/dist/shared/validation/artifact-registry.js +1 -18
  35. package/package.json +1 -1
  36. package/scripts/claude-plugins/vcm-lsp-bridge/vcm-lsp-proxy.mjs +5 -1
  37. package/scripts/harness-tools/check-docs-sync +246 -0
  38. package/scripts/harness-tools/check-durable-docs +24 -0
  39. package/scripts/harness-tools/run-long-check +3 -3
  40. package/scripts/verify-package.mjs +1 -0
package/README.md CHANGED
@@ -211,10 +211,11 @@ Typical flow:
211
211
 
212
212
  ```text
213
213
  Project Manager
214
- -> Architect
214
+ -> Architect (plan, scaffold, and architecture docs)
215
215
  -> Coder
216
216
  -> Tester
217
- -> Architect docs sync
217
+ -> validation-adequacy Gate Review
218
+ -> code-diff Gate Review (code and docs)
218
219
  -> Project Manager final acceptance
219
220
  ```
220
221
 
@@ -7,6 +7,7 @@ export function createCommandRunner() {
7
7
  cwd: options.cwd,
8
8
  env: options.env,
9
9
  timeout: options.timeoutMs,
10
+ input: options.input,
10
11
  reject: false
11
12
  });
12
13
  return {
@@ -70,13 +70,16 @@ export function createNodeFileSystemAdapter() {
70
70
  async readJson(targetPath) {
71
71
  let attempt = 0;
72
72
  for (;;) {
73
- const content = await runFileOperation(() => fs.readFile(targetPath, "utf8"));
74
73
  try {
74
+ const content = await runFileOperation(() => fs.readFile(targetPath, "utf8"));
75
75
  return JSON.parse(content);
76
76
  }
77
77
  catch (error) {
78
+ // A concurrent rename-over can leave the target briefly missing on
79
+ // some shared mounts, so ENOENT gets the same short retry as a
80
+ // partially written file. A file that stays missing still throws.
78
81
  const delayMs = JSON_READ_RETRY_DELAYS_MS[attempt];
79
- if (!(error instanceof SyntaxError) || delayMs === undefined) {
82
+ if (!(error instanceof SyntaxError || getErrorCode(error) === "ENOENT") || delayMs === undefined) {
80
83
  throw error;
81
84
  }
82
85
  attempt += 1;
@@ -147,9 +147,6 @@ function artifactNameToPath(paths, artifactName) {
147
147
  if (artifactName === "docs-update-report.md") {
148
148
  return paths.docsUpdateReportPath;
149
149
  }
150
- if (artifactName === "docs-sync-report.md") {
151
- return paths.docsSyncReportPath;
152
- }
153
150
  if (artifactName === "workflow-progress.md") {
154
151
  return paths.workflowProgressPath;
155
152
  }
@@ -226,7 +226,6 @@ function degradedArtifactSummary(handoffDir) {
226
226
  architectureDiagnosisPath: `${handoffDir}/architecture-diagnosis.md`,
227
227
  testReportPath: `${handoffDir}/test-report.md`,
228
228
  docsUpdateReportPath: `${handoffDir}/docs-update-report.md`,
229
- docsSyncReportPath: `${handoffDir}/docs-sync-report.md`,
230
229
  workflowProgressPath: `${handoffDir}/workflow-progress.md`,
231
230
  finalAcceptancePath: `${handoffDir}/final-acceptance.md`
232
231
  },
@@ -320,6 +320,12 @@ const WHOLE_FILES = [
320
320
  mode: 0o755,
321
321
  templatePath: "scripts/harness-tools/check-durable-docs"
322
322
  },
323
+ {
324
+ path: ".ai/tools/check-docs-sync",
325
+ category: "durable-docs-tool",
326
+ mode: 0o755,
327
+ templatePath: "scripts/harness-tools/check-docs-sync"
328
+ },
323
329
  {
324
330
  path: ".claude/skills/vcm-architecture-interview/SKILL.md",
325
331
  category: "skill",
@@ -2,6 +2,7 @@ import path from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import Fastify from "fastify";
4
4
  import fastifyStatic from "@fastify/static";
5
+ import { createDocsCoverageCheck } from "./services/docs-sync-check.js";
5
6
  import { createArtifactService } from "./services/artifact-service.js";
6
7
  import { createClaudeAdapter } from "./adapters/claude-adapter.js";
7
8
  import { createCodexBridgeAdapter } from "./adapters/codex-bridge-adapter.js";
@@ -354,6 +355,7 @@ export function createDefaultServerDeps(options = {}) {
354
355
  sessionService,
355
356
  roundService,
356
357
  workflowControlService,
358
+ checkDocsCoverage: createDocsCoverageCheck(runner),
357
359
  onArchitecturePlanDisposition: ({ repoRoot, taskSlug, accepted }) => architectRestartService.recordArchitectureGateDisposition(repoRoot, taskSlug, accepted)
358
360
  });
359
361
  const translationWorkerService = createTranslationWorkerService({
@@ -4,7 +4,7 @@ import { checkMarkdownArtifact, readArtifactSectionValue } from "../../shared/va
4
4
  import { getManagedArtifactDefinition, isArtifactKind, isManagedArtifactKind } from "../../shared/validation/artifact-registry.js";
5
5
  import { VcmError } from "../errors.js";
6
6
  import { resolveRepoPath } from "../adapters/filesystem.js";
7
- import { renderArchitectureBriefTemplate, renderArchitectureDiagnosisTemplate, renderArchitectureEvidenceTemplate, renderArchitecturePlanTemplate, renderArchitectDebugTemplate, renderCoderCompletionTemplate, renderDocsUpdateReportTemplate, renderDocsSyncReportTemplate, renderFinalAcceptanceTemplate, renderKnownIssuesTemplate, renderPlanningProgressTemplate, renderMessageRouteTemplate, renderTestReportTemplate, renderWorkflowProgressTemplate } from "../templates/handoff.js";
7
+ import { renderArchitectureBriefTemplate, renderArchitectureDiagnosisTemplate, renderArchitectureEvidenceTemplate, renderArchitecturePlanTemplate, renderArchitectDebugTemplate, renderCoderCompletionTemplate, renderDocsUpdateReportTemplate, renderFinalAcceptanceTemplate, renderKnownIssuesTemplate, renderPlanningProgressTemplate, renderMessageRouteTemplate, renderTestReportTemplate, renderWorkflowProgressTemplate } from "../templates/handoff.js";
8
8
  import { renderRoleCommandTemplate } from "../templates/role-command.js";
9
9
  import { isMemoryProposalSubmissionPath } from "./memory-review-paths.js";
10
10
  import { validateManagedArtifactContent } from "./managed-artifact-validation.js";
@@ -19,7 +19,6 @@ const ARTIFACT_PATH_KEYS = [
19
19
  ["architecture-diagnosis", "architectureDiagnosisPath"],
20
20
  ["test-report", "testReportPath"],
21
21
  ["docs-update-report", "docsUpdateReportPath"],
22
- ["docs-sync-report", "docsSyncReportPath"],
23
22
  ["workflow-progress", "workflowProgressPath"],
24
23
  ["final-acceptance", "finalAcceptancePath"]
25
24
  ];
@@ -57,7 +56,6 @@ export function createArtifactService(fs, deps = {}) {
57
56
  architectureDiagnosisPath: path.posix.join(handoffDir, "architecture-diagnosis.md"),
58
57
  testReportPath: path.posix.join(handoffDir, "test-report.md"),
59
58
  docsUpdateReportPath: path.posix.join(handoffDir, "docs-update-report.md"),
60
- docsSyncReportPath: path.posix.join(handoffDir, "docs-sync-report.md"),
61
59
  workflowProgressPath: path.posix.join(handoffDir, "workflow-progress.md"),
62
60
  finalAcceptancePath: path.posix.join(handoffDir, "final-acceptance.md")
63
61
  };
@@ -88,7 +86,6 @@ export function createArtifactService(fs, deps = {}) {
88
86
  [paths.architectureDiagnosisPath, renderArchitectureDiagnosisTemplate(input.taskSlug)],
89
87
  [paths.testReportPath, renderTestReportTemplate(input.taskSlug)],
90
88
  [paths.docsUpdateReportPath, renderDocsUpdateReportTemplate(input.taskSlug)],
91
- [paths.docsSyncReportPath, renderDocsSyncReportTemplate(input.taskSlug)],
92
89
  [paths.workflowProgressPath, renderWorkflowProgressTemplate(input.taskSlug)],
93
90
  [paths.finalAcceptancePath, renderFinalAcceptanceTemplate(input.taskSlug)],
94
91
  ...Object.values(paths.messageRoutePaths).map((messagePath) => [
@@ -230,18 +227,6 @@ export function createArtifactService(fs, deps = {}) {
230
227
  if (validation.errors.length > 0) {
231
228
  throw artifactRejected(input.kind, validation.errors);
232
229
  }
233
- if ((input.kind === "docs-update-report" || input.kind === "docs-sync-report")
234
- && deps.workflowControlService) {
235
- // Memory-owned documentation work is independent of PM's active dispatch.
236
- if (!assignment) {
237
- await deps.workflowControlService.assertDocsArtifactAllowed({
238
- taskRepoRoot: input.repoRoot,
239
- stateRoot: input.stateRoot ?? ".ai/vcm",
240
- handoffDir: input.handoffDir,
241
- taskSlug: input.taskSlug
242
- }, input.kind, input.role);
243
- }
244
- }
245
230
  if (input.kind === "workflow-progress") {
246
231
  if (input.mode !== "final") {
247
232
  throw artifactRejected(input.kind, ["Workflow Progress must be submitted in final mode."]);
@@ -8,6 +8,7 @@ import { submitTerminalInput } from "../runtime/terminal-submit.js";
8
8
  import { readVcmMemoryHostFrame, readVcmMemoryBlock, replaceVcmMemoryBlock } from "../templates/harness/memory-block.js";
9
9
  import { ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH, architectPlanningCandidateSnapshotPath, durableDocAssignmentReportPath, memoryReviewRoleDraftPath, MEMORY_REVIEW_RUNS_ROOT, MEMORY_REVIEW_STATE_PATH } from "./memory-review-paths.js";
10
10
  import { parseMemoryProposal, validateMemoryProposal } from "./memory-proposal-validation.js";
11
+ const MEMORY_REVIEW_STOP_BLOCK_LIMIT = 3;
11
12
  const MEMORY_FILE_DEFINITIONS = [
12
13
  { path: "CLAUDE.md", title: "Shared Memory" },
13
14
  { path: ".claude/agents/project-manager.md", title: "Project Manager Memory", role: "project-manager" },
@@ -436,43 +437,75 @@ export function createAutoMemoryService(deps) {
436
437
  if (!(await deps.appSettings.getPreferences()).autoMemoryEnabled && !preserveDisabledReview(state)) {
437
438
  if (state) {
438
439
  await discardActiveReview(input.taskRepoRoot, state);
439
- return true;
440
+ return { handled: true };
440
441
  }
441
- return false;
442
+ return { handled: false };
442
443
  }
443
444
  if (state?.status === "reviewing" && !state.reviewPromptDispatchedAt) {
444
- return false;
445
+ return { handled: false };
445
446
  }
446
447
  if (!state || state.status !== "reviewing") {
447
- return false;
448
+ return { handled: false };
448
449
  }
449
450
  if (input.eventName === "UserPromptSubmit" || input.eventName === "PostCompact") {
450
- return true;
451
+ return { handled: true };
451
452
  }
452
453
  if (input.eventName === "StopFailure") {
453
454
  await failReview(input.taskRepoRoot, state, "Task Harness Retrospective memory review turn failed.");
454
- return true;
455
+ return { handled: true };
455
456
  }
456
457
  if (input.eventName === "Stop") {
457
458
  if (!state.retrospectiveReportPath
458
459
  || !(await deps.fs.pathExists(state.retrospectiveReportPath))
459
460
  || !(await deps.fs.readText(state.retrospectiveReportPath)).trim()) {
460
- await failReview(input.taskRepoRoot, state, "Task Harness Retrospective did not write the required retrospective report.");
461
- return true;
461
+ return rejectHarnessEngineerStop(input, state, "Task Harness Retrospective did not write the required retrospective report.");
462
462
  }
463
463
  try {
464
464
  await recordHarnessEngineerMemoryResult(input.taskRepoRoot, state);
465
- if (state.assignments.length > 0) {
466
- await dispatchCurrentDurableDocAssignment(input.baseRepoRoot, input.taskRepoRoot, state);
467
- }
468
465
  }
469
466
  catch (error) {
470
- await failReview(input.taskRepoRoot, state, `Harness Engineer memory result could not be recorded: ${errorMessage(error)}`);
471
- return true;
467
+ const message = `Harness Engineer memory result could not be recorded: ${errorMessage(error)}`;
468
+ if (!isFixableMemoryResultError(error)) {
469
+ await failReview(input.taskRepoRoot, state, message);
470
+ return { handled: true };
471
+ }
472
+ const hint = error instanceof VcmError && error.hint ? ` ${error.hint}` : "";
473
+ return rejectHarnessEngineerStop(input, state, `${message}${hint}`);
472
474
  }
473
- return true;
475
+ if (state.assignments.length > 0) {
476
+ try {
477
+ await dispatchCurrentDurableDocAssignment(input.baseRepoRoot, input.taskRepoRoot, state);
478
+ }
479
+ catch (error) {
480
+ await failReview(input.taskRepoRoot, state, `Durable-document assignment could not be dispatched: ${errorMessage(error)}`);
481
+ }
482
+ }
483
+ return { handled: true };
474
484
  }
475
- return true;
485
+ return { handled: true };
486
+ }
487
+ // A correctable retrospective result keeps the Harness Engineer turn alive
488
+ // with the validation error, a bounded number of times, so the role can fix
489
+ // review-result.json or the memory commit instead of the review failing
490
+ // silently after the turn has ended.
491
+ async function rejectHarnessEngineerStop(input, state, message) {
492
+ const blocks = state.stopBlockCount ?? 0;
493
+ if (!input.allowStopBlock || blocks >= MEMORY_REVIEW_STOP_BLOCK_LIMIT) {
494
+ await failReview(input.taskRepoRoot, state, message);
495
+ return { handled: true };
496
+ }
497
+ state.stopBlockCount = blocks + 1;
498
+ state.error = message;
499
+ state.updatedAt = now();
500
+ await persistActiveState(input.taskRepoRoot, state);
501
+ return {
502
+ handled: true,
503
+ stopBlockReason: [
504
+ message,
505
+ `Fix the retrospective report, ${resolveRepoPath(input.taskRepoRoot, reviewResultRelativePath(state.runId))}, or the memory commit, then end the turn again.`,
506
+ `Attempt ${state.stopBlockCount} of ${MEMORY_REVIEW_STOP_BLOCK_LIMIT}; after that the memory review fails.`
507
+ ].join(" ")
508
+ };
476
509
  }
477
510
  async function getFile(_baseRepoRoot, taskRepoRoot, filePath) {
478
511
  await assertMemoryBlocksInstalled(deps.fs, taskRepoRoot);
@@ -549,9 +582,47 @@ export function createAutoMemoryService(deps) {
549
582
  statusCode: 409
550
583
  });
551
584
  }
585
+ if (await harnessEngineerResultWritten(taskRepoRoot, state)) {
586
+ // The Harness Engineer finished its turn, so re-validate the current
587
+ // result file and memory commit before discarding anything.
588
+ try {
589
+ await recordHarnessEngineerMemoryResult(taskRepoRoot, state);
590
+ }
591
+ catch (error) {
592
+ if (await memoryChangedSinceReview(taskRepoRoot, state)) {
593
+ // Reviewed memory is already committed; restarting would lose the
594
+ // durable-document moves recorded only in this run's result file.
595
+ const message = `Harness Engineer memory result could not be recorded: ${errorMessage(error)}`;
596
+ await failReview(taskRepoRoot, state, message);
597
+ throw new VcmError({
598
+ code: "MEMORY_REVIEW_RESULT_STILL_INVALID",
599
+ message,
600
+ statusCode: 409,
601
+ hint: `Correct ${resolveRepoPath(taskRepoRoot, reviewResultRelativePath(state.runId))} or the memory commit, then retry.`
602
+ });
603
+ }
604
+ await clearActiveState(taskRepoRoot);
605
+ return getState(baseRepoRoot, taskRepoRoot);
606
+ }
607
+ if (state.assignments.length > 0) {
608
+ await dispatchCurrentDurableDocAssignment(baseRepoRoot, taskRepoRoot, state);
609
+ }
610
+ return getState(baseRepoRoot, taskRepoRoot);
611
+ }
552
612
  await clearActiveState(taskRepoRoot);
553
613
  return getState(baseRepoRoot, taskRepoRoot);
554
614
  }
615
+ async function harnessEngineerResultWritten(taskRepoRoot, state) {
616
+ if (!state.reviewBaseCommit || !state.reviewPromptDispatchedAt) {
617
+ return false;
618
+ }
619
+ return deps.fs.pathExists(resolveRepoPath(taskRepoRoot, reviewResultRelativePath(state.runId)));
620
+ }
621
+ async function memoryChangedSinceReview(taskRepoRoot, state) {
622
+ const before = await readRunMemorySet(taskRepoRoot, state.runId, "before");
623
+ const after = await readMemorySet(taskRepoRoot);
624
+ return MEMORY_FILE_DEFINITIONS.some((definition) => before[definition.path] !== after[definition.path]);
625
+ }
555
626
  async function retryDurableDocAssignment(baseRepoRoot, taskRepoRoot, assignmentId) {
556
627
  const state = await loadActiveState(taskRepoRoot);
557
628
  const assignment = state && (state.status === "documenting" || state.status === "failed")
@@ -996,7 +1067,7 @@ export function createAutoMemoryService(deps) {
996
1067
  await persistActiveState(taskRepoRoot, state);
997
1068
  }
998
1069
  async function readAndValidateHarnessMemoryReviewResult(taskRepoRoot, state, before, after, currentHead, changedMemoryPaths) {
999
- const resultPath = resolveRepoPath(taskRepoRoot, `${MEMORY_REVIEW_RUNS_ROOT}/${state.runId}/review-result.json`);
1070
+ const resultPath = resolveRepoPath(taskRepoRoot, reviewResultRelativePath(state.runId));
1000
1071
  if (!(await deps.fs.pathExists(resultPath))) {
1001
1072
  throw new VcmError({
1002
1073
  code: "MEMORY_REVIEW_RESULT_MISSING",
@@ -1791,6 +1862,20 @@ function ensureTrailingNewline(content) {
1791
1862
  function sha256(content) {
1792
1863
  return createHash("sha256").update(content).digest("hex");
1793
1864
  }
1865
+ function reviewResultRelativePath(runId) {
1866
+ return `${MEMORY_REVIEW_RUNS_ROOT}/${runId}/review-result.json`;
1867
+ }
1868
+ // Errors the Harness Engineer can correct in its own output: the result file,
1869
+ // the memory edit, or the memory commit. A malformed result file surfaces as a
1870
+ // JSON SyntaxError.
1871
+ function isFixableMemoryResultError(error) {
1872
+ if (error instanceof SyntaxError) {
1873
+ return true;
1874
+ }
1875
+ return error instanceof VcmError
1876
+ && error.code.startsWith("MEMORY_")
1877
+ && error.code !== "MEMORY_REVIEW_BASE_MISSING";
1878
+ }
1794
1879
  function errorMessage(error) {
1795
1880
  return error instanceof Error ? error.message : String(error);
1796
1881
  }
@@ -114,29 +114,30 @@ export function createClaudeHookService(deps) {
114
114
  dispatchedCount: 0
115
115
  };
116
116
  }
117
- async function processHarnessEngineerHook(input) {
117
+ async function processHarnessEngineerHook(input, options) {
118
118
  const eventName = parseBusinessHookEvent(input.event.hook_event_name);
119
119
  const context = await getProjectToolHookContext("Harness Engineer");
120
120
  const projectScoped = input.taskSlug === "__project_harness_engineer__";
121
- const session = projectScoped
122
- ? await deps.sessionService.recordProjectHarnessEngineerHookEvent(context.project.repoRoot, {
123
- eventName,
121
+ const recordSessionEvent = (recordedEventName) => projectScoped
122
+ ? deps.sessionService.recordProjectHarnessEngineerHookEvent(context.project.repoRoot, {
123
+ eventName: recordedEventName,
124
124
  sessionId: stringOrUndefined(input.event.session_id),
125
125
  transcriptPath: stringOrUndefined(input.event.transcript_path),
126
126
  cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd),
127
127
  runtimeSessionId: stringOrUndefined(input.event.vcm_runtime_session_id),
128
128
  runtimeSessionToken: input.runtimeSessionToken
129
129
  })
130
- : await deps.sessionService.recordRoleHookEvent(context.project.repoRoot, {
130
+ : deps.sessionService.recordRoleHookEvent(context.project.repoRoot, {
131
131
  taskSlug: input.taskSlug,
132
132
  role: input.role,
133
- eventName,
133
+ eventName: recordedEventName,
134
134
  sessionId: stringOrUndefined(input.event.session_id),
135
135
  transcriptPath: stringOrUndefined(input.event.transcript_path),
136
136
  cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd),
137
137
  runtimeSessionId: stringOrUndefined(input.event.vcm_runtime_session_id),
138
138
  runtimeSessionToken: input.runtimeSessionToken
139
139
  });
140
+ const session = await recordSessionEvent(eventName);
140
141
  if (!session) {
141
142
  return completedHookResult(input, eventName);
142
143
  }
@@ -144,14 +145,32 @@ export function createClaudeHookService(deps) {
144
145
  ? (await deps.taskService.listTasks(context.project.repoRoot))
145
146
  .find((task) => task.cleanupStatus !== "cleaned" && (projectScoped || task.taskSlug === input.taskSlug))
146
147
  : undefined;
147
- const memoryHandled = activeTask
148
+ const memoryResult = activeTask
148
149
  ? await deps.autoMemoryService?.handleHarnessEngineerHook({
149
150
  baseRepoRoot: context.project.repoRoot,
150
151
  taskRepoRoot: getTaskRuntimeRepoRoot(activeTask),
151
152
  taskSlug: activeTask.taskSlug,
152
- eventName
153
+ eventName,
154
+ allowStopBlock: options.allowBlock && eventName === "Stop"
153
155
  })
154
- : false;
156
+ : undefined;
157
+ if (memoryResult?.stopBlockReason) {
158
+ // The memory review result is still correctable: keep the Harness
159
+ // Engineer turn alive and skip retrospective completion until it passes.
160
+ // The turn continues, so the session must not stay recorded as idle and
161
+ // receive queued input meanwhile.
162
+ await recordSessionEvent("UserPromptSubmit");
163
+ return {
164
+ ok: true,
165
+ eventName,
166
+ taskSlug: activeTask?.taskSlug ?? input.taskSlug,
167
+ role: input.role,
168
+ sessionUpdated: Boolean(session),
169
+ dispatchedCount: 0,
170
+ stopDecision: { behavior: "block", reason: memoryResult.stopBlockReason }
171
+ };
172
+ }
173
+ const memoryHandled = memoryResult?.handled ?? false;
155
174
  const memoryState = activeTask && deps.autoMemoryService
156
175
  ? await deps.autoMemoryService.getState(context.project.repoRoot, getTaskRuntimeRepoRoot(activeTask))
157
176
  : undefined;
@@ -631,7 +650,7 @@ export function createClaudeHookService(deps) {
631
650
  });
632
651
  if (eventName === "Stop") {
633
652
  const memoryState = await deps.autoMemoryService.getState(context.project.repoRoot, context.taskRepoRoot);
634
- await deps.harnessFeedbackService?.completeWaitingTaskRetrospective(context.project.repoRoot, context.taskSlug, memoryState.status);
653
+ await deps.harnessFeedbackService?.completeWaitingTaskRetrospective(context.project.repoRoot, context.taskSlug, memoryState);
635
654
  }
636
655
  return {
637
656
  ok: true,
@@ -908,7 +927,7 @@ export function createClaudeHookService(deps) {
908
927
  return processTranslatorHook(input);
909
928
  }
910
929
  if (isHarnessEngineerToolRoleName(input.role)) {
911
- return processHarnessEngineerHook(input);
930
+ return processHarnessEngineerHook(input, { allowBlock: false });
912
931
  }
913
932
  if (eventName === "UserPromptSubmit") {
914
933
  return handleUserPromptSubmitHook(input);
@@ -930,7 +949,7 @@ export function createClaudeHookService(deps) {
930
949
  return processTranslatorHook(input);
931
950
  }
932
951
  if (isHarnessEngineerToolRoleName(input.role)) {
933
- return processHarnessEngineerHook(input);
952
+ return processHarnessEngineerHook(input, { allowBlock: true });
934
953
  }
935
954
  return processStopHook(input, { allowBlock: true });
936
955
  });
@@ -0,0 +1,32 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ const DOCS_SYNC_TOOL = ".ai/tools/check-docs-sync";
4
+ const DOCS_SYNC_TIMEOUT_MS = 60_000;
5
+ // Runs the task worktree's check-docs-sync, which compares the task's commits
6
+ // with the architecture plan's Docs Impact, and returns one error per finding.
7
+ // A worktree whose harness predates the tool is not checked.
8
+ export function createDocsCoverageCheck(runner) {
9
+ return async (taskRepoRoot) => {
10
+ const tool = path.join(taskRepoRoot, DOCS_SYNC_TOOL);
11
+ if (!fs.existsSync(tool)) {
12
+ return [];
13
+ }
14
+ const result = await runner.run(process.execPath, [tool, "--json"], {
15
+ cwd: taskRepoRoot,
16
+ timeoutMs: DOCS_SYNC_TIMEOUT_MS
17
+ });
18
+ if (result.exitCode === 0) {
19
+ return [];
20
+ }
21
+ try {
22
+ const parsed = JSON.parse(result.stdout);
23
+ if (result.exitCode === 1 && Array.isArray(parsed.findings) && parsed.findings.length > 0) {
24
+ return parsed.findings.map((finding) => `${finding.code} ${finding.subject}: ${finding.message}`);
25
+ }
26
+ }
27
+ catch {
28
+ // Fall through to the raw tool error below.
29
+ }
30
+ return [`${DOCS_SYNC_TOOL} could not check the task: ${(result.stderr || result.stdout).trim() || `exit ${result.exitCode}`}`];
31
+ };
32
+ }
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import path from "node:path";
3
- import { CODE_DIFF_FINDING_SCOPES, CODE_DIFF_SOURCES, GATE_REVIEW_GATES } from "../../shared/types/gate-review.js";
3
+ import { CODE_DIFF_DOCS_COVERAGE_ERROR_PREFIX, CODE_DIFF_FINDING_SCOPES, CODE_DIFF_SOURCES, GATE_REVIEW_GATES } from "../../shared/types/gate-review.js";
4
4
  import { checkMarkdownArtifact, readArtifactSectionValue } from "../../shared/validation/artifact-check.js";
5
5
  import { VcmError } from "../errors.js";
6
6
  import { resolveRepoPath } from "../adapters/filesystem.js";
@@ -379,6 +379,14 @@ export function createGateReviewService(deps) {
379
379
  message: prerequisiteError
380
380
  };
381
381
  }
382
+ const coverageFindings = deps.checkDocsCoverage
383
+ ? await deps.checkDocsCoverage(context.taskRepoRoot)
384
+ : [];
385
+ if (coverageFindings.length > 0) {
386
+ const message = `${CODE_DIFF_DOCS_COVERAGE_ERROR_PREFIX} ${coverageFindings.join(" ")}`;
387
+ index = await recordCodeDiffStartFailure(index, context, gate, codeDiffSource, message, codeDiffInput);
388
+ return { status: "failed_to_start", gate, record: index.gates[gate], message };
389
+ }
382
390
  }
383
391
  async function recordCodeDiffStartFailure(currentIndex, currentContext, currentGate, currentSource, message, currentCodeDiffInput) {
384
392
  const failedIndex = applyGateState(currentIndex, currentGate, {
@@ -1293,7 +1301,7 @@ async function computeInputHash(deps, taskRepoRoot, gate, codeDiffInput, codeDif
1293
1301
  }
1294
1302
  }
1295
1303
  if (gate === "validation-adequacy") {
1296
- const evidencePathspec = ["--", ".", ":(exclude).ai/vcm/**", ":(exclude)docs/**"];
1304
+ const evidencePathspec = ["--", ".", ":(exclude).ai/vcm/**", ":(exclude)docs/**", ":(exclude,glob)**/ARCHITECTURE.md", ":(exclude).ai/generated/**"];
1297
1305
  digest.update("trackedEvidence");
1298
1306
  digest.update(await commandStdout(deps.runner, taskRepoRoot, ["ls-files", "-s", ...evidencePathspec]));
1299
1307
  digest.update("workingEvidence");
@@ -175,6 +175,7 @@ export function createHarnessFeedbackService(deps) {
175
175
  ...processedMarker,
176
176
  status: "failed",
177
177
  durableDocFailure: input.memoryReviewHasAssignments,
178
+ memoryReviewFailure: true,
178
179
  failedAt: timestamp,
179
180
  updatedAt: timestamp,
180
181
  error: input.memoryReviewError
@@ -194,15 +195,28 @@ export function createHarnessFeedbackService(deps) {
194
195
  await completeTaskRetrospective(repoRoot, processedMarker);
195
196
  return true;
196
197
  }
197
- async function completeWaitingTaskRetrospective(repoRoot, taskSlug, memoryStatus) {
198
+ async function completeWaitingTaskRetrospective(repoRoot, taskSlug, memory) {
198
199
  const marker = await loadTaskRetrospectiveMarker(repoRoot, taskSlug);
199
- if (!marker || (marker.status !== "waiting-docs" && !(marker.status === "failed" && marker.durableDocFailure))) {
200
+ // A memory review that failed validation recovers only when a retry
201
+ // re-validated and applied that same run; a discarded run never does.
202
+ const memoryReviewRecovered = Boolean(marker?.memoryReviewFailure
203
+ && marker.memoryRunId
204
+ && memory.runs.some((run) => run.runId === marker.memoryRunId && run.status === "applied"));
205
+ if (!marker
206
+ || (marker.status !== "waiting-docs"
207
+ && !(marker.status === "failed" && (marker.durableDocFailure || memoryReviewRecovered)))) {
200
208
  return false;
201
209
  }
210
+ const memoryStatus = memory.status;
202
211
  if (memoryStatus === "documenting") {
203
212
  if (marker.status === "failed") {
204
213
  await persistTaskRetrospectiveMarker(repoRoot, {
205
- ...marker, status: "waiting-docs", updatedAt: now(), error: undefined, failedAt: undefined
214
+ ...marker,
215
+ status: "waiting-docs",
216
+ memoryReviewFailure: undefined,
217
+ updatedAt: now(),
218
+ error: undefined,
219
+ failedAt: undefined
206
220
  });
207
221
  }
208
222
  return true;
@@ -235,6 +249,7 @@ export function createHarnessFeedbackService(deps) {
235
249
  ...processedMarker,
236
250
  status: "completed",
237
251
  durableDocFailure: undefined,
252
+ memoryReviewFailure: undefined,
238
253
  error: undefined,
239
254
  failedAt: undefined,
240
255
  completedAt: timestamp,
@@ -408,8 +423,11 @@ export function createHarnessFeedbackService(deps) {
408
423
  `Write the complete machine-readable review to: ${memoryReview.reviewResultPath}`,
409
424
  "The JSON root must be: {\"version\":1,\"runId\":\"<assigned run id>\",\"memoryCommit\":\"<full commit or none>\",\"decisions\":[],\"durableDocAssignments\":[]}.",
410
425
  "Each decision must contain itemId, source (existing|proposal), target, entry, decision, reason, impactIfAbsent, evidence (non-empty array), finalContent, and durableDocPath.",
426
+ "Every decision field is a non-empty string except evidence. Use \"none\" for finalContent when the decision keeps no memory text (remove, reject, move-to-durable-doc), and for durableDocPath unless the decision is move-to-durable-doc, where it must equal the assignment targetPath.",
427
+ "For retain, update, keep-in-memory, and keep-memory-reference, finalContent must be the exact memory text now present in the <VCM-memory> block; an update's finalContent must be one complete semantic entry as it now appears.",
411
428
  "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.",
412
429
  "Each durableDocAssignment must contain sourceMemoryPath, sourceEntry, targetPath, content, reason, and evidence (non-empty array). Use a project-relative Markdown target outside .ai/vcm.",
430
+ "VCM validates review-result.json and the memory commit when you end the turn. If validation fails, the Stop is rejected with the error; correct the result file or the memory commit and end the turn again.",
413
431
  "In the retrospective report, include only this memory summary:",
414
432
  "",
415
433
  "## Memory Review",
@@ -41,7 +41,9 @@ export function lspUnavailableWarning(language) {
41
41
  : `Install or repair ${language.serverCommand} in the environment running the VCM backend${installCommands[language.serverCommand] ? ` (${installCommands[language.serverCommand]})` : ""} and ensure it is executable on PATH.`;
42
42
  return `LSP for ${language.label} is unavailable: ${language.error ?? `${language.serverCommand} is not runnable.`} ${recovery} Restart the LSP roles (Architect, Reviewer) after fixing the environment so VCM can detect and register the server again.`;
43
43
  }
44
- export function renderLspNotice(status) {
44
+ // Architect stops semantic work in an unavailable language; Reviewer completes
45
+ // the review and records those relationships as not LSP-verified.
46
+ export function renderLspNotice(status, role = "architect") {
45
47
  if (status.languages.length === 0)
46
48
  return undefined;
47
49
  const available = status.languages.filter((language) => language.state === "server_runnable");
@@ -54,7 +56,9 @@ export function renderLspNotice(status) {
54
56
  ] : []),
55
57
  ...unavailable.map(lspUnavailableWarning),
56
58
  ...(unavailable.length > 0 ? [
57
- "If your assignment requires a semantic query in an unavailable language, stop that work and report the environment prerequisite through the current communication channel. Do not substitute source text search or claim the relationship is verified. Continue only after the server is available."
59
+ role === "reviewer"
60
+ ? "For relationships in an unavailable language, complete the review by reading the complete candidate code, record each such relationship as not LSP-verified in the report, and name the missing server. Do not stop the review or treat text matches as verified relationships."
61
+ : "If your assignment requires a semantic query in an unavailable language, stop that work and report the environment prerequisite through the current communication channel. Do not substitute source text search or claim the relationship is verified. Continue only after the server is available."
58
62
  ] : [])
59
63
  ].join("\n");
60
64
  }
@@ -26,6 +26,7 @@ export const GATE_ANALYSIS_FIELDS = {
26
26
  "Invalidated Assumptions",
27
27
  "Existing-Class Completeness",
28
28
  "Relationship Evidence",
29
+ "Architecture Docs",
29
30
  "Ownership",
30
31
  "Data Flow",
31
32
  "Lifecycle",
@@ -60,7 +61,7 @@ export const GATE_ANALYSIS_FIELDS = {
60
61
  "State Lifecycle And Failure Paths",
61
62
  "Coding Standards",
62
63
  "Baseline Test Integrity",
63
- "Generated Context And Durable Docs",
64
+ "Documentation Fit",
64
65
  "Code Readiness"
65
66
  ]
66
67
  };
@@ -59,7 +59,7 @@ export function createRuntimeCoordinatorService(deps) {
59
59
  await deps.translationService.stopTask(taskRepoRoot, activeTask.taskSlug).catch(() => undefined);
60
60
  }
61
61
  const memoryState = await reconcileAutoMemory(repoRoot, activeTask, preferences.autoTaskHarnessReviewEnabled ? "auto" : undefined);
62
- await deps.harnessFeedbackService.completeWaitingTaskRetrospective(repoRoot, activeTask.taskSlug, memoryState.status);
62
+ await deps.harnessFeedbackService.completeWaitingTaskRetrospective(repoRoot, activeTask.taskSlug, memoryState);
63
63
  const memoryReadiness = await getTaskRetrospectiveMemoryReadiness(repoRoot, activeTask);
64
64
  if ((preferences.autoTaskHarnessReviewEnabled || memoryReadiness.trigger) && memoryReadiness.ready) {
65
65
  await maybeStartTaskHarnessRetrospective(repoRoot, activeTask, memoryReadiness.trigger ?? "auto");
@@ -85,7 +85,7 @@ export function createSessionService(deps) {
85
85
  const lspPluginDirs = roleUsesLsp(role)
86
86
  ? lspStatus ? await prepareTaskLspPlugin(deps.fs, taskRepoRoot, lspStatus) : [VCM_LSP_PLUGIN_DIR]
87
87
  : [];
88
- const appendSystemPrompt = [input.appendSystemPrompt, lspStatus && renderLspNotice(lspStatus)]
88
+ const appendSystemPrompt = [input.appendSystemPrompt, lspStatus && renderLspNotice(lspStatus, role)]
89
89
  .filter(Boolean).join("\n\n") || undefined;
90
90
  const startCommand = {
91
91
  ...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride, appendSystemPrompt, lspPluginDirs),
@@ -60,7 +60,6 @@ function degradedArtifactSummary(handoffDir) {
60
60
  architectureDiagnosisPath: `${handoffDir}/architecture-diagnosis.md`,
61
61
  testReportPath: `${handoffDir}/test-report.md`,
62
62
  docsUpdateReportPath: `${handoffDir}/docs-update-report.md`,
63
- docsSyncReportPath: `${handoffDir}/docs-sync-report.md`,
64
63
  workflowProgressPath: `${handoffDir}/workflow-progress.md`,
65
64
  finalAcceptancePath: `${handoffDir}/final-acceptance.md`
66
65
  },