vibe-coding-master 0.7.47 → 0.7.48
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 +3 -1
- package/dist/backend/services/architect-restart-service.js +251 -78
- package/dist/backend/services/artifact-service.js +60 -131
- package/dist/backend/services/claude-hook-service.js +3 -0
- package/dist/backend/services/gate-review-service.js +42 -217
- package/dist/backend/services/harness-feedback-service.js +13 -24
- package/dist/backend/services/managed-artifact-validation.js +403 -0
- package/dist/backend/services/message-service.js +13 -64
- package/dist/backend/services/runtime-recovery-service.js +1 -0
- package/dist/backend/services/task-close-service.js +3 -1
- package/dist/backend/templates/harness/gate-review.js +13 -98
- package/dist/backend/templates/harness/vcm-report-harness-issue-skill.js +1 -1
- package/dist/shared/validation/artifact-registry.js +68 -0
- package/package.json +1 -1
- package/scripts/harness-tools/vcm-artifact +1 -34
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { DISPATCHABLE_ROLES
|
|
2
|
+
import { DISPATCHABLE_ROLES } from "../../shared/constants.js";
|
|
3
3
|
import { checkMarkdownArtifact } from "../../shared/validation/artifact-check.js";
|
|
4
|
-
import {
|
|
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
7
|
import { renderArchitectureBriefTemplate, renderArchitectureDiagnosisTemplate, renderArchitectureEvidenceTemplate, renderArchitecturePlanTemplate, renderArchitectDebugTemplate, renderCoderCompletionTemplate, renderDocsUpdateReportTemplate, renderDocsSyncReportTemplate, renderFinalAcceptanceTemplate, renderKnownIssuesTemplate, renderPlanningProgressTemplate, renderMessageRouteTemplate, renderTestReportTemplate, renderWorkflowProgressTemplate } from "../templates/handoff.js";
|
|
8
8
|
import { renderRoleCommandTemplate } from "../templates/role-command.js";
|
|
9
|
-
import { validateMemoryProposal } from "./memory-proposal-validation.js";
|
|
10
9
|
import { isMemoryProposalSubmissionPath } from "./memory-review-paths.js";
|
|
10
|
+
import { validateManagedArtifactContent } from "./managed-artifact-validation.js";
|
|
11
11
|
const ARTIFACT_PATH_KEYS = [
|
|
12
12
|
["architecture-brief", "architectureBriefPath"],
|
|
13
13
|
["architecture-evidence", "architectureEvidencePath"],
|
|
@@ -191,21 +191,33 @@ export function createArtifactService(fs, deps = {}) {
|
|
|
191
191
|
if (!normalized.trim()) {
|
|
192
192
|
throw artifactRejected(input.kind, ["Artifact content is empty."]);
|
|
193
193
|
}
|
|
194
|
+
if (!isManagedArtifactKind(input.kind)) {
|
|
195
|
+
throw new VcmError({
|
|
196
|
+
code: "ARTIFACT_KIND_INVALID",
|
|
197
|
+
message: `Unknown managed artifact kind: ${input.kind}`,
|
|
198
|
+
statusCode: 400
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
const definition = getManagedArtifactDefinition(input.kind);
|
|
202
|
+
const owners = Array.isArray(definition.owner) ? definition.owner : [definition.owner];
|
|
203
|
+
if (!owners.includes(input.role)) {
|
|
204
|
+
throw new VcmError({
|
|
205
|
+
code: "ARTIFACT_OWNER_MISMATCH",
|
|
206
|
+
message: `${input.kind} is owned by ${owners.join("|")}, not ${input.role}.`,
|
|
207
|
+
statusCode: 403
|
|
208
|
+
});
|
|
209
|
+
}
|
|
194
210
|
if (isArtifactKind(input.kind)) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (!owners.includes(input.role)) {
|
|
198
|
-
throw new VcmError({
|
|
199
|
-
code: "ARTIFACT_OWNER_MISMATCH",
|
|
200
|
-
message: `${input.kind} is owned by ${owners.join("|")}, not ${input.role}.`,
|
|
201
|
-
statusCode: 403
|
|
202
|
-
});
|
|
211
|
+
if (input.artifactPath?.trim()) {
|
|
212
|
+
throw artifactRejected(input.kind, ["--path is not allowed for fixed artifact kinds."]);
|
|
203
213
|
}
|
|
204
|
-
const artifactPath = path.posix.join(input.handoffDir, definition.fileName);
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
214
|
+
const artifactPath = path.posix.join(input.handoffDir, definition.fileName ?? "");
|
|
215
|
+
const validation = validateManagedArtifactContent(input.kind, normalized, {
|
|
216
|
+
path: artifactPath,
|
|
217
|
+
mode: input.mode
|
|
218
|
+
});
|
|
219
|
+
if (validation.errors.length > 0) {
|
|
220
|
+
throw artifactRejected(input.kind, validation.errors);
|
|
209
221
|
}
|
|
210
222
|
if (input.kind === "workflow-progress") {
|
|
211
223
|
if (input.mode !== "final") {
|
|
@@ -238,7 +250,7 @@ export function createArtifactService(fs, deps = {}) {
|
|
|
238
250
|
kind: input.kind,
|
|
239
251
|
mode: input.mode,
|
|
240
252
|
path: artifactPath,
|
|
241
|
-
status:
|
|
253
|
+
status: validation.status
|
|
242
254
|
};
|
|
243
255
|
}
|
|
244
256
|
const dynamic = await validateDynamicArtifact(fs, input, normalized, deps.workflowControlService);
|
|
@@ -253,16 +265,6 @@ export function createArtifactService(fs, deps = {}) {
|
|
|
253
265
|
}
|
|
254
266
|
};
|
|
255
267
|
}
|
|
256
|
-
function artifactCheckErrors(check, mode) {
|
|
257
|
-
const errors = [
|
|
258
|
-
...check.missingHeadings.map((heading) => `Missing required heading: ${heading}.`),
|
|
259
|
-
...check.invalidFields
|
|
260
|
-
];
|
|
261
|
-
if (mode === "final" && check.hasPlaceholder) {
|
|
262
|
-
errors.push("Final artifacts must not contain TBD, draft, or not-run placeholders.");
|
|
263
|
-
}
|
|
264
|
-
return errors;
|
|
265
|
-
}
|
|
266
268
|
function artifactRejected(kind, errors) {
|
|
267
269
|
return new VcmError({
|
|
268
270
|
code: "ARTIFACT_VALIDATION_FAILED",
|
|
@@ -272,16 +274,13 @@ function artifactRejected(kind, errors) {
|
|
|
272
274
|
});
|
|
273
275
|
}
|
|
274
276
|
async function validateDynamicArtifact(fs, input, content, workflowControlService) {
|
|
275
|
-
if (input.mode !== "final") {
|
|
276
|
-
throw artifactRejected(input.kind, ["Dynamic artifacts must be submitted in final mode."]);
|
|
277
|
-
}
|
|
278
277
|
const artifactPath = normalizeRelativeArtifactPath(input.artifactPath);
|
|
279
278
|
if (input.kind === "route-message") {
|
|
280
279
|
const route = DEFAULT_MESSAGE_ROUTES.find(([fromRole, toRole]) => artifactPath === path.posix.join(input.handoffDir, "messages", `${fromRole}-${toRole}.md`));
|
|
281
280
|
if (!route || route[0] !== input.role) {
|
|
282
281
|
throw artifactRejected(input.kind, ["Route-message path must name the submitting role as the sender."]);
|
|
283
282
|
}
|
|
284
|
-
|
|
283
|
+
assertManagedArtifactContent(input.kind, content, artifactPath, input.mode);
|
|
285
284
|
if (input.role === "project-manager") {
|
|
286
285
|
if (!workflowControlService) {
|
|
287
286
|
throw new VcmError({
|
|
@@ -302,40 +301,46 @@ async function validateDynamicArtifact(fs, input, content, workflowControlServic
|
|
|
302
301
|
return { kind: input.kind, root: input.repoRoot, path: artifactPath };
|
|
303
302
|
}
|
|
304
303
|
if (input.kind === "coder-worker-report") {
|
|
305
|
-
if (
|
|
304
|
+
if (!/^\.ai\/vcm\/coder-workers\/reports\/[A-Za-z0-9._-]+\.md$/.test(artifactPath)) {
|
|
306
305
|
throw artifactRejected(input.kind, ["Coder Worker reports must use the assigned .ai/vcm/coder-workers/reports/<worker-id>.md path."]);
|
|
307
306
|
}
|
|
308
|
-
|
|
307
|
+
assertManagedArtifactContent(input.kind, content, artifactPath, input.mode);
|
|
309
308
|
return { kind: input.kind, root: input.repoRoot, path: artifactPath };
|
|
310
309
|
}
|
|
311
310
|
if (input.kind === "gate-review-report") {
|
|
312
|
-
if (
|
|
311
|
+
if (!/^\.ai\/vcm\/gate-reviews\/requests\/[A-Za-z0-9._-]+\.report\.md$/.test(artifactPath)) {
|
|
313
312
|
throw artifactRejected(input.kind, ["Reviewer must submit the assigned request report path."]);
|
|
314
313
|
}
|
|
315
|
-
|
|
314
|
+
const requestId = path.posix.basename(artifactPath, ".report.md");
|
|
315
|
+
const requestPath = path.posix.join(".ai/vcm/gate-reviews/requests", `${requestId}.json`);
|
|
316
|
+
const absoluteRequestPath = resolveRepoPath(input.repoRoot, requestPath);
|
|
317
|
+
if (!(await fs.pathExists(absoluteRequestPath))) {
|
|
318
|
+
throw artifactRejected(input.kind, [`Gate request does not exist: ${requestPath}.`]);
|
|
319
|
+
}
|
|
320
|
+
const request = await fs.readJson(absoluteRequestPath);
|
|
321
|
+
if (request.reportPath !== artifactPath) {
|
|
322
|
+
throw artifactRejected(input.kind, [
|
|
323
|
+
`Report path must match the assigned request path ${request.reportPath ?? "<missing>"}.`
|
|
324
|
+
]);
|
|
325
|
+
}
|
|
326
|
+
assertManagedArtifactContent(input.kind, content, artifactPath, input.mode, {
|
|
327
|
+
expectedGate: request.gate,
|
|
328
|
+
expectedRequestId: request.requestId ?? requestId
|
|
329
|
+
});
|
|
316
330
|
return { kind: input.kind, root: input.repoRoot, path: artifactPath };
|
|
317
331
|
}
|
|
318
332
|
if (input.kind === "memory-proposal") {
|
|
319
|
-
if (!VCM_ROLE_NAMES.includes(input.role)) {
|
|
320
|
-
throw artifactRejected(input.kind, ["Only a VCM workflow role may submit a Memory Proposal."]);
|
|
321
|
-
}
|
|
322
333
|
if (!isMemoryProposalSubmissionPath(artifactPath, input.role)) {
|
|
323
334
|
throw artifactRejected(input.kind, ["Memory proposal path is not assigned to the submitting role."]);
|
|
324
335
|
}
|
|
325
|
-
|
|
326
|
-
if (memoryError) {
|
|
327
|
-
throw artifactRejected(input.kind, [`Memory proposal ${memoryError}.`]);
|
|
328
|
-
}
|
|
336
|
+
assertManagedArtifactContent(input.kind, content, artifactPath, input.mode);
|
|
329
337
|
return { kind: input.kind, root: input.repoRoot, path: artifactPath };
|
|
330
338
|
}
|
|
331
339
|
if (input.kind === "harness-feedback") {
|
|
332
|
-
if (!VCM_ROLE_NAMES.includes(input.role)) {
|
|
333
|
-
throw artifactRejected(input.kind, ["Only a VCM workflow role may submit Harness Feedback."]);
|
|
334
|
-
}
|
|
335
340
|
if (!/^\.ai\/vcm\/harness-feedback\/pending\/[A-Za-z0-9._-]+\.md$/.test(artifactPath)) {
|
|
336
341
|
throw artifactRejected(input.kind, ["Harness Feedback path must be under .ai/vcm/harness-feedback/pending/. "]);
|
|
337
342
|
}
|
|
338
|
-
|
|
343
|
+
assertManagedArtifactContent(input.kind, content, artifactPath, input.mode);
|
|
339
344
|
return { kind: input.kind, root: input.baseRepoRoot, path: artifactPath };
|
|
340
345
|
}
|
|
341
346
|
throw new VcmError({
|
|
@@ -344,89 +349,16 @@ async function validateDynamicArtifact(fs, input, content, workflowControlServic
|
|
|
344
349
|
statusCode: 400
|
|
345
350
|
});
|
|
346
351
|
}
|
|
347
|
-
function
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
errors.push("Implementation Result must be success|has_failed_items.");
|
|
357
|
-
}
|
|
358
|
-
for (const heading of [
|
|
359
|
-
"Assigned Scope", "Item Dispositions", "Files Changed", "Tests Added Or Updated",
|
|
360
|
-
"L0/L1 Checks", "Commit", "Skipped Assigned Checks", "Objective Failures"
|
|
361
|
-
]) {
|
|
362
|
-
if (!new RegExp(`^## ${escapeRegExp(heading)}\\s*$`, "m").test(content)) {
|
|
363
|
-
errors.push(`Missing required section: ${heading}.`);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
if (errors.length > 0)
|
|
367
|
-
throw artifactRejected("coder-worker-report", errors);
|
|
368
|
-
}
|
|
369
|
-
function validateRouteMessage(content) {
|
|
370
|
-
const type = /^type:\s*(\S+)\s*$/m.exec(content)?.[1];
|
|
371
|
-
const allowed = new Set(["task", "question", "revise", "cancel", "result", "blocked", "finding"]);
|
|
372
|
-
if (!type || !allowed.has(type)) {
|
|
373
|
-
throw artifactRejected("route-message", ["Frontmatter type must be one of task|question|revise|cancel|result|blocked|finding."]);
|
|
374
|
-
}
|
|
375
|
-
if (!content.startsWith("---\n") || !/\n---\n/.test(content) || !content.split(/\n---\n/, 2)[1]?.trim()) {
|
|
376
|
-
throw artifactRejected("route-message", ["Route message requires frontmatter and a non-empty body."]);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
async function validateGateReviewReport(fs, repoRoot, artifactPath, content) {
|
|
380
|
-
const requestId = path.posix.basename(artifactPath, ".report.md");
|
|
381
|
-
const requestPath = path.posix.join(".ai/vcm/gate-reviews/requests", `${requestId}.json`);
|
|
382
|
-
const absoluteRequestPath = resolveRepoPath(repoRoot, requestPath);
|
|
383
|
-
if (!(await fs.pathExists(absoluteRequestPath))) {
|
|
384
|
-
throw artifactRejected("gate-review-report", [`Gate request does not exist: ${requestPath}.`]);
|
|
385
|
-
}
|
|
386
|
-
const request = await fs.readJson(absoluteRequestPath);
|
|
387
|
-
const fields = Object.fromEntries(["Gate", "Request", "Decision", "Summary"].map((field) => [
|
|
388
|
-
field,
|
|
389
|
-
new RegExp(`^${field}:\\s*(.+?)\\s*$`, "mi").exec(content)?.[1]?.trim()
|
|
390
|
-
]));
|
|
391
|
-
const errors = [];
|
|
392
|
-
if (fields.Gate !== request.gate)
|
|
393
|
-
errors.push(`Gate must be ${request.gate ?? "the requested gate"}.`);
|
|
394
|
-
if (fields.Request !== request.requestId)
|
|
395
|
-
errors.push(`Request must be ${request.requestId ?? requestId}.`);
|
|
396
|
-
if (fields.Decision !== "approve" && fields.Decision !== "request_changes")
|
|
397
|
-
errors.push("Decision must be approve|request_changes.");
|
|
398
|
-
if (!fields.Summary)
|
|
399
|
-
errors.push("Summary is required.");
|
|
400
|
-
const requiredSection = request.gate === "architecture-plan"
|
|
401
|
-
? "Architecture Analysis"
|
|
402
|
-
: request.gate === "validation-adequacy"
|
|
403
|
-
? "Validation Analysis"
|
|
404
|
-
: "Code Diff Analysis";
|
|
405
|
-
if (!new RegExp(`^## ${escapeRegExp(requiredSection)}\\s*$`, "m").test(content)) {
|
|
406
|
-
errors.push(`Missing required section: ${requiredSection}.`);
|
|
407
|
-
}
|
|
408
|
-
if (!/^## Findings\s*$/m.test(content))
|
|
409
|
-
errors.push("Missing required section: Findings.");
|
|
410
|
-
if (errors.length > 0)
|
|
411
|
-
throw artifactRejected("gate-review-report", errors);
|
|
412
|
-
}
|
|
413
|
-
function validateHarnessFeedback(content) {
|
|
414
|
-
const errors = [];
|
|
415
|
-
if (!/^#\s+\S.+$/m.test(content))
|
|
416
|
-
errors.push("Harness Feedback requires a title.");
|
|
417
|
-
for (const field of [
|
|
418
|
-
"Reporter role", "Task slug", "Summary", "Observed problem", "Expected behavior",
|
|
419
|
-
"Evidence", "Suspected harness area", "Impact", "Urgency"
|
|
420
|
-
]) {
|
|
421
|
-
if (!new RegExp(`^- ${escapeRegExp(field)}:\\s*\\S`, "m").test(content)) {
|
|
422
|
-
errors.push(`Harness Feedback is missing ${field}.`);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
if (!/^- Urgency:\s*(low|medium|high)\s*$/m.test(content)) {
|
|
426
|
-
errors.push("Urgency must be low|medium|high.");
|
|
352
|
+
function assertManagedArtifactContent(kind, content, artifactPath, mode, expected = {}) {
|
|
353
|
+
const validation = validateManagedArtifactContent(kind, content, {
|
|
354
|
+
path: artifactPath,
|
|
355
|
+
mode,
|
|
356
|
+
expectedGate: expected.expectedGate,
|
|
357
|
+
expectedRequestId: expected.expectedRequestId
|
|
358
|
+
});
|
|
359
|
+
if (validation.errors.length > 0) {
|
|
360
|
+
throw artifactRejected(kind, validation.errors);
|
|
427
361
|
}
|
|
428
|
-
if (errors.length > 0)
|
|
429
|
-
throw artifactRejected("harness-feedback", errors);
|
|
430
362
|
}
|
|
431
363
|
function normalizeRelativeArtifactPath(value) {
|
|
432
364
|
const normalized = value?.replaceAll("\\", "/").replace(/^\.\//, "").trim();
|
|
@@ -445,9 +377,6 @@ async function writeAtomic(fs, absolutePath, content) {
|
|
|
445
377
|
}
|
|
446
378
|
await fs.writeText(absolutePath, content);
|
|
447
379
|
}
|
|
448
|
-
function escapeRegExp(value) {
|
|
449
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
450
|
-
}
|
|
451
380
|
function getLegacyRoleCommandPath(roleCommandsDir, role) {
|
|
452
381
|
return path.posix.join(roleCommandsDir, `${role}-command.md`);
|
|
453
382
|
}
|
|
@@ -235,6 +235,9 @@ export function createClaudeHookService(deps) {
|
|
|
235
235
|
if (!session) {
|
|
236
236
|
return completedHookResult(input, eventName);
|
|
237
237
|
}
|
|
238
|
+
if (input.role === "architect") {
|
|
239
|
+
await deps.architectRestartService?.recordReplacementPromptSubmitted(context.project.repoRoot, context.taskSlug, session.id);
|
|
240
|
+
}
|
|
238
241
|
const boundToTask = await isHookSessionBoundToTask(context, input.role);
|
|
239
242
|
if (boundToTask) {
|
|
240
243
|
deps.jobGuard?.notePromptSubmitted({
|
|
@@ -6,6 +6,7 @@ import { VcmError } from "../errors.js";
|
|
|
6
6
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
7
7
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
8
8
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
9
|
+
import { parseGateReviewReportArtifact } from "./managed-artifact-validation.js";
|
|
9
10
|
const REVIEWER_AGENT_PATH = ".claude/agents/reviewer.md";
|
|
10
11
|
const GATE_REVIEW_DIR = ".ai/vcm/gate-reviews";
|
|
11
12
|
const REQUESTS_DIR = ".ai/vcm/gate-reviews/requests";
|
|
@@ -15,49 +16,6 @@ const REVIEWER_ROLE = "reviewer";
|
|
|
15
16
|
const DEFAULT_REPORT_POLL_INTERVAL_MS = 1000;
|
|
16
17
|
const activeRuns = new Map();
|
|
17
18
|
const gateStateLocks = new Map();
|
|
18
|
-
const ARCHITECTURE_ANALYSIS_FIELDS = [
|
|
19
|
-
"Evidence Read",
|
|
20
|
-
"Architecture Brief Fit",
|
|
21
|
-
"End-To-End Flow",
|
|
22
|
-
"Scope Fit",
|
|
23
|
-
"Code Reality",
|
|
24
|
-
"Ownership",
|
|
25
|
-
"Data Flow",
|
|
26
|
-
"Lifecycle",
|
|
27
|
-
"Invariants",
|
|
28
|
-
"Boundaries And Public Surface",
|
|
29
|
-
"Failure Model",
|
|
30
|
-
"Coder Readiness"
|
|
31
|
-
];
|
|
32
|
-
const VALIDATION_ANALYSIS_FIELDS = [
|
|
33
|
-
"Evidence Read",
|
|
34
|
-
"Changed Behavior And Risk",
|
|
35
|
-
"Coverage Mapping",
|
|
36
|
-
"Baseline Coverage",
|
|
37
|
-
"L2 Integration Coverage",
|
|
38
|
-
"L3 Trigger Assessment",
|
|
39
|
-
"L3 End-To-End Coverage",
|
|
40
|
-
"Boundary And Failure Coverage",
|
|
41
|
-
"Public Contract Coverage",
|
|
42
|
-
"Test Integrity",
|
|
43
|
-
"Test Infrastructure",
|
|
44
|
-
"Skips And Gaps",
|
|
45
|
-
"User Approval And Gap Disposition",
|
|
46
|
-
"Validation Readiness"
|
|
47
|
-
];
|
|
48
|
-
const CODE_DIFF_ANALYSIS_FIELDS = [
|
|
49
|
-
"Commit Range And Sources",
|
|
50
|
-
"Evidence Read",
|
|
51
|
-
"Changed Files And Symbols",
|
|
52
|
-
"Changed Behavior",
|
|
53
|
-
"Source Evidence Fit",
|
|
54
|
-
"Callers And Public Surface",
|
|
55
|
-
"State Lifecycle And Failure Paths",
|
|
56
|
-
"Coding Standards",
|
|
57
|
-
"Baseline Test Integrity",
|
|
58
|
-
"Generated Context And Durable Docs",
|
|
59
|
-
"Code Readiness"
|
|
60
|
-
];
|
|
61
19
|
const SOURCE_ARTIFACTS = {
|
|
62
20
|
"architecture-plan": [
|
|
63
21
|
".ai/vcm/handoffs/architecture-brief.md",
|
|
@@ -1531,113 +1489,61 @@ async function parseGateReport(fs, taskRepoRoot, gate, requestId, timestamp, rep
|
|
|
1531
1489
|
});
|
|
1532
1490
|
}
|
|
1533
1491
|
const content = await fs.readText(absolutePath);
|
|
1534
|
-
const
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
statusCode: 500
|
|
1540
|
-
});
|
|
1541
|
-
}
|
|
1542
|
-
const parsedRequest = matchField(content, "Request");
|
|
1543
|
-
if (requestId && parsedRequest !== requestId) {
|
|
1544
|
-
throw new VcmError({
|
|
1545
|
-
code: "GATE_REVIEW_REPORT_STALE",
|
|
1546
|
-
message: `Gate review report request is ${parsedRequest ?? "missing"}, expected ${requestId}.`,
|
|
1547
|
-
statusCode: 500
|
|
1548
|
-
});
|
|
1549
|
-
}
|
|
1550
|
-
const decision = normalizeDecision(matchField(content, "Decision"));
|
|
1551
|
-
if (!decision) {
|
|
1492
|
+
const validation = parseGateReviewReportArtifact(content, {
|
|
1493
|
+
expectedGate: gate,
|
|
1494
|
+
expectedRequestId: requestId
|
|
1495
|
+
});
|
|
1496
|
+
if (!validation.parsed || validation.errors.length > 0) {
|
|
1552
1497
|
throw new VcmError({
|
|
1553
|
-
code:
|
|
1554
|
-
message: `Gate review report
|
|
1498
|
+
code: gateReportErrorCode(validation.errors),
|
|
1499
|
+
message: `Gate review report is invalid: ${validation.errors.join(" ")}`,
|
|
1555
1500
|
statusCode: 500
|
|
1556
1501
|
});
|
|
1557
1502
|
}
|
|
1558
|
-
const findings = extractFindings(content);
|
|
1559
|
-
if (gate === "architecture-plan") {
|
|
1560
|
-
validateArchitectureAnalysis(content);
|
|
1561
|
-
}
|
|
1562
1503
|
if (gate === "validation-adequacy") {
|
|
1563
|
-
|
|
1564
|
-
if (decision === "approve") {
|
|
1504
|
+
if (validation.parsed.decision === "approve") {
|
|
1565
1505
|
await validateValidationApprovalInput(fs, taskRepoRoot);
|
|
1566
1506
|
}
|
|
1567
1507
|
}
|
|
1568
|
-
if (gate === "code-diff") {
|
|
1569
|
-
validateCodeDiffAnalysis(content);
|
|
1570
|
-
}
|
|
1571
|
-
if (decision === "request_changes") {
|
|
1572
|
-
validateRequestChangeFindings(findings);
|
|
1573
|
-
if (gate === "code-diff") {
|
|
1574
|
-
validateCodeDiffFindings(findings);
|
|
1575
|
-
}
|
|
1576
|
-
}
|
|
1577
1508
|
return {
|
|
1578
1509
|
gate,
|
|
1579
|
-
requestId:
|
|
1580
|
-
decision,
|
|
1581
|
-
summary:
|
|
1582
|
-
findings,
|
|
1510
|
+
requestId: validation.parsed.requestId,
|
|
1511
|
+
decision: validation.parsed.decision,
|
|
1512
|
+
summary: validation.parsed.summary,
|
|
1513
|
+
findings: validation.parsed.findings,
|
|
1583
1514
|
reportPath,
|
|
1584
1515
|
content,
|
|
1585
1516
|
parsedAt: timestamp
|
|
1586
1517
|
};
|
|
1587
1518
|
}
|
|
1588
|
-
function
|
|
1589
|
-
const
|
|
1590
|
-
if (
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
if (
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
if (missingFields.length > 0) {
|
|
1617
|
-
throw new VcmError({
|
|
1618
|
-
code: "GATE_REVIEW_VALIDATION_ANALYSIS_INCOMPLETE",
|
|
1619
|
-
message: `Validation Analysis is missing required evidence: ${missingFields.join(", ")}.`,
|
|
1620
|
-
statusCode: 500
|
|
1621
|
-
});
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
function validateCodeDiffAnalysis(content) {
|
|
1625
|
-
const section = extractMarkdownSection(content, "Code Diff Analysis");
|
|
1626
|
-
if (!section) {
|
|
1627
|
-
throw new VcmError({
|
|
1628
|
-
code: "GATE_REVIEW_CODE_DIFF_ANALYSIS_MISSING",
|
|
1629
|
-
message: "Code-diff review must contain a non-empty Code Diff Analysis section.",
|
|
1630
|
-
statusCode: 500
|
|
1631
|
-
});
|
|
1632
|
-
}
|
|
1633
|
-
const missingFields = CODE_DIFF_ANALYSIS_FIELDS.filter((field) => !matchField(section, field));
|
|
1634
|
-
if (missingFields.length > 0) {
|
|
1635
|
-
throw new VcmError({
|
|
1636
|
-
code: "GATE_REVIEW_CODE_DIFF_ANALYSIS_INCOMPLETE",
|
|
1637
|
-
message: `Code Diff Analysis is missing required evidence: ${missingFields.join(", ")}.`,
|
|
1638
|
-
statusCode: 500
|
|
1639
|
-
});
|
|
1640
|
-
}
|
|
1519
|
+
function gateReportErrorCode(errors) {
|
|
1520
|
+
const message = errors.join(" ");
|
|
1521
|
+
if (/Architecture Analysis must appear exactly once/.test(message))
|
|
1522
|
+
return "GATE_REVIEW_ARCHITECTURE_ANALYSIS_MISSING";
|
|
1523
|
+
if (/Architecture Analysis field/.test(message))
|
|
1524
|
+
return "GATE_REVIEW_ARCHITECTURE_ANALYSIS_INCOMPLETE";
|
|
1525
|
+
if (/Validation Analysis must appear exactly once/.test(message))
|
|
1526
|
+
return "GATE_REVIEW_VALIDATION_ANALYSIS_MISSING";
|
|
1527
|
+
if (/Validation Analysis field/.test(message))
|
|
1528
|
+
return "GATE_REVIEW_VALIDATION_ANALYSIS_INCOMPLETE";
|
|
1529
|
+
if (/Code Diff Analysis must appear exactly once/.test(message))
|
|
1530
|
+
return "GATE_REVIEW_CODE_DIFF_ANALYSIS_MISSING";
|
|
1531
|
+
if (/Code Diff Analysis field/.test(message))
|
|
1532
|
+
return "GATE_REVIEW_CODE_DIFF_ANALYSIS_INCOMPLETE";
|
|
1533
|
+
if (/Finding Scope|field File|field Line Or Symbol/.test(message)) {
|
|
1534
|
+
return "GATE_REVIEW_CODE_DIFF_FINDING_LOCATION_MISSING";
|
|
1535
|
+
}
|
|
1536
|
+
if (/field Evidence|field Expected|field Gap|field Risk/.test(message))
|
|
1537
|
+
return "GATE_REVIEW_FINDING_INCOMPLETE";
|
|
1538
|
+
if (/requires at least one structured finding/.test(message))
|
|
1539
|
+
return "GATE_REVIEW_FINDINGS_MISSING";
|
|
1540
|
+
if (/Decision must/.test(message))
|
|
1541
|
+
return "GATE_REVIEW_DECISION_MISSING";
|
|
1542
|
+
if (/Gate must/.test(message))
|
|
1543
|
+
return "GATE_REVIEW_REPORT_GATE_MISMATCH";
|
|
1544
|
+
if (/Request must/.test(message))
|
|
1545
|
+
return "GATE_REVIEW_REPORT_STALE";
|
|
1546
|
+
return "GATE_REVIEW_REPORT_INVALID";
|
|
1641
1547
|
}
|
|
1642
1548
|
async function validateValidationApprovalInput(fs, taskRepoRoot) {
|
|
1643
1549
|
const relativePath = CORE_INPUT_ARTIFACTS["validation-adequacy"];
|
|
@@ -1679,38 +1585,6 @@ function formatArtifactCheckFailure(check) {
|
|
|
1679
1585
|
function renderFoundValue(value) {
|
|
1680
1586
|
return value && value.trim().length > 0 ? JSON.stringify(value.trim()) : "<missing>";
|
|
1681
1587
|
}
|
|
1682
|
-
function validateRequestChangeFindings(findings) {
|
|
1683
|
-
if (findings.length === 0) {
|
|
1684
|
-
throw new VcmError({
|
|
1685
|
-
code: "GATE_REVIEW_FINDINGS_MISSING",
|
|
1686
|
-
message: "A request_changes decision must contain at least one structured finding.",
|
|
1687
|
-
statusCode: 500
|
|
1688
|
-
});
|
|
1689
|
-
}
|
|
1690
|
-
const incomplete = findings.find((finding) => (!finding.evidence.trim()
|
|
1691
|
-
|| !finding.expected.trim()
|
|
1692
|
-
|| !finding.gap.trim()
|
|
1693
|
-
|| !finding.risk.trim()));
|
|
1694
|
-
if (incomplete) {
|
|
1695
|
-
throw new VcmError({
|
|
1696
|
-
code: "GATE_REVIEW_FINDING_INCOMPLETE",
|
|
1697
|
-
message: `Finding ${incomplete.title} must contain Evidence, Expected, Gap, and Risk.`,
|
|
1698
|
-
statusCode: 500
|
|
1699
|
-
});
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
function validateCodeDiffFindings(findings) {
|
|
1703
|
-
const incomplete = findings.find((finding) => (!finding.file?.trim()
|
|
1704
|
-
|| !finding.location?.trim()
|
|
1705
|
-
|| !finding.scope));
|
|
1706
|
-
if (incomplete) {
|
|
1707
|
-
throw new VcmError({
|
|
1708
|
-
code: "GATE_REVIEW_CODE_DIFF_FINDING_LOCATION_MISSING",
|
|
1709
|
-
message: `Code-diff finding ${incomplete.title} must contain File, Line Or Symbol, and Finding Scope.`,
|
|
1710
|
-
statusCode: 500
|
|
1711
|
-
});
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
1588
|
function extractMarkdownSection(content, heading) {
|
|
1715
1589
|
const match = new RegExp(`^##\\s+${escapeRegex(heading)}\\s*$`, "im").exec(content);
|
|
1716
1590
|
if (!match || match.index === undefined) {
|
|
@@ -1780,39 +1654,6 @@ function matchField(content, field) {
|
|
|
1780
1654
|
const match = content.match(new RegExp(`^\\s*(?:[-*]\\s*)?${escapeRegex(field)}\\s*:\\s*(.+?)\\s*$`, "mi"));
|
|
1781
1655
|
return match?.[1]?.trim();
|
|
1782
1656
|
}
|
|
1783
|
-
function extractSummary(content) {
|
|
1784
|
-
const field = matchField(content, "Summary");
|
|
1785
|
-
if (field) {
|
|
1786
|
-
return field;
|
|
1787
|
-
}
|
|
1788
|
-
const section = content.match(/^##\s+Summary\s*\n([\s\S]*?)(?=\n##\s+|\s*$)/im);
|
|
1789
|
-
return section?.[1]?.trim() || undefined;
|
|
1790
|
-
}
|
|
1791
|
-
function extractFindings(content) {
|
|
1792
|
-
const findings = [];
|
|
1793
|
-
const blocks = content.split(/\n(?=#{2,4}\s+|-+\s*severity\s*:|severity\s*:)/i);
|
|
1794
|
-
for (const block of blocks) {
|
|
1795
|
-
const heading = block.match(/^#{2,4}\s+(critical|high|medium|low)\s*:\s*(.+)$/im);
|
|
1796
|
-
const severity = normalizeSeverity(matchField(block, "severity") ?? heading?.[1]);
|
|
1797
|
-
const title = matchField(block, "title") ?? heading?.[2]?.trim() ?? block.match(/^#{2,4}\s+(.+)$/m)?.[1]?.trim();
|
|
1798
|
-
if (!severity || !title) {
|
|
1799
|
-
continue;
|
|
1800
|
-
}
|
|
1801
|
-
findings.push({
|
|
1802
|
-
severity,
|
|
1803
|
-
title,
|
|
1804
|
-
file: matchField(block, "file"),
|
|
1805
|
-
line: parsePositiveInteger(matchField(block, "line")),
|
|
1806
|
-
location: matchField(block, "line or symbol"),
|
|
1807
|
-
scope: normalizeCodeDiffFindingScope(matchField(block, "finding scope")),
|
|
1808
|
-
evidence: matchField(block, "evidence") ?? "",
|
|
1809
|
-
expected: matchField(block, "expected") ?? "",
|
|
1810
|
-
gap: matchField(block, "gap") ?? "",
|
|
1811
|
-
risk: matchField(block, "risk") ?? ""
|
|
1812
|
-
});
|
|
1813
|
-
}
|
|
1814
|
-
return findings;
|
|
1815
|
-
}
|
|
1816
1657
|
function getSourceArtifacts(gate, codeDiffSources) {
|
|
1817
1658
|
if (gate !== "code-diff") {
|
|
1818
1659
|
return SOURCE_ARTIFACTS[gate];
|
|
@@ -1928,13 +1769,6 @@ function isFinding(value) {
|
|
|
1928
1769
|
function isString(value) {
|
|
1929
1770
|
return typeof value === "string";
|
|
1930
1771
|
}
|
|
1931
|
-
function parsePositiveInteger(value) {
|
|
1932
|
-
if (!value) {
|
|
1933
|
-
return undefined;
|
|
1934
|
-
}
|
|
1935
|
-
const parsed = Number.parseInt(value, 10);
|
|
1936
|
-
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
1937
|
-
}
|
|
1938
1772
|
function assertExceptionReason(reason) {
|
|
1939
1773
|
if (!reason?.trim()) {
|
|
1940
1774
|
throw new VcmError({
|
|
@@ -1998,16 +1832,7 @@ function errorMessage(error) {
|
|
|
1998
1832
|
return "Unknown Gate review error.";
|
|
1999
1833
|
}
|
|
2000
1834
|
function isPendingReportError(error) {
|
|
2001
|
-
return error instanceof VcmError &&
|
|
2002
|
-
"GATE_REVIEW_DECISION_MISSING",
|
|
2003
|
-
"GATE_REVIEW_ARCHITECTURE_ANALYSIS_MISSING",
|
|
2004
|
-
"GATE_REVIEW_ARCHITECTURE_ANALYSIS_INCOMPLETE",
|
|
2005
|
-
"GATE_REVIEW_FINDINGS_MISSING",
|
|
2006
|
-
"GATE_REVIEW_FINDING_INCOMPLETE",
|
|
2007
|
-
"GATE_REVIEW_REPORT_GATE_MISMATCH",
|
|
2008
|
-
"GATE_REVIEW_REPORT_MISSING",
|
|
2009
|
-
"GATE_REVIEW_REPORT_STALE"
|
|
2010
|
-
].includes(error.code);
|
|
1835
|
+
return error instanceof VcmError && error.code === "GATE_REVIEW_REPORT_MISSING";
|
|
2011
1836
|
}
|
|
2012
1837
|
function delay(ms) {
|
|
2013
1838
|
if (ms <= 0) {
|