vibe-coding-master 0.9.7 → 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,
@@ -22,6 +22,9 @@ const ARTIFACT_PATH_KEYS = [
22
22
  ["workflow-progress", "workflowProgressPath"],
23
23
  ["final-acceptance", "finalAcceptancePath"]
24
24
  ];
25
+ // Gate requests whose report VCM still waits for; any other status means the
26
+ // run already ended and a report would be ignored.
27
+ const OPEN_GATE_REQUEST_STATUSES = new Set(["requested", "running"]);
25
28
  const ROLE_COMMAND_PLACEHOLDER_PATTERN = /(^|\n)\s*(TBD|status:\s*draft)\s*(\n|$)/i;
26
29
  const DEFAULT_MESSAGE_ROUTES = [
27
30
  ["project-manager", "architect"],
@@ -217,6 +220,16 @@ export function createArtifactService(fs, deps = {}) {
217
220
  }
218
221
  }
219
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
+ }
220
233
  throw artifactRejected(input.kind, ["--path is not allowed for fixed artifact kinds."]);
221
234
  }
222
235
  const artifactPath = assignment?.reportPath ?? path.posix.join(input.handoffDir, definition.fileName ?? "");
@@ -331,6 +344,11 @@ async function validateDynamicArtifact(fs, input, content, workflowControlServic
331
344
  `Report path must match the assigned request path ${request.reportPath ?? "<missing>"}.`
332
345
  ]);
333
346
  }
347
+ if (request.status && !OPEN_GATE_REQUEST_STATUSES.has(request.status)) {
348
+ throw artifactRejected(input.kind, [
349
+ `Gate request ${request.requestId ?? requestId} is already ${request.status}, so VCM will not use this report. Stop this review; VCM sends a new request if the gate runs again.`
350
+ ]);
351
+ }
334
352
  assertManagedArtifactContent(input.kind, content, artifactPath, input.mode, {
335
353
  expectedGate: request.gate,
336
354
  expectedRequestId: request.requestId ?? requestId
@@ -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
@@ -1029,7 +1029,11 @@ function normalizeDiagnosticString(value, maxLength) {
1029
1029
  function stringOrUndefined(value) {
1030
1030
  return typeof value === "string" ? value : undefined;
1031
1031
  }
1032
+ // VCM submits its prompts as a terminal paste, and newer Claude Code versions
1033
+ // report a pasted prompt wrapped in <pasted_content id="..."> tags, so the
1034
+ // wrapper is removed before looking for VCM's marker.
1035
+ const PASTED_CONTENT_OPEN_TAG = /^<pasted_content\b[^>]*>/i;
1032
1036
  function isDirectUserPrompt(prompt) {
1033
- const normalized = prompt.trim();
1037
+ const normalized = prompt.trim().replace(PASTED_CONTENT_OPEN_TAG, "").trim();
1034
1038
  return normalized.length > 0 && !/^\[VCM(?:\s|\])/i.test(normalized);
1035
1039
  }
@@ -15,6 +15,9 @@ const GATE_REVIEW_VERSION = 1;
15
15
  const HARNESS_COMMIT_PREFIX = "[VCM Harness] ";
16
16
  const REVIEWER_ROLE = "reviewer";
17
17
  const DEFAULT_REPORT_POLL_INTERVAL_MS = 1000;
18
+ // A Reviewer that just submitted a report usually ends its turn within
19
+ // seconds; a missed Stop hook leaves the activity stale, so the wait is capped.
20
+ const DEFAULT_REVIEWER_IDLE_WAIT_MS = 5 * 60 * 1000;
18
21
  const activeRuns = new Map();
19
22
  const gateStateLocks = new Map();
20
23
  const SOURCE_ARTIFACTS = {
@@ -35,6 +38,14 @@ const SOURCE_ARTIFACTS = {
35
38
  ".ai/vcm/gate-reviews/validation-adequacy-review.md"
36
39
  ]
37
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
+ ]);
38
49
  const CODE_DIFF_SOURCE_ARTIFACTS = {
39
50
  coder: [
40
51
  ".ai/vcm/handoffs/architecture-plan.md",
@@ -54,6 +65,7 @@ const VALID_CODE_DIFF_FINDING_SCOPES = new Set(CODE_DIFF_FINDING_SCOPES);
54
65
  export function createGateReviewService(deps) {
55
66
  const now = deps.now ?? (() => new Date().toISOString());
56
67
  const reportPollIntervalMs = deps.reportPollIntervalMs ?? DEFAULT_REPORT_POLL_INTERVAL_MS;
68
+ const reviewerIdleWaitMs = deps.reviewerIdleWaitMs ?? DEFAULT_REVIEWER_IDLE_WAIT_MS;
57
69
  async function getContext(repoRoot, taskSlug) {
58
70
  const projectConfig = await deps.projectService.loadConfig(repoRoot);
59
71
  const task = await deps.taskService.loadTask(repoRoot, taskSlug);
@@ -529,6 +541,7 @@ export function createGateReviewService(deps) {
529
541
  });
530
542
  }
531
543
  const session = await ensureReviewerSession(context);
544
+ await waitForReviewerIdle(context, controller.signal);
532
545
  throwIfGateRunCancelled(controller.signal);
533
546
  await submitTerminalInput(deps.runtime, session.id, prompt);
534
547
  await deps.sessionService.markRoleActivityRunning(context.repoRoot, context.taskSlug, REVIEWER_ROLE, session.id);
@@ -647,6 +660,20 @@ export function createGateReviewService(deps) {
647
660
  return next.gates[gate];
648
661
  });
649
662
  }
663
+ // Claude Code queues a prompt typed into a running turn but sends its
664
+ // UserPromptSubmit hook at once, so the running turn's Stop would look like
665
+ // the end of this request's turn and fail it. Wait for that turn to end.
666
+ async function waitForReviewerIdle(context, signal) {
667
+ const deadline = Date.now() + reviewerIdleWaitMs;
668
+ while (true) {
669
+ throwIfGateRunCancelled(signal);
670
+ const session = await deps.sessionService.getRoleSession(context.repoRoot, context.taskSlug, REVIEWER_ROLE);
671
+ if (session?.activityStatus !== "running" || Date.now() >= deadline) {
672
+ return;
673
+ }
674
+ await delay(reportPollIntervalMs);
675
+ }
676
+ }
650
677
  async function ensureReviewerSession(context) {
651
678
  const existing = await deps.sessionService.getRoleSession(context.repoRoot, context.taskSlug, REVIEWER_ROLE);
652
679
  if (existing?.status === "running" && deps.runtime.getSession(existing.id)) {
@@ -1260,7 +1287,7 @@ async function computeInputHash(deps, taskRepoRoot, gate, codeDiffInput, codeDif
1260
1287
  ".ai/tools/request-gate-review",
1261
1288
  "docs/CODING_STANDARDS.md"
1262
1289
  ];
1263
- const sourceArtifacts = getSourceArtifacts(gate, codeDiffSources);
1290
+ const sourceArtifacts = getSourceArtifacts(gate, codeDiffSources).filter((item) => gate !== "validation-adequacy" || !VALIDATION_HASH_EXCLUDED_ARTIFACTS.has(item));
1264
1291
  for (const relativePath of new Set([...common, ...sourceArtifacts].filter((item) => item !== coreArtifact))) {
1265
1292
  digest.update(relativePath);
1266
1293
  const absolutePath = resolveRepoPath(taskRepoRoot, relativePath);
@@ -833,9 +833,12 @@ async function allowedArchitectFix(fs, input, state, segment, source, resumedFro
833
833
  return [`${source}/architect`];
834
834
  }
835
835
  // A new fix needs validation; any other post-validation Architect change
836
- // is reviewed by an incremental code-diff Gate.
837
- if (artifactFresh && artifact.complete)
836
+ // is reviewed by an incremental code-diff Gate. Architect also rewrites its
837
+ // artifact for a docs correction, so once a code-diff Gate has reviewed
838
+ // this dispatch, that Gate's result routes the flow instead.
839
+ if (artifactFresh && artifact.complete && !(await codeDiffReviewedDispatch(fs, input, state, source))) {
838
840
  return [`${source}/tester`];
841
+ }
839
842
  return allowedPostImplementationArchitect(fs, input, state, source, acceptanceFlow);
840
843
  }
841
844
  return allowedAfterTester(fs, input, state, source, source, {
@@ -843,6 +846,18 @@ async function allowedArchitectFix(fs, input, state, segment, source, resumedFro
843
846
  acceptanceFlow
844
847
  });
845
848
  }
849
+ // True when a code-diff Gate finished, or was skipped or overridden, after the
850
+ // active Architect dispatch was confirmed.
851
+ async function codeDiffReviewedDispatch(fs, input, state, flow) {
852
+ if (!matchingEvidenceBaseline(state, flow, "architect"))
853
+ return false;
854
+ const codeDiff = await gateState(fs, input, "code-diff");
855
+ // A disabled Gate reviews nothing, so it never stands in for Tester.
856
+ if (!codeDiff || codeDiff.status === "disabled")
857
+ return false;
858
+ return (codeDiff.status === "completed" && freshGateDecision(state, flow, "architect", "code-diff", codeDiff) !== undefined)
859
+ || gatePassedForDispatch(state, flow, "architect", "code-diff", codeDiff);
860
+ }
846
861
  async function allowedPostImplementationArchitect(fs, input, state, flow, acceptanceFlow) {
847
862
  if (flow === "code-change") {
848
863
  const planningTransition = await allowedFreshArchitecturePlan(fs, input, state, matchingEvidenceBaseline(state, "code-change", "architect")?.confirmedAt);