vibe-coding-master 0.8.5 → 0.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -589,10 +589,26 @@ before documentation work starts. VCM assigns architecture and known-issues
589
589
  documents to Architect and testing documentation to Tester. For another target,
590
590
  PM selects Architect, Coder, or Tester and asks the user only when ownership
591
591
  cannot be determined. These assignments are backend-dispatched work rather than
592
- a Docs-Only Flow. Each owner updates and commits the document, submits the exact
593
- assignment ID in `docs-update-report.md`, and finishes before the next assignment
594
- starts. Harness Studio shows assignment status and allows failed work to be
595
- retried.
592
+ a Docs-Only Flow. Each owner submits `docs-update-report` through `vcm-artifact`
593
+ with the exact assigned `--path` and Assignment ID. Reports are isolated under
594
+ `.ai/vcm/memory-review/runs/<run-id>/assignments/<assignment-id>/report.md`.
595
+ The reported commit must modify the target document within the assignment's
596
+ original commit range; unrelated later commits do not invalidate it. Existing
597
+ correct documentation can be reported as `unchanged` without a new commit.
598
+ For `synced`, the entire `Commit` section is one bare lowercase hexadecimal
599
+ hash (7-40 characters), without Markdown or explanatory text. The template,
600
+ artifact submission, and assignment completion use the same format contract.
601
+
602
+ Assignments run one at a time. A failed assignment does not block later items.
603
+ After the remaining items finish, unresolved failures are reported in Harness
604
+ Studio with their causes and individual Retry buttons. Retry preserves earlier
605
+ committed work and the original baseline, including after a backend restart.
606
+ An old assignment whose baseline was already overwritten is not automatically
607
+ rebased; verify the current document and use `unchanged` when it already preserves
608
+ the assigned content. Errors identify the reported commit and recorded range.
609
+ Migration content and review snapshots remain in the task-local run directory;
610
+ failures do not automatically restore removed memory. Turning Auto Memory off
611
+ does not discard or cancel already-applied document migrations.
596
612
 
597
613
  When Auto Memory is disabled, Review Task Harness does not collect proposals or
598
614
  ask Harness Engineer to update memory. When enabled, both automatic and manual
@@ -246,7 +246,7 @@ export function createDefaultServerDeps(options = {}) {
246
246
  const workflowControlService = createWorkflowControlService({ fs });
247
247
  const artifactService = createArtifactService(fs, {
248
248
  workflowControlService,
249
- isRoleMemoryTurn: (taskRepoRoot, role) => autoMemoryService.isRoleMemoryTurn(taskRepoRoot, role)
249
+ getDurableDocAssignment: (taskRepoRoot, role) => autoMemoryService.getDurableDocAssignment(taskRepoRoot, role)
250
250
  });
251
251
  const projectService = createProjectService({ fs, git, appSettings });
252
252
  const taskService = createTaskService({ fs, git, artifactService, projectService });
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { DISPATCHABLE_ROLES } from "../../shared/constants.js";
3
- import { checkMarkdownArtifact } from "../../shared/validation/artifact-check.js";
3
+ import { checkMarkdownArtifact, readArtifactSectionValue } from "../../shared/validation/artifact-check.js";
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";
@@ -208,10 +208,21 @@ export function createArtifactService(fs, deps = {}) {
208
208
  });
209
209
  }
210
210
  if (isArtifactKind(input.kind)) {
211
- if (input.artifactPath?.trim()) {
211
+ const assignment = input.kind === "docs-update-report"
212
+ ? await deps.getDurableDocAssignment?.(input.repoRoot, input.role)
213
+ : undefined;
214
+ if (assignment) {
215
+ if (input.artifactPath !== assignment.reportPath) {
216
+ throw artifactRejected(input.kind, [`Use the assigned --path ${assignment.reportPath}.`]);
217
+ }
218
+ if (readArtifactSectionValue(normalized, "Assignment ID")?.trim() !== assignment.id) {
219
+ throw artifactRejected(input.kind, [`Assignment ID must be exactly ${assignment.id}.`]);
220
+ }
221
+ }
222
+ else if (input.artifactPath?.trim()) {
212
223
  throw artifactRejected(input.kind, ["--path is not allowed for fixed artifact kinds."]);
213
224
  }
214
- const artifactPath = path.posix.join(input.handoffDir, definition.fileName ?? "");
225
+ const artifactPath = assignment?.reportPath ?? path.posix.join(input.handoffDir, definition.fileName ?? "");
215
226
  const validation = validateManagedArtifactContent(input.kind, normalized, {
216
227
  path: artifactPath,
217
228
  mode: input.mode
@@ -222,9 +233,7 @@ export function createArtifactService(fs, deps = {}) {
222
233
  if ((input.kind === "docs-update-report" || input.kind === "docs-sync-report")
223
234
  && deps.workflowControlService) {
224
235
  // Memory-owned documentation work is independent of PM's active dispatch.
225
- const memoryUpdate = input.kind === "docs-update-report"
226
- && await deps.isRoleMemoryTurn?.(input.repoRoot, input.role);
227
- if (!memoryUpdate) {
236
+ if (!assignment) {
228
237
  await deps.workflowControlService.assertDocsArtifactAllowed({
229
238
  taskRepoRoot: input.repoRoot,
230
239
  stateRoot: input.stateRoot ?? ".ai/vcm",
@@ -1,11 +1,12 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import path from "node:path";
3
- import { checkMarkdownArtifact, readArtifactSectionValue } from "../../shared/validation/artifact-check.js";
3
+ import { checkMarkdownArtifact, readArtifactSectionContent, readArtifactSectionValue } from "../../shared/validation/artifact-check.js";
4
+ import { DOCS_UPDATE_COMMIT_RULE } from "../../shared/validation/artifact-contract.js";
4
5
  import { resolveRepoPath } from "../adapters/filesystem.js";
5
6
  import { VcmError } from "../errors.js";
6
7
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
7
8
  import { readVcmMemoryHostFrame, readVcmMemoryBlock, replaceVcmMemoryBlock } from "../templates/harness/memory-block.js";
8
- import { ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH, architectPlanningCandidateSnapshotPath, memoryReviewRoleDraftPath, MEMORY_REVIEW_RUNS_ROOT, MEMORY_REVIEW_STATE_PATH } from "./memory-review-paths.js";
9
+ import { ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH, architectPlanningCandidateSnapshotPath, durableDocAssignmentReportPath, memoryReviewRoleDraftPath, MEMORY_REVIEW_RUNS_ROOT, MEMORY_REVIEW_STATE_PATH } from "./memory-review-paths.js";
9
10
  import { parseMemoryProposal, validateMemoryProposal } from "./memory-proposal-validation.js";
10
11
  const MEMORY_FILE_DEFINITIONS = [
11
12
  { path: "CLAUDE.md", title: "Shared Memory" },
@@ -18,6 +19,20 @@ const MEMORY_FILE_DEFINITIONS = [
18
19
  ];
19
20
  export function createAutoMemoryService(deps) {
20
21
  const now = deps.now ?? (() => new Date().toISOString());
22
+ const taskLocks = new Map();
23
+ async function withTaskLock(taskRepoRoot, operation) {
24
+ const previous = taskLocks.get(taskRepoRoot) ?? Promise.resolve();
25
+ const current = previous.catch(() => undefined).then(operation);
26
+ taskLocks.set(taskRepoRoot, current);
27
+ try {
28
+ return await current;
29
+ }
30
+ finally {
31
+ if (taskLocks.get(taskRepoRoot) === current) {
32
+ taskLocks.delete(taskRepoRoot);
33
+ }
34
+ }
35
+ }
21
36
  async function readMemorySet(repoRoot) {
22
37
  const memory = {};
23
38
  for (const definition of MEMORY_FILE_DEFINITIONS) {
@@ -151,7 +166,7 @@ export function createAutoMemoryService(deps) {
151
166
  else if (active?.status === "reviewing" && active.reviewPromptDispatchedAt) {
152
167
  return getState(input.baseRepoRoot, input.taskRepoRoot);
153
168
  }
154
- else if (active) {
169
+ else if (active && !preserveDisabledReview(active)) {
155
170
  await discardActiveReview(input.taskRepoRoot, active);
156
171
  }
157
172
  return getState(input.baseRepoRoot, input.taskRepoRoot);
@@ -361,6 +376,11 @@ export function createAutoMemoryService(deps) {
361
376
  }
362
377
  return assignment.status === "running" && assignment.owner === role;
363
378
  }
379
+ async function getDurableDocAssignment(taskRepoRoot, role) {
380
+ const state = await loadActiveState(taskRepoRoot);
381
+ const assignment = state?.status === "documenting" ? currentDurableDocAssignment(state) : undefined;
382
+ return assignment?.status === "running" && assignment.owner === role ? assignment : undefined;
383
+ }
364
384
  async function handleRoleHook(input) {
365
385
  const state = await loadActiveState(input.taskRepoRoot);
366
386
  if (!(await deps.appSettings.getPreferences()).autoMemoryEnabled && !preserveDisabledReview(state)) {
@@ -522,12 +542,19 @@ export function createAutoMemoryService(deps) {
522
542
  statusCode: 409
523
543
  });
524
544
  }
545
+ if (state.assignments.length > 0) {
546
+ throw new VcmError({
547
+ code: "DURABLE_DOC_ASSIGNMENTS_FAILED",
548
+ message: "Memory was already applied. Retry the failed durable-document assignments individually.",
549
+ statusCode: 409
550
+ });
551
+ }
525
552
  await clearActiveState(taskRepoRoot);
526
553
  return getState(baseRepoRoot, taskRepoRoot);
527
554
  }
528
555
  async function retryDurableDocAssignment(baseRepoRoot, taskRepoRoot, assignmentId) {
529
556
  const state = await loadActiveState(taskRepoRoot);
530
- const assignment = state?.status === "documenting"
557
+ const assignment = state && (state.status === "documenting" || state.status === "failed")
531
558
  ? state.assignments.find((candidate) => candidate.id === assignmentId)
532
559
  : undefined;
533
560
  if (!state || !assignment || assignment.status !== "failed") {
@@ -539,11 +566,10 @@ export function createAutoMemoryService(deps) {
539
566
  }
540
567
  assignment.status = assignment.owner ? "pending" : "waiting-owner";
541
568
  assignment.updatedAt = now();
542
- delete assignment.error;
543
569
  delete assignment.requestedOwner;
544
570
  delete assignment.dispatchedAt;
545
- delete assignment.baseCommit;
546
- delete assignment.reportHashBefore;
571
+ state.status = "documenting";
572
+ delete state.error;
547
573
  await persistMemoryAssignmentState(taskRepoRoot, state);
548
574
  await dispatchCurrentDurableDocAssignment(baseRepoRoot, taskRepoRoot, state);
549
575
  return getState(baseRepoRoot, taskRepoRoot);
@@ -620,6 +646,7 @@ export function createAutoMemoryService(deps) {
620
646
  assignment.error = `${input.role} durable-document assignment turn failed.`;
621
647
  assignment.updatedAt = now();
622
648
  await persistMemoryAssignmentState(input.taskRepoRoot, state);
649
+ await dispatchCurrentDurableDocAssignment(input.baseRepoRoot, input.taskRepoRoot, state);
623
650
  return true;
624
651
  }
625
652
  if (input.eventName !== "Stop") {
@@ -644,13 +671,6 @@ export function createAutoMemoryService(deps) {
644
671
  assignment.updatedAt = assignment.completedAt;
645
672
  delete assignment.error;
646
673
  await persistMemoryAssignmentState(input.taskRepoRoot, state);
647
- const next = currentDurableDocAssignment(state);
648
- if (next) {
649
- await dispatchCurrentDurableDocAssignment(input.baseRepoRoot, input.taskRepoRoot, state);
650
- }
651
- else {
652
- await clearActiveState(input.taskRepoRoot);
653
- }
654
674
  }
655
675
  catch (error) {
656
676
  assignment.status = "failed";
@@ -658,11 +678,13 @@ export function createAutoMemoryService(deps) {
658
678
  assignment.updatedAt = now();
659
679
  await persistMemoryAssignmentState(input.taskRepoRoot, state);
660
680
  }
681
+ await dispatchCurrentDurableDocAssignment(input.baseRepoRoot, input.taskRepoRoot, state);
661
682
  return true;
662
683
  }
663
684
  async function dispatchCurrentDurableDocAssignment(baseRepoRoot, taskRepoRoot, state) {
664
- const assignment = currentDurableDocAssignment(state);
665
- if (!assignment || assignment.status === "failed") {
685
+ let assignment = currentDurableDocAssignment(state);
686
+ if (!assignment) {
687
+ await finishDurableDocAssignments(taskRepoRoot, state);
666
688
  return;
667
689
  }
668
690
  if (assignment.status === "running" || assignment.status === "resolving-owner") {
@@ -678,28 +700,50 @@ export function createAutoMemoryService(deps) {
678
700
  assignment.updatedAt = now();
679
701
  await persistMemoryAssignmentState(taskRepoRoot, state);
680
702
  }
681
- const role = assignment.owner ?? "project-manager";
682
- try {
683
- const session = await ensureWorkflowRoleSession(baseRepoRoot, state.taskSlug, role);
684
- if (session.activityStatus === "running") {
703
+ while (assignment) {
704
+ try {
705
+ await dispatchDurableDocAssignment(baseRepoRoot, taskRepoRoot, state, assignment);
685
706
  return;
686
707
  }
687
- assignment.status = assignment.owner ? "running" : "resolving-owner";
708
+ catch (error) {
709
+ assignment.status = "failed";
710
+ assignment.error = `Unable to dispatch durable-document assignment: ${errorMessage(error)}`;
711
+ assignment.updatedAt = now();
712
+ await persistMemoryAssignmentState(taskRepoRoot, state);
713
+ assignment = currentDurableDocAssignment(state);
714
+ }
715
+ }
716
+ await finishDurableDocAssignments(taskRepoRoot, state);
717
+ }
718
+ async function dispatchDurableDocAssignment(baseRepoRoot, taskRepoRoot, state, assignment) {
719
+ const session = await ensureWorkflowRoleSession(baseRepoRoot, state.taskSlug, assignment.owner ?? "project-manager");
720
+ if (session.activityStatus === "running") {
721
+ return;
722
+ }
723
+ assignment.status = assignment.owner ? "running" : "resolving-owner";
724
+ assignment.reportPath = durableDocAssignmentReportPath(assignment.runId, assignment.id);
725
+ if (!assignment.baseCommit) {
688
726
  assignment.baseCommit = await deps.git.getHeadCommit(taskRepoRoot);
689
727
  assignment.reportHashBefore = await hashOptionalFile(taskRepoRoot, assignment.reportPath);
690
- assignment.dispatchedAt = now();
691
- assignment.updatedAt = assignment.dispatchedAt;
692
- await persistMemoryAssignmentState(taskRepoRoot, state);
693
- await submitTerminalInput(deps.runtime, session.id, assignment.owner
694
- ? buildDurableDocAssignmentPrompt(taskRepoRoot, assignment)
695
- : buildDurableDocOwnerPrompt(taskRepoRoot, assignment));
696
728
  }
697
- catch (error) {
698
- assignment.status = "failed";
699
- assignment.error = `Unable to dispatch durable-document assignment: ${errorMessage(error)}`;
700
- assignment.updatedAt = now();
729
+ assignment.dispatchedAt = now();
730
+ assignment.updatedAt = assignment.dispatchedAt;
731
+ await persistMemoryAssignmentState(taskRepoRoot, state);
732
+ await submitTerminalInput(deps.runtime, session.id, assignment.owner
733
+ ? buildDurableDocAssignmentPrompt(taskRepoRoot, assignment)
734
+ : buildDurableDocOwnerPrompt(taskRepoRoot, assignment));
735
+ }
736
+ async function finishDurableDocAssignments(taskRepoRoot, state) {
737
+ const failed = state.assignments.filter((assignment) => assignment.status === "failed");
738
+ if (failed.length > 0) {
739
+ state.status = "failed";
740
+ state.error = `Memory was applied, but ${failed.length} durable-document assignment(s) failed: ${failed.map((assignment) => assignment.targetPath).join(", ")}. Retry the failed assignments; their content is retained in the memory review run.`;
701
741
  await persistMemoryAssignmentState(taskRepoRoot, state);
742
+ return;
702
743
  }
744
+ delete state.error;
745
+ await persistMemoryAssignmentState(taskRepoRoot, state);
746
+ await clearActiveState(taskRepoRoot);
703
747
  }
704
748
  async function validateDurableDocAssignmentCompletion(taskRepoRoot, assignment) {
705
749
  const reportAbsolutePath = resolveRepoPath(taskRepoRoot, assignment.reportPath);
@@ -727,22 +771,46 @@ export function createAutoMemoryService(deps) {
727
771
  if (decision !== "synced" && decision !== "unchanged") {
728
772
  throw new Error("Decision must be synced or unchanged");
729
773
  }
774
+ if (!(await deps.fs.pathExists(resolveRepoPath(taskRepoRoot, assignment.targetPath)))) {
775
+ throw new Error(`target document is missing: ${assignment.targetPath}`);
776
+ }
730
777
  const currentHead = await deps.git.getHeadCommit(taskRepoRoot);
731
778
  if (decision === "synced") {
732
- if (!assignment.baseCommit || currentHead === assignment.baseCommit) {
733
- throw new Error("documentation changes were not committed");
734
- }
735
- const changedPaths = await deps.git.getChangedPaths(taskRepoRoot, assignment.baseCommit, currentHead);
736
- if (!changedPaths.includes(assignment.targetPath)) {
737
- throw new Error(`documentation commit does not include ${assignment.targetPath}`);
738
- }
739
- const reportedCommit = readArtifactSectionValue(reportContent, "Commit")?.trim();
740
- if (!reportedCommit || reportedCommit === "TBD" || !currentHead.startsWith(reportedCommit)) {
741
- throw new Error(`Commit must identify the current documentation commit ${currentHead}`);
742
- }
779
+ return validateDurableDocCommit(taskRepoRoot, assignment, reportContent, currentHead);
743
780
  }
744
781
  return currentHead;
745
782
  }
783
+ async function validateDurableDocCommit(taskRepoRoot, assignment, reportContent, currentHead) {
784
+ const reportedCommit = readArtifactSectionContent(reportContent, "Commit");
785
+ let commit;
786
+ try {
787
+ commit = (await deps.git.getCommitInfo(taskRepoRoot, reportedCommit)).sha;
788
+ }
789
+ catch (error) {
790
+ const detail = error instanceof VcmError && error.hint ? `${errorMessage(error)} ${error.hint}` : errorMessage(error);
791
+ throw new Error(`Commit ${reportedCommit} could not be resolved to a unique Git commit: ${detail}`);
792
+ }
793
+ const existingContentHint = `If ${assignment.targetPath} already preserves the assigned content, report unchanged with verification evidence; do not create a redundant commit.`;
794
+ if (!assignment.baseCommit) {
795
+ throw new Error(`Assignment ${assignment.id} has no recorded commit baseline; cannot validate Commit ${commit}. ${existingContentHint}`);
796
+ }
797
+ if (currentHead === assignment.baseCommit) {
798
+ throw new Error(`No commits follow assignment baseline ${assignment.baseCommit} (HEAD is the same commit); received Commit ${commit}. The document may already have been committed before this baseline. ${existingContentHint}`);
799
+ }
800
+ const commits = await deps.git.getCommitList(taskRepoRoot, `${assignment.baseCommit}..${currentHead}`);
801
+ if (!commits.some((candidate) => candidate.sha === commit)) {
802
+ throw new Error(`Commit ${commit} exists but is outside assignment range ${assignment.baseCommit}..${currentHead}. ${existingContentHint}`);
803
+ }
804
+ const commitPaths = await deps.git.getChangedPaths(taskRepoRoot, `${commit}^`, commit);
805
+ if (!commitPaths.includes(assignment.targetPath)) {
806
+ throw new Error(`reported commit ${commit} does not modify ${assignment.targetPath}`);
807
+ }
808
+ const changedPaths = await deps.git.getChangedPaths(taskRepoRoot, assignment.baseCommit, currentHead);
809
+ if (!changedPaths.includes(assignment.targetPath)) {
810
+ throw new Error(`Commit ${commit} modified ${assignment.targetPath}, but no net change remains in ${assignment.baseCommit}..${currentHead}. Verify the current document before reporting completion.`);
811
+ }
812
+ return commit;
813
+ }
746
814
  async function hashOptionalFile(taskRepoRoot, relativePath) {
747
815
  const absolutePath = resolveRepoPath(taskRepoRoot, relativePath);
748
816
  return await deps.fs.pathExists(absolutePath)
@@ -756,6 +824,7 @@ export function createAutoMemoryService(deps) {
756
824
  await persistRun(taskRepoRoot, {
757
825
  ...run,
758
826
  assignments: state.assignments,
827
+ error: state.error,
759
828
  updatedAt: state.updatedAt
760
829
  });
761
830
  }
@@ -897,7 +966,7 @@ export function createAutoMemoryService(deps) {
897
966
  evidence: assignment.evidence,
898
967
  ...(owner ? { owner } : {}),
899
968
  status: owner ? "pending" : "waiting-owner",
900
- reportPath: ".ai/vcm/handoffs/docs-update-report.md",
969
+ reportPath: durableDocAssignmentReportPath(state.runId, `${state.runId}-doc-${index + 1}`),
901
970
  createdAt: timestamp,
902
971
  updatedAt: timestamp
903
972
  };
@@ -1317,20 +1386,21 @@ export function createAutoMemoryService(deps) {
1317
1386
  return `sha256:${sha256(content)}`;
1318
1387
  }
1319
1388
  return {
1320
- reconcileTask,
1389
+ reconcileTask: (input) => withTaskLock(input.taskRepoRoot, () => reconcileTask(input)),
1321
1390
  getState,
1322
1391
  getTaskRetrospectiveReadiness,
1323
1392
  getFile,
1324
1393
  updateFile,
1325
1394
  revertRun,
1326
1395
  retryFailedReview,
1327
- retryDurableDocAssignment,
1328
- resolveDurableDocAssignmentOwner,
1396
+ retryDurableDocAssignment: (base, task, id) => withTaskLock(task, () => retryDurableDocAssignment(base, task, id)),
1397
+ resolveDurableDocAssignmentOwner: (base, task, id, owner) => withTaskLock(task, () => resolveDurableDocAssignmentOwner(base, task, id, owner)),
1329
1398
  prepareTaskRetrospectiveReview,
1330
1399
  cancelTaskRetrospectiveReview,
1331
1400
  isRoleMemoryTurn,
1332
- handleRoleHook,
1333
- handleHarnessEngineerHook,
1401
+ getDurableDocAssignment,
1402
+ handleRoleHook: (input) => withTaskLock(input.taskRepoRoot, () => handleRoleHook(input)),
1403
+ handleHarnessEngineerHook: (input) => withTaskLock(input.taskRepoRoot, () => handleHarnessEngineerHook(input)),
1334
1404
  assertHarnessEngineerAvailable
1335
1405
  };
1336
1406
  }
@@ -1350,11 +1420,12 @@ function currentDraft(state) {
1350
1420
  return state.drafts.find((draft) => draft.status !== "completed");
1351
1421
  }
1352
1422
  function preserveDisabledReview(state) {
1353
- return state?.status === "documenting"
1423
+ return Boolean(state?.assignments.length)
1354
1424
  || (state?.status === "reviewing" && Boolean(state.reviewPromptDispatchedAt));
1355
1425
  }
1356
1426
  function currentDurableDocAssignment(state) {
1357
- return state.assignments.find((assignment) => assignment.status !== "completed");
1427
+ return state.assignments.find((assignment) => assignment.status === "running" || assignment.status === "resolving-owner")
1428
+ ?? state.assignments.find((assignment) => assignment.status === "pending" || assignment.status === "waiting-owner");
1358
1429
  }
1359
1430
  function toReviewCandidates(source, currentRole, items) {
1360
1431
  return items.map((item) => ({
@@ -1435,7 +1506,12 @@ function buildDurableDocAssignmentPrompt(taskRepoRoot, assignment) {
1435
1506
  ...assignment.evidence.map((item) => `- ${item}`),
1436
1507
  "",
1437
1508
  "Verify the content, update the target and any directly related durable documentation, run applicable documentation checks, and commit the documentation changes.",
1438
- "Submit .ai/vcm/handoffs/docs-update-report.md through vcm-artifact. Set its Assignment ID to the exact ID above and record the final commit.",
1509
+ `Assigned report path: ${assignment.reportPath}`,
1510
+ `Submit with .ai/tools/vcm-artifact docs-update-report --file <candidate> --path ${assignment.reportPath} --mode final`,
1511
+ "Set Assignment ID to the exact ID above.",
1512
+ DOCS_UPDATE_COMMIT_RULE,
1513
+ "For synced, record the commit that changed the target document; it need not be HEAD. If the document already preserves the content, use unchanged with evidence; do not create a redundant commit.",
1514
+ ...(assignment.error ? [`Previous failure: ${assignment.error}`] : []),
1439
1515
  "End the turn after VCM accepts the report."
1440
1516
  ].join("\n");
1441
1517
  }
@@ -160,6 +160,7 @@ export function createClaudeHookService(deps) {
160
160
  taskSlug: activeTask.taskSlug,
161
161
  eventName,
162
162
  memoryReviewStatus: memoryState?.status ?? "idle",
163
+ memoryReviewHasAssignments: Boolean(memoryState?.active?.assignments.length),
163
164
  memoryReviewError: memoryState?.active?.error
164
165
  })
165
166
  : false;
@@ -164,6 +164,7 @@ export function createHarnessFeedbackService(deps) {
164
164
  await persistTaskRetrospectiveMarker(repoRoot, {
165
165
  ...processedMarker,
166
166
  status: "failed",
167
+ durableDocFailure: input.memoryReviewHasAssignments,
167
168
  failedAt: timestamp,
168
169
  updatedAt: timestamp,
169
170
  error: input.memoryReviewError
@@ -185,17 +186,26 @@ export function createHarnessFeedbackService(deps) {
185
186
  }
186
187
  async function completeWaitingTaskRetrospective(repoRoot, taskSlug, memoryStatus) {
187
188
  const marker = await loadTaskRetrospectiveMarker(repoRoot, taskSlug);
188
- if (!marker || marker.status !== "waiting-docs") {
189
+ if (!marker || (marker.status !== "waiting-docs" && !(marker.status === "failed" && marker.durableDocFailure))) {
189
190
  return false;
190
191
  }
191
192
  if (memoryStatus === "documenting") {
193
+ if (marker.status === "failed") {
194
+ await persistTaskRetrospectiveMarker(repoRoot, {
195
+ ...marker, status: "waiting-docs", updatedAt: now(), error: undefined, failedAt: undefined
196
+ });
197
+ }
192
198
  return true;
193
199
  }
194
200
  if (memoryStatus === "failed") {
201
+ if (marker.status === "failed") {
202
+ return true;
203
+ }
195
204
  const timestamp = now();
196
205
  await persistTaskRetrospectiveMarker(repoRoot, {
197
206
  ...marker,
198
207
  status: "failed",
208
+ durableDocFailure: true,
199
209
  failedAt: timestamp,
200
210
  updatedAt: timestamp,
201
211
  error: "Task Harness Retrospective durable-document assignment failed."
@@ -214,6 +224,9 @@ export function createHarnessFeedbackService(deps) {
214
224
  await persistTaskRetrospectiveMarker(repoRoot, {
215
225
  ...processedMarker,
216
226
  status: "completed",
227
+ durableDocFailure: undefined,
228
+ error: undefined,
229
+ failedAt: undefined,
217
230
  completedAt: timestamp,
218
231
  updatedAt: timestamp
219
232
  });
@@ -6,6 +6,9 @@ const MEMORY_REVIEW_ROLE_DRAFT_PATTERN = new RegExp(`^${escapeRegExp(MEMORY_REVI
6
6
  export function memoryReviewRoleDraftPath(runId, role) {
7
7
  return `${MEMORY_REVIEW_RUNS_ROOT}/${runId}/drafts/${role}.md`;
8
8
  }
9
+ export function durableDocAssignmentReportPath(runId, assignmentId) {
10
+ return `${MEMORY_REVIEW_RUNS_ROOT}/${runId}/assignments/${assignmentId}/report.md`;
11
+ }
9
12
  export function isMemoryProposalSubmissionPath(artifactPath, role) {
10
13
  if (artifactPath === ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH) {
11
14
  return role === "architect";
@@ -1,4 +1,4 @@
1
- import { ARCHITECT_DEBUG_DISPOSITIONS, ARCHITECT_DEBUG_STATUSES, ARCHITECTURE_BRIEF_STATUSES, ARCHITECTURE_DIAGNOSIS_DISPOSITIONS, ARCHITECTURE_EVIDENCE_STATUSES, ARCHITECTURE_PLAN_RESULTS, CODER_COMPLETION_DECISIONS, DOCS_REPORT_DECISIONS, DOCS_SYNC_CORRECTION_OWNERS, FINAL_ACCEPTANCE_DECISIONS, L3_ACTIONS, L3_REQUIRED_VALUES, PLANNING_PROGRESS_STATUSES, STRICT_NONE_VALUE, TEST_INFRASTRUCTURE_STATUSES, TEST_RESULTS, renderArtifactOptions } from "../../shared/validation/artifact-contract.js";
1
+ import { ARCHITECT_DEBUG_DISPOSITIONS, ARCHITECT_DEBUG_STATUSES, ARCHITECTURE_BRIEF_STATUSES, ARCHITECTURE_DIAGNOSIS_DISPOSITIONS, ARCHITECTURE_EVIDENCE_STATUSES, ARCHITECTURE_PLAN_RESULTS, CODER_COMPLETION_DECISIONS, DOCS_REPORT_DECISIONS, DOCS_UPDATE_COMMIT_RULE, DOCS_SYNC_CORRECTION_OWNERS, FINAL_ACCEPTANCE_DECISIONS, L3_ACTIONS, L3_REQUIRED_VALUES, PLANNING_PROGRESS_STATUSES, STRICT_NONE_VALUE, TEST_INFRASTRUCTURE_STATUSES, TEST_RESULTS, renderArtifactOptions } from "../../shared/validation/artifact-contract.js";
2
2
  const CURRENT_HANDOFF_NOTICE = "<!-- VCM current handoff: replace this file with one complete, self-contained snapshot of the current result. Restate all still-relevant evidence; do not refer to a prior revision, route message, Session, or transcript as evidence. -->";
3
3
  export function renderArchitectureBriefTemplate(taskSlug) {
4
4
  return `# Architecture Brief: ${taskSlug}
@@ -534,6 +534,8 @@ export function renderDocsUpdateReportTemplate(taskSlug, assignmentId = "docs-on
534
534
 
535
535
  ${CURRENT_HANDOFF_NOTICE}
536
536
 
537
+ ${DOCS_UPDATE_COMMIT_RULE}
538
+
537
539
  ## Summary
538
540
 
539
541
  TBD
@@ -301,7 +301,8 @@ Each rewritten \`architecture-diagnosis.md\` must be a complete, self-contained
301
301
  - For a \`[VCM Durable Documentation Assignment]\`, update the assigned durable
302
302
  document and directly related documentation, run applicable documentation
303
303
  checks, commit the result, and submit \`docs-update-report.md\` with the exact
304
- assigned Assignment ID. This backend assignment does not start Docs-Only Flow.
304
+ assigned Assignment ID. Use \`vcm-artifact docs-update-report --path <assigned-report-path>\`;
305
+ do not use the shared Docs-Only report path. This backend assignment does not start Docs-Only Flow.
305
306
 
306
307
  - In Docs-Only Flow, verify claims against current code and durable docs, update the PM-assigned project documents and related documentation needed for consistency, run applicable documentation checks, commit the changes, and submit \`.ai/vcm/handoffs/docs-update-report.md\` through \`vcm-artifact\` before reporting to PM.
307
308
  - In Code-Change Flow, Architect Debug Flow, and a code-producing Architecture Diagnosis Flow, perform post-validation docs sync only when project-manager requests it after tester completes.
@@ -82,7 +82,8 @@ ${renderRoleMemoryRules("coder")}
82
82
  - For a \`[VCM Durable Documentation Assignment]\`, verify and update the assigned
83
83
  durable document and directly related documentation, run applicable checks,
84
84
  commit the result, and submit \`docs-update-report.md\` with the exact assigned
85
- Assignment ID. This backend assignment does not start Docs-Only Flow.
85
+ Assignment ID. Use \`vcm-artifact docs-update-report --path <assigned-report-path>\`;
86
+ do not use the shared Docs-Only report path. This backend assignment does not start Docs-Only Flow.
86
87
 
87
88
  - In Docs-Only Flow, commit the documentation changes and submit \`.ai/vcm/handoffs/docs-update-report.md\` through \`vcm-artifact\` with the decision, changed and reviewed documents, evidence, checks, commit, and remaining documentation issues. Do not submit \`coder-completion.md\` for Docs-Only work.
88
89
  - Submit \`.ai/vcm/handoffs/coder-completion.md\` before routing back to project-manager: write a candidate outside \`.ai/vcm\`, then run \`.ai/tools/vcm-artifact coder-completion --file <candidate> --mode draft|final\`. This file is the complete, self-contained current implementation completion evidence, not a log. Each revision must restate every Scaffold Manifest disposition, changed file, helper, deviation, generated-context result, baseline-test change, L0/L1 command and result, worker result, commit, and objective failure still needed to review the current implementation without a prior revision. Replace stale content instead of appending history.
@@ -42,8 +42,8 @@ ${renderRoleMemoryRules("tester")}
42
42
  - For a \`[VCM Durable Documentation Assignment]\`, verify and update the assigned
43
43
  durable document and directly related documentation, run applicable
44
44
  documentation checks, commit the result, and submit \`docs-update-report.md\`
45
- with the exact assigned Assignment ID. This backend assignment does not start
46
- Docs-Only Flow.
45
+ with the exact assigned Assignment ID. Use \`vcm-artifact docs-update-report --path <assigned-report-path>\`;
46
+ do not use the shared Docs-Only report path. This backend assignment does not start Docs-Only Flow.
47
47
 
48
48
  - Verify the assigned documentation against current project evidence, update the required documentation, run applicable documentation checks, and commit the changes.
49
49
  - Submit \`.ai/vcm/handoffs/docs-update-report.md\` through \`vcm-artifact\` with the decision, changed and reviewed documents, evidence, checks, commit, and remaining documentation issues.
@@ -1,4 +1,4 @@
1
- import { ARCHITECT_DEBUG_DISPOSITIONS, ARCHITECT_DEBUG_STATUSES, ARCHITECTURE_BRIEF_STATUSES, ARCHITECTURE_DIAGNOSIS_DISPOSITIONS, ARCHITECTURE_EVIDENCE_STATUSES, ARCHITECTURE_PLAN_RESULTS, CODER_COMPLETION_DECISIONS, DOCS_REPORT_DECISIONS, DOCS_SYNC_CORRECTION_OWNERS, FINAL_ACCEPTANCE_DECISIONS, L3_ACTIONS, L3_REQUIRED_VALUES, PLANNING_PROGRESS_STATUSES, STRICT_NONE_VALUE, TEST_INFRASTRUCTURE_STATUSES, TEST_RESULTS } from "./artifact-contract.js";
1
+ import { ARCHITECT_DEBUG_DISPOSITIONS, ARCHITECT_DEBUG_STATUSES, ARCHITECTURE_BRIEF_STATUSES, ARCHITECTURE_DIAGNOSIS_DISPOSITIONS, ARCHITECTURE_EVIDENCE_STATUSES, ARCHITECTURE_PLAN_RESULTS, CODER_COMPLETION_DECISIONS, DOCS_REPORT_DECISIONS, DOCS_UPDATE_COMMIT_PATTERN, DOCS_UPDATE_COMMIT_RULE, DOCS_SYNC_CORRECTION_OWNERS, FINAL_ACCEPTANCE_DECISIONS, L3_ACTIONS, L3_REQUIRED_VALUES, PLANNING_PROGRESS_STATUSES, STRICT_NONE_VALUE, TEST_INFRASTRUCTURE_STATUSES, TEST_RESULTS } from "./artifact-contract.js";
2
2
  import { getArtifactDefinition } from "./artifact-registry.js";
3
3
  const PLACEHOLDER_PATTERN = /(^|\n)\s*(TBD|Not run yet\.?|status:\s*draft)\s*(\n|$)/i;
4
4
  export function checkMarkdownArtifact(kind, artifactPath, content, options = {}) {
@@ -252,7 +252,13 @@ function validateArtifactFields(kind, content, mode) {
252
252
  return invalidFields;
253
253
  }
254
254
  if (kind === "docs-update-report") {
255
- return validateDecision(content, DOCS_REPORT_DECISIONS);
255
+ const invalidFields = validateDecision(content, DOCS_REPORT_DECISIONS);
256
+ const decision = readArtifactSectionContent(content, "Decision")?.trim().toLowerCase();
257
+ const commit = readArtifactSectionContent(content, "Commit");
258
+ if (mode === "final" && decision === "synced" && !DOCS_UPDATE_COMMIT_PATTERN.test(commit ?? "")) {
259
+ invalidFields.push(`${DOCS_UPDATE_COMMIT_RULE} Received Commit: ${JSON.stringify(commit ?? "")}.`);
260
+ }
261
+ return invalidFields;
256
262
  }
257
263
  if (kind === "docs-sync-report") {
258
264
  const invalidFields = validateDecision(content, DOCS_REPORT_DECISIONS);
@@ -30,6 +30,8 @@ export const TEST_INFRASTRUCTURE_STATUSES = [
30
30
  export const L3_REQUIRED_VALUES = ["yes", "no"];
31
31
  export const L3_ACTIONS = ["run-existing", "updated", "added"];
32
32
  export const DOCS_REPORT_DECISIONS = ["synced", "unchanged", "blocked"];
33
+ export const DOCS_UPDATE_COMMIT_PATTERN = /^[0-9a-f]{7,40}$/;
34
+ export const DOCS_UPDATE_COMMIT_RULE = "When Decision is synced, Commit must contain only one bare lowercase hexadecimal commit hash (7-40 characters, for example abc1234) on a single line. No backticks, bullets, links, subjects, or extra lines; put explanations in Evidence Reviewed. An unchanged result does not require a new commit.";
33
35
  export const DOCS_SYNC_CORRECTION_OWNERS = ["none", "architect", "coder", "tester"];
34
36
  export const FINAL_ACCEPTANCE_DECISIONS = [
35
37
  "accepted",