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.
- package/dist/backend/server.js +2 -1
- package/dist/backend/services/artifact-service.js +18 -0
- package/dist/backend/services/auto-memory-service.js +41 -1
- package/dist/backend/services/claude-hook-service.js +5 -1
- package/dist/backend/services/gate-review-service.js +28 -1
- package/dist/backend/services/workflow-control-service.js +17 -2
- package/dist-frontend/assets/{index-Ct8NwH3l.js → index-DG2WfNXT.js} +36 -36
- package/dist-frontend/index.html +1 -1
- package/package.json +1 -1
package/dist/backend/server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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);
|