vibe-coding-master 0.9.8 → 0.9.9

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.
@@ -275,7 +275,8 @@ export function createDefaultServerDeps(options = {}) {
275
275
  const workflowControlService = createWorkflowControlService({ fs });
276
276
  const artifactService = createArtifactService(fs, {
277
277
  workflowControlService,
278
- getDurableDocAssignment: (taskRepoRoot, role) => autoMemoryService.getDurableDocAssignment(taskRepoRoot, role)
278
+ getDurableDocAssignment: (taskRepoRoot, role) => autoMemoryService.getDurableDocAssignment(taskRepoRoot, role),
279
+ getCompletedDurableDocAssignment: (taskRepoRoot, role, artifactPath) => autoMemoryService.getCompletedDurableDocAssignment(taskRepoRoot, role, artifactPath)
279
280
  });
280
281
  const projectService = createProjectService({
281
282
  fs,
@@ -220,6 +220,16 @@ export function createArtifactService(fs, deps = {}) {
220
220
  }
221
221
  }
222
222
  else if (input.artifactPath?.trim()) {
223
+ const completed = input.kind === "docs-update-report"
224
+ ? await deps.getCompletedDurableDocAssignment?.(input.repoRoot, input.role, input.artifactPath.trim())
225
+ : undefined;
226
+ if (completed) {
227
+ throw new VcmError({
228
+ code: "DURABLE_DOC_ASSIGNMENT_COMPLETED",
229
+ message: `Durable-documentation assignment ${completed.id} is already completed${completed.commit ? ` (commit ${completed.commit})` : ""}, and VCM accepted its report. No action is needed; end the turn.`,
230
+ statusCode: 409
231
+ });
232
+ }
223
233
  throw artifactRejected(input.kind, ["--path is not allowed for fixed artifact kinds."]);
224
234
  }
225
235
  const artifactPath = assignment?.reportPath ?? path.posix.join(input.handoffDir, definition.fileName ?? "");
@@ -18,6 +18,7 @@ const MEMORY_FILE_DEFINITIONS = [
18
18
  { path: ".claude/agents/reviewer.md", title: "Reviewer Memory", role: "reviewer" },
19
19
  { path: ".claude/agents/harness-engineer.md", title: "Harness Engineer Memory", role: "harness-engineer" }
20
20
  ];
21
+ const DURABLE_DOC_TURN_END_GRACE_MS = 30 * 1000;
21
22
  export function createAutoMemoryService(deps) {
22
23
  const now = deps.now ?? (() => new Date().toISOString());
23
24
  const taskLocks = new Map();
@@ -382,6 +383,24 @@ export function createAutoMemoryService(deps) {
382
383
  const assignment = state?.status === "documenting" ? currentDurableDocAssignment(state) : undefined;
383
384
  return assignment?.status === "running" && assignment.owner === role ? assignment : undefined;
384
385
  }
386
+ async function getCompletedDurableDocAssignment(taskRepoRoot, role, artifactPath) {
387
+ const runId = artifactPath.startsWith(`${MEMORY_REVIEW_RUNS_ROOT}/`)
388
+ ? artifactPath.slice(MEMORY_REVIEW_RUNS_ROOT.length + 1).split("/")[0]
389
+ : undefined;
390
+ if (!runId) {
391
+ return undefined;
392
+ }
393
+ let run;
394
+ try {
395
+ run = await readRun(taskRepoRoot, runId);
396
+ }
397
+ catch {
398
+ return undefined;
399
+ }
400
+ return run.assignments?.find((assignment) => assignment.status === "completed"
401
+ && assignment.owner === role
402
+ && assignment.reportPath === artifactPath);
403
+ }
385
404
  async function handleRoleHook(input) {
386
405
  const state = await loadActiveState(input.taskRepoRoot);
387
406
  if (!(await deps.appSettings.getPreferences()).autoMemoryEnabled && !preserveDisabledReview(state)) {
@@ -764,7 +783,12 @@ export function createAutoMemoryService(deps) {
764
783
  return;
765
784
  }
766
785
  const existing = await deps.sessionService.getRoleSession(baseRepoRoot, state.taskSlug, role);
767
- if (existing?.activityStatus === "running" && deps.runtime.getSession(existing.id)) {
786
+ // The assignment's turn reports its own end through the Stop hook, so a
787
+ // live session keeps the assignment. Sending it again here would queue a
788
+ // duplicate prompt behind that turn.
789
+ if (existing?.status === "running"
790
+ && deps.runtime.getSession(existing.id)
791
+ && !turnInterruptedAfterDispatch(existing, assignment)) {
768
792
  return;
769
793
  }
770
794
  assignment.status = assignment.owner ? "pending" : "waiting-owner";
@@ -803,6 +827,21 @@ export function createAutoMemoryService(deps) {
803
827
  await submitTerminalInput(deps.runtime, session.id, assignment.owner
804
828
  ? buildDurableDocAssignmentPrompt(taskRepoRoot, assignment)
805
829
  : buildDurableDocOwnerPrompt(taskRepoRoot, assignment));
830
+ // Until the role's UserPromptSubmit hook arrives the session still looks
831
+ // idle; mark it running so nothing dispatches into it meanwhile.
832
+ await deps.sessionService.markRoleActivityRunning(baseRepoRoot, state.taskSlug, assignment.owner ?? "project-manager", session.id);
833
+ }
834
+ // An interrupted turn sends no Stop hook, so its assignment would otherwise
835
+ // stay running. A turn that ends normally records its end on the session
836
+ // just before its Stop hook completes or fails the assignment, so the grace
837
+ // period keeps a reconcile from racing that hook.
838
+ function turnInterruptedAfterDispatch(session, assignment) {
839
+ if (session.activityStatus === "running" || !session.lastTurnEndedAt || !assignment.dispatchedAt) {
840
+ return false;
841
+ }
842
+ const endedAt = Date.parse(session.lastTurnEndedAt);
843
+ return endedAt > Date.parse(assignment.dispatchedAt)
844
+ && Date.parse(now()) - endedAt >= DURABLE_DOC_TURN_END_GRACE_MS;
806
845
  }
807
846
  async function finishDurableDocAssignments(taskRepoRoot, state) {
808
847
  const failed = state.assignments.filter((assignment) => assignment.status === "failed");
@@ -1471,6 +1510,7 @@ export function createAutoMemoryService(deps) {
1471
1510
  cancelTaskRetrospectiveReview,
1472
1511
  isRoleMemoryTurn,
1473
1512
  getDurableDocAssignment,
1513
+ getCompletedDurableDocAssignment,
1474
1514
  handleRoleHook: (input) => withTaskLock(input.taskRepoRoot, () => handleRoleHook(input)),
1475
1515
  handleHarnessEngineerHook: (input) => withTaskLock(input.taskRepoRoot, () => handleHarnessEngineerHook(input)),
1476
1516
  assertHarnessEngineerAvailable
@@ -38,6 +38,14 @@ const SOURCE_ARTIFACTS = {
38
38
  ".ai/vcm/gate-reviews/validation-adequacy-review.md"
39
39
  ]
40
40
  };
41
+ // Reviewer reads the Debug and Diagnosis handoffs, but they stay out of the
42
+ // validation-adequacy input hash: a Post-Validation Docs Correction rewrites
43
+ // them, and like docs it changes no code or test evidence. Code and test
44
+ // changes still change the tracked-evidence hash.
45
+ const VALIDATION_HASH_EXCLUDED_ARTIFACTS = new Set([
46
+ ".ai/vcm/handoffs/architect-debug.md",
47
+ ".ai/vcm/handoffs/architecture-diagnosis.md"
48
+ ]);
41
49
  const CODE_DIFF_SOURCE_ARTIFACTS = {
42
50
  coder: [
43
51
  ".ai/vcm/handoffs/architecture-plan.md",
@@ -1279,7 +1287,7 @@ async function computeInputHash(deps, taskRepoRoot, gate, codeDiffInput, codeDif
1279
1287
  ".ai/tools/request-gate-review",
1280
1288
  "docs/CODING_STANDARDS.md"
1281
1289
  ];
1282
- const sourceArtifacts = getSourceArtifacts(gate, codeDiffSources);
1290
+ const sourceArtifacts = getSourceArtifacts(gate, codeDiffSources).filter((item) => gate !== "validation-adequacy" || !VALIDATION_HASH_EXCLUDED_ARTIFACTS.has(item));
1283
1291
  for (const relativePath of new Set([...common, ...sourceArtifacts].filter((item) => item !== coreArtifact))) {
1284
1292
  digest.update(relativePath);
1285
1293
  const absolutePath = resolveRepoPath(taskRepoRoot, relativePath);