vibe-coding-master 0.7.47 → 0.7.49
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 +7 -0
- package/dist/backend/api/session-routes.js +41 -1
- package/dist/backend/server.js +17 -3
- 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 +6 -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/role-context-restart-service.js +240 -0
- package/dist/backend/services/runtime-recovery-service.js +2 -0
- package/dist/backend/services/session-service.js +39 -2
- package/dist/backend/services/task-close-service.js +6 -1
- package/dist/backend/templates/harness/coder-agent.js +1 -0
- package/dist/backend/templates/harness/gate-review.js +13 -98
- package/dist/backend/templates/harness/tester-agent.js +1 -0
- package/dist/backend/templates/harness/vcm-report-harness-issue-skill.js +1 -1
- package/dist/shared/validation/artifact-registry.js +68 -0
- package/dist-frontend/assets/{index-nAV6toi8.js → index-DLsIPTvK.js} +34 -34
- package/dist-frontend/index.html +1 -1
- package/package.json +1 -1
- package/scripts/harness-tools/vcm-artifact +1 -34
|
@@ -4,6 +4,7 @@ import { checkMarkdownArtifact, readArtifactSectionValue } from "../../shared/va
|
|
|
4
4
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
5
5
|
import { VcmError } from "../errors.js";
|
|
6
6
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
7
|
+
import { parseHarnessFeedbackArtifact } from "./managed-artifact-validation.js";
|
|
7
8
|
const FEEDBACK_ROOT = ".ai/vcm/harness-feedback";
|
|
8
9
|
const PENDING_DIR = `${FEEDBACK_ROOT}/pending`;
|
|
9
10
|
const TASK_RETROSPECTIVE_DIR = `${FEEDBACK_ROOT}/task-retrospectives`;
|
|
@@ -266,19 +267,22 @@ export function createHarnessFeedbackService(deps) {
|
|
|
266
267
|
}
|
|
267
268
|
function parseFeedbackItem(relativePath, content) {
|
|
268
269
|
const id = sanitizeFeedbackId(path.posix.basename(relativePath, ".md"));
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
270
|
+
const validation = parseHarnessFeedbackArtifact(content);
|
|
271
|
+
if (!validation.parsed || validation.errors.length > 0) {
|
|
272
|
+
throw new VcmError({
|
|
273
|
+
code: "HARNESS_FEEDBACK_INVALID",
|
|
274
|
+
message: `Invalid Harness Feedback ${relativePath}: ${validation.errors.join(" ")}`,
|
|
275
|
+
statusCode: 422
|
|
276
|
+
});
|
|
277
|
+
}
|
|
274
278
|
return {
|
|
275
279
|
id,
|
|
276
|
-
title: compactLine(title),
|
|
280
|
+
title: compactLine(validation.parsed.title),
|
|
277
281
|
path: relativePath,
|
|
278
282
|
source: "role-feedback",
|
|
279
|
-
reporterRole:
|
|
280
|
-
taskSlug:
|
|
281
|
-
summary:
|
|
283
|
+
reporterRole: validation.parsed.reporterRole,
|
|
284
|
+
taskSlug: validation.parsed.taskSlug,
|
|
285
|
+
summary: validation.parsed.summary
|
|
282
286
|
};
|
|
283
287
|
}
|
|
284
288
|
function buildTaskRetrospectivePrompt(repoRoot, analysisPath, pendingFeedbackPaths, memoryReview) {
|
|
@@ -493,21 +497,6 @@ function getRetrospectiveReportErrors(content) {
|
|
|
493
497
|
}
|
|
494
498
|
return errors;
|
|
495
499
|
}
|
|
496
|
-
function parseSimpleMetadata(content) {
|
|
497
|
-
const result = {};
|
|
498
|
-
for (const line of content.split(/\r?\n/).slice(0, 80)) {
|
|
499
|
-
const match = /^[-*]?\s*([A-Za-z][A-Za-z -]{1,40})\s*:\s*(.+)$/.exec(line.trim());
|
|
500
|
-
if (!match) {
|
|
501
|
-
continue;
|
|
502
|
-
}
|
|
503
|
-
result[match[1].trim().toLowerCase()] = match[2].trim();
|
|
504
|
-
}
|
|
505
|
-
return result;
|
|
506
|
-
}
|
|
507
|
-
function firstHeading(content) {
|
|
508
|
-
const heading = content.split(/\r?\n/).find((line) => /^#{1,3}\s+\S/.test(line));
|
|
509
|
-
return heading?.replace(/^#{1,3}\s+/, "").trim();
|
|
510
|
-
}
|
|
511
500
|
function compactLine(value) {
|
|
512
501
|
return value.replace(/\s+/g, " ").trim().slice(0, 160);
|
|
513
502
|
}
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { CODE_DIFF_FINDING_SCOPES, GATE_REVIEW_GATES } from "../../shared/types/gate-review.js";
|
|
2
|
+
import { checkMarkdownArtifact } from "../../shared/validation/artifact-check.js";
|
|
3
|
+
import { getManagedArtifactDefinition, isArtifactKind } from "../../shared/validation/artifact-registry.js";
|
|
4
|
+
import { parseMemoryProposal } from "./memory-proposal-validation.js";
|
|
5
|
+
export const ROUTE_MESSAGE_TYPES = [
|
|
6
|
+
"task",
|
|
7
|
+
"question",
|
|
8
|
+
"revise",
|
|
9
|
+
"cancel",
|
|
10
|
+
"result",
|
|
11
|
+
"blocked",
|
|
12
|
+
"finding"
|
|
13
|
+
];
|
|
14
|
+
export const GATE_ANALYSIS_HEADINGS = {
|
|
15
|
+
"architecture-plan": "Architecture Analysis",
|
|
16
|
+
"validation-adequacy": "Validation Analysis",
|
|
17
|
+
"code-diff": "Code Diff Analysis"
|
|
18
|
+
};
|
|
19
|
+
export const GATE_ANALYSIS_FIELDS = {
|
|
20
|
+
"architecture-plan": [
|
|
21
|
+
"Evidence Read",
|
|
22
|
+
"Architecture Brief Fit",
|
|
23
|
+
"End-To-End Flow",
|
|
24
|
+
"Scope Fit",
|
|
25
|
+
"Code Reality",
|
|
26
|
+
"Invalidated Assumptions",
|
|
27
|
+
"Existing-Class Completeness",
|
|
28
|
+
"Ownership",
|
|
29
|
+
"Data Flow",
|
|
30
|
+
"Lifecycle",
|
|
31
|
+
"Invariants",
|
|
32
|
+
"Boundaries And Public Surface",
|
|
33
|
+
"Failure Model",
|
|
34
|
+
"Coder Readiness"
|
|
35
|
+
],
|
|
36
|
+
"validation-adequacy": [
|
|
37
|
+
"Evidence Read",
|
|
38
|
+
"Changed Behavior And Risk",
|
|
39
|
+
"Coverage Mapping",
|
|
40
|
+
"Baseline Coverage",
|
|
41
|
+
"L2 Integration Coverage",
|
|
42
|
+
"L3 Trigger Assessment",
|
|
43
|
+
"L3 End-To-End Coverage",
|
|
44
|
+
"Boundary And Failure Coverage",
|
|
45
|
+
"Public Contract Coverage",
|
|
46
|
+
"Test Integrity",
|
|
47
|
+
"Test Infrastructure",
|
|
48
|
+
"Skips And Gaps",
|
|
49
|
+
"User Approval And Gap Disposition",
|
|
50
|
+
"Validation Readiness"
|
|
51
|
+
],
|
|
52
|
+
"code-diff": [
|
|
53
|
+
"Commit Range And Sources",
|
|
54
|
+
"Evidence Read",
|
|
55
|
+
"Changed Files And Symbols",
|
|
56
|
+
"Changed Behavior",
|
|
57
|
+
"Source Evidence Fit",
|
|
58
|
+
"Callers And Public Surface",
|
|
59
|
+
"State Lifecycle And Failure Paths",
|
|
60
|
+
"Coding Standards",
|
|
61
|
+
"Baseline Test Integrity",
|
|
62
|
+
"Generated Context And Durable Docs",
|
|
63
|
+
"Code Readiness"
|
|
64
|
+
]
|
|
65
|
+
};
|
|
66
|
+
const PLACEHOLDER_VALUE = /^(?:TBD|Not run yet\.?|<[^>]+>)$/i;
|
|
67
|
+
export function validateManagedArtifactContent(kind, content, context) {
|
|
68
|
+
const definition = getManagedArtifactDefinition(kind);
|
|
69
|
+
if (!definition.allowedModes.includes(context.mode)) {
|
|
70
|
+
return {
|
|
71
|
+
errors: [`${kind} supports only ${definition.allowedModes.join("|")} mode.`],
|
|
72
|
+
status: "incomplete"
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (isArtifactKind(kind)) {
|
|
76
|
+
const check = checkMarkdownArtifact(kind, context.path, content, { mode: context.mode });
|
|
77
|
+
return {
|
|
78
|
+
errors: artifactCheckErrors(check, context.mode),
|
|
79
|
+
status: check.status,
|
|
80
|
+
check
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const result = kind === "route-message"
|
|
84
|
+
? parseRouteMessageArtifact(content)
|
|
85
|
+
: kind === "coder-worker-report"
|
|
86
|
+
? parseCoderWorkerReportArtifact(content)
|
|
87
|
+
: kind === "gate-review-report"
|
|
88
|
+
? parseGateReviewReportArtifact(content, {
|
|
89
|
+
expectedGate: context.expectedGate,
|
|
90
|
+
expectedRequestId: context.expectedRequestId
|
|
91
|
+
})
|
|
92
|
+
: kind === "memory-proposal"
|
|
93
|
+
? parseMemoryProposalArtifact(content)
|
|
94
|
+
: parseHarnessFeedbackArtifact(content);
|
|
95
|
+
return {
|
|
96
|
+
errors: result.errors,
|
|
97
|
+
status: result.errors.length === 0 ? "accepted" : "incomplete",
|
|
98
|
+
parsed: result.parsed
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export function parseRouteMessageArtifact(content) {
|
|
102
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(content);
|
|
103
|
+
if (!frontmatter) {
|
|
104
|
+
return { errors: ["Route message requires YAML-style frontmatter bounded by ---."] };
|
|
105
|
+
}
|
|
106
|
+
const fields = parseColonFields(frontmatter[1]);
|
|
107
|
+
const errors = [];
|
|
108
|
+
const type = fields.type;
|
|
109
|
+
if (!type || !ROUTE_MESSAGE_TYPES.includes(type)) {
|
|
110
|
+
errors.push(`Frontmatter type must be exactly one of ${ROUTE_MESSAGE_TYPES.join("|")}.`);
|
|
111
|
+
}
|
|
112
|
+
const body = content.slice(frontmatter[0].length).trim();
|
|
113
|
+
if (!body) {
|
|
114
|
+
errors.push("Route message body must not be empty.");
|
|
115
|
+
}
|
|
116
|
+
if (errors.length > 0) {
|
|
117
|
+
return { errors };
|
|
118
|
+
}
|
|
119
|
+
const refs = fields.artifact_refs ?? fields.artifactRefs ?? fields.related_artifact;
|
|
120
|
+
return {
|
|
121
|
+
errors: [],
|
|
122
|
+
parsed: {
|
|
123
|
+
type: type,
|
|
124
|
+
body,
|
|
125
|
+
artifactRefs: refs ? refs.split(",").map((entry) => entry.trim()).filter(Boolean) : []
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export function parseCoderWorkerReportArtifact(content) {
|
|
130
|
+
const definition = getManagedArtifactDefinition("coder-worker-report");
|
|
131
|
+
const errors = validateRequiredHeadings(content, definition.requiredHeadings);
|
|
132
|
+
const titleMatches = [...content.matchAll(/^# Coder Worker Report:\s*(\S.+?)\s*$/gm)];
|
|
133
|
+
if (titleMatches.length !== 1) {
|
|
134
|
+
errors.push("Coder Worker report requires exactly one '# Coder Worker Report: <worker-id>' title.");
|
|
135
|
+
}
|
|
136
|
+
const workerState = readUniqueField(content, "Worker State", errors);
|
|
137
|
+
if (workerState !== "completed") {
|
|
138
|
+
errors.push(`Worker State must be exactly completed; found ${renderFoundValue(workerState)}.`);
|
|
139
|
+
}
|
|
140
|
+
const implementationResult = readUniqueField(content, "Implementation Result", errors);
|
|
141
|
+
if (implementationResult !== "success" && implementationResult !== "has_failed_items") {
|
|
142
|
+
errors.push(`Implementation Result must be exactly success|has_failed_items; found ${renderFoundValue(implementationResult)}.`);
|
|
143
|
+
}
|
|
144
|
+
if (errors.length > 0) {
|
|
145
|
+
return { errors };
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
errors: [],
|
|
149
|
+
parsed: {
|
|
150
|
+
workerId: titleMatches[0][1].trim(),
|
|
151
|
+
workerState: "completed",
|
|
152
|
+
implementationResult: implementationResult
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export function parseGateReviewReportArtifact(content, context = {}) {
|
|
157
|
+
const errors = [];
|
|
158
|
+
const gateValue = readUniqueField(content, "Gate", errors);
|
|
159
|
+
const requestId = readUniqueField(content, "Request", errors);
|
|
160
|
+
const decisionValue = readUniqueField(content, "Decision", errors);
|
|
161
|
+
const summary = readUniqueField(content, "Summary", errors);
|
|
162
|
+
const gate = GATE_REVIEW_GATES.includes(gateValue)
|
|
163
|
+
? gateValue
|
|
164
|
+
: undefined;
|
|
165
|
+
if (!gate) {
|
|
166
|
+
errors.push(`Gate must be exactly one of ${GATE_REVIEW_GATES.join("|")}; found ${renderFoundValue(gateValue)}.`);
|
|
167
|
+
}
|
|
168
|
+
if (context.expectedGate && gateValue !== context.expectedGate) {
|
|
169
|
+
errors.push(`Gate must be ${context.expectedGate}; found ${renderFoundValue(gateValue)}.`);
|
|
170
|
+
}
|
|
171
|
+
if (context.expectedRequestId && requestId !== context.expectedRequestId) {
|
|
172
|
+
errors.push(`Request must be ${context.expectedRequestId}; found ${renderFoundValue(requestId)}.`);
|
|
173
|
+
}
|
|
174
|
+
if (decisionValue !== "approve" && decisionValue !== "request_changes") {
|
|
175
|
+
errors.push(`Decision must be exactly approve|request_changes; found ${renderFoundValue(decisionValue)}.`);
|
|
176
|
+
}
|
|
177
|
+
if (!isSubstantiveValue(summary)) {
|
|
178
|
+
errors.push("Summary must contain a substantive one-line value.");
|
|
179
|
+
}
|
|
180
|
+
let analysis = {};
|
|
181
|
+
if (gate) {
|
|
182
|
+
const expectedHeading = GATE_ANALYSIS_HEADINGS[gate];
|
|
183
|
+
for (const candidate of Object.values(GATE_ANALYSIS_HEADINGS)) {
|
|
184
|
+
const count = countMarkdownHeading(content, candidate);
|
|
185
|
+
if (candidate === expectedHeading && count !== 1) {
|
|
186
|
+
errors.push(`${expectedHeading} must appear exactly once; found ${count}.`);
|
|
187
|
+
}
|
|
188
|
+
if (candidate !== expectedHeading && count !== 0) {
|
|
189
|
+
errors.push(`${candidate} is not allowed for the ${gate} gate.`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const section = readMarkdownSection(content, expectedHeading);
|
|
193
|
+
if (section) {
|
|
194
|
+
analysis = parseRequiredBulletFields(section, GATE_ANALYSIS_FIELDS[gate], errors, expectedHeading);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const findingsCount = countMarkdownHeading(content, "Findings");
|
|
198
|
+
if (findingsCount !== 1) {
|
|
199
|
+
errors.push(`Findings must appear exactly once; found ${findingsCount}.`);
|
|
200
|
+
}
|
|
201
|
+
const findingsBody = readMarkdownSection(content, "Findings");
|
|
202
|
+
let findings = [];
|
|
203
|
+
if (decisionValue === "approve") {
|
|
204
|
+
if (findingsBody?.trim() !== "None.") {
|
|
205
|
+
errors.push('Findings must contain exactly "None." when Decision is approve.');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else if (decisionValue === "request_changes") {
|
|
209
|
+
const parsedFindings = parseGateFindings(findingsBody ?? "", gate, errors);
|
|
210
|
+
findings = parsedFindings;
|
|
211
|
+
if (findings.length === 0) {
|
|
212
|
+
errors.push("Decision request_changes requires at least one structured finding.");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (errors.length > 0 || !gate || !requestId || !summary) {
|
|
216
|
+
return { errors };
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
errors: [],
|
|
220
|
+
parsed: {
|
|
221
|
+
gate,
|
|
222
|
+
requestId,
|
|
223
|
+
decision: decisionValue,
|
|
224
|
+
summary,
|
|
225
|
+
findings,
|
|
226
|
+
analysis
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
export function parseMemoryProposalArtifact(content) {
|
|
231
|
+
const result = parseMemoryProposal(content);
|
|
232
|
+
return result.error
|
|
233
|
+
? { errors: [`Memory proposal ${result.error}.`] }
|
|
234
|
+
: { errors: [], parsed: result.proposal };
|
|
235
|
+
}
|
|
236
|
+
export function parseHarnessFeedbackArtifact(content) {
|
|
237
|
+
const errors = [];
|
|
238
|
+
const titleMatches = [...content.matchAll(/^#\s+(\S.+?)\s*$/gm)];
|
|
239
|
+
if (titleMatches.length !== 1) {
|
|
240
|
+
errors.push(`Harness Feedback requires exactly one '# <short problem title>' heading.`);
|
|
241
|
+
}
|
|
242
|
+
const fieldNames = [
|
|
243
|
+
"Reporter role",
|
|
244
|
+
"Task slug",
|
|
245
|
+
"Summary",
|
|
246
|
+
"Observed problem",
|
|
247
|
+
"Expected behavior",
|
|
248
|
+
"Evidence",
|
|
249
|
+
"Suspected harness area",
|
|
250
|
+
"Impact",
|
|
251
|
+
"Urgency"
|
|
252
|
+
];
|
|
253
|
+
const values = {};
|
|
254
|
+
for (const field of fieldNames) {
|
|
255
|
+
const matches = [...content.matchAll(new RegExp(`^- ${escapeRegExp(field)}:\\s*(.*?)\\s*$`, "gmi"))];
|
|
256
|
+
if (matches.length !== 1 || !isSubstantiveValue(matches[0]?.[1])) {
|
|
257
|
+
errors.push(`Harness Feedback field ${field} must appear exactly once with a substantive value.`);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
values[field] = matches[0][1].trim();
|
|
261
|
+
}
|
|
262
|
+
if (values.Urgency !== "low" && values.Urgency !== "medium" && values.Urgency !== "high") {
|
|
263
|
+
errors.push(`Urgency must be exactly low|medium|high; found ${renderFoundValue(values.Urgency)}.`);
|
|
264
|
+
}
|
|
265
|
+
if (errors.length > 0) {
|
|
266
|
+
return { errors };
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
errors: [],
|
|
270
|
+
parsed: {
|
|
271
|
+
title: titleMatches[0][1].trim(),
|
|
272
|
+
reporterRole: values["Reporter role"],
|
|
273
|
+
taskSlug: values["Task slug"],
|
|
274
|
+
summary: values.Summary,
|
|
275
|
+
observedProblem: values["Observed problem"],
|
|
276
|
+
expectedBehavior: values["Expected behavior"],
|
|
277
|
+
evidence: values.Evidence,
|
|
278
|
+
suspectedHarnessArea: values["Suspected harness area"],
|
|
279
|
+
impact: values.Impact,
|
|
280
|
+
urgency: values.Urgency
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
export function artifactCheckErrors(check, mode) {
|
|
285
|
+
const errors = [
|
|
286
|
+
...check.missingHeadings.map((heading) => `Missing required heading: ${heading}.`),
|
|
287
|
+
...check.invalidFields
|
|
288
|
+
];
|
|
289
|
+
if (mode === "final" && check.hasPlaceholder) {
|
|
290
|
+
errors.push("Final artifacts must not contain TBD, draft, or not-run placeholders.");
|
|
291
|
+
}
|
|
292
|
+
return errors;
|
|
293
|
+
}
|
|
294
|
+
function parseGateFindings(content, gate, errors) {
|
|
295
|
+
const headings = [...content.matchAll(/^### (critical|high|medium|low):\s*(\S.+?)\s*$/gmi)];
|
|
296
|
+
if (headings.length === 0) {
|
|
297
|
+
return [];
|
|
298
|
+
}
|
|
299
|
+
if (content.slice(0, headings[0].index).trim()) {
|
|
300
|
+
errors.push("Findings must contain only structured ### <severity>: <title> blocks.");
|
|
301
|
+
}
|
|
302
|
+
const findings = [];
|
|
303
|
+
for (let index = 0; index < headings.length; index += 1) {
|
|
304
|
+
const heading = headings[index];
|
|
305
|
+
const start = (heading.index ?? 0) + heading[0].length;
|
|
306
|
+
const end = index + 1 < headings.length ? headings[index + 1].index ?? content.length : content.length;
|
|
307
|
+
const block = content.slice(start, end).trim();
|
|
308
|
+
const fields = parseRequiredBulletFields(block, ["Evidence", "Expected", "Gap", "Risk"], errors, heading[2]);
|
|
309
|
+
const finding = {
|
|
310
|
+
severity: heading[1].toLowerCase(),
|
|
311
|
+
title: heading[2].trim(),
|
|
312
|
+
evidence: fields.Evidence ?? "",
|
|
313
|
+
expected: fields.Expected ?? "",
|
|
314
|
+
gap: fields.Gap ?? "",
|
|
315
|
+
risk: fields.Risk ?? ""
|
|
316
|
+
};
|
|
317
|
+
if (gate === "code-diff") {
|
|
318
|
+
const codeFields = parseRequiredBulletFields(block, ["File", "Line Or Symbol", "Finding Scope"], errors, heading[2]);
|
|
319
|
+
if (!CODE_DIFF_FINDING_SCOPES.includes(codeFields["Finding Scope"])) {
|
|
320
|
+
errors.push(`${heading[2]} Finding Scope must be exactly ${CODE_DIFF_FINDING_SCOPES.join("|")}; found ${renderFoundValue(codeFields["Finding Scope"])}.`);
|
|
321
|
+
}
|
|
322
|
+
finding.file = codeFields.File;
|
|
323
|
+
finding.location = codeFields["Line Or Symbol"];
|
|
324
|
+
finding.scope = codeFields["Finding Scope"];
|
|
325
|
+
}
|
|
326
|
+
findings.push(finding);
|
|
327
|
+
}
|
|
328
|
+
return findings;
|
|
329
|
+
}
|
|
330
|
+
function parseRequiredBulletFields(content, requiredFields, errors, sectionName) {
|
|
331
|
+
const values = {};
|
|
332
|
+
for (const field of requiredFields) {
|
|
333
|
+
const matches = [...content.matchAll(new RegExp(`^- ${escapeRegExp(field)}:\\s*(.*?)\\s*$`, "gmi"))];
|
|
334
|
+
if (matches.length !== 1) {
|
|
335
|
+
errors.push(`${sectionName} field ${field} must appear exactly once; found ${matches.length}.`);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const value = matches[0][1].trim();
|
|
339
|
+
if (!isSubstantiveValue(value)) {
|
|
340
|
+
errors.push(`${sectionName} field ${field} must contain a substantive value.`);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
values[field] = value;
|
|
344
|
+
}
|
|
345
|
+
return values;
|
|
346
|
+
}
|
|
347
|
+
function validateRequiredHeadings(content, headings) {
|
|
348
|
+
const errors = [];
|
|
349
|
+
let previous = -1;
|
|
350
|
+
for (const heading of headings) {
|
|
351
|
+
const matches = [...content.matchAll(new RegExp(`^## ${escapeRegExp(heading)}\\s*$`, "gmi"))];
|
|
352
|
+
if (matches.length !== 1) {
|
|
353
|
+
errors.push(`Heading ${heading} must appear exactly once; found ${matches.length}.`);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const index = matches[0].index ?? -1;
|
|
357
|
+
if (index < previous) {
|
|
358
|
+
errors.push(`Heading ${heading} is out of the required order.`);
|
|
359
|
+
}
|
|
360
|
+
previous = index;
|
|
361
|
+
}
|
|
362
|
+
return errors;
|
|
363
|
+
}
|
|
364
|
+
function readUniqueField(content, field, errors) {
|
|
365
|
+
const matches = [...content.matchAll(new RegExp(`^${escapeRegExp(field)}:\\s*(.*?)\\s*$`, "gmi"))];
|
|
366
|
+
if (matches.length !== 1) {
|
|
367
|
+
errors.push(`${field} must appear exactly once; found ${matches.length}.`);
|
|
368
|
+
return undefined;
|
|
369
|
+
}
|
|
370
|
+
return matches[0][1].trim();
|
|
371
|
+
}
|
|
372
|
+
function readMarkdownSection(content, heading) {
|
|
373
|
+
const match = new RegExp(`^## ${escapeRegExp(heading)}\\s*$`, "mi").exec(content);
|
|
374
|
+
if (!match || match.index === undefined) {
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
const remainder = content.slice(match.index + match[0].length);
|
|
378
|
+
const nextHeading = remainder.search(/^##\s+/m);
|
|
379
|
+
return (nextHeading >= 0 ? remainder.slice(0, nextHeading) : remainder).trim();
|
|
380
|
+
}
|
|
381
|
+
function countMarkdownHeading(content, heading) {
|
|
382
|
+
return [...content.matchAll(new RegExp(`^## ${escapeRegExp(heading)}\\s*$`, "gmi"))].length;
|
|
383
|
+
}
|
|
384
|
+
function parseColonFields(content) {
|
|
385
|
+
const fields = {};
|
|
386
|
+
for (const line of content.split(/\r?\n/)) {
|
|
387
|
+
const delimiter = line.indexOf(":");
|
|
388
|
+
if (delimiter <= 0) {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
fields[line.slice(0, delimiter).trim()] = line.slice(delimiter + 1).trim();
|
|
392
|
+
}
|
|
393
|
+
return fields;
|
|
394
|
+
}
|
|
395
|
+
function isSubstantiveValue(value) {
|
|
396
|
+
return Boolean(value?.trim() && !PLACEHOLDER_VALUE.test(value.trim()));
|
|
397
|
+
}
|
|
398
|
+
function renderFoundValue(value) {
|
|
399
|
+
return value?.trim() ? JSON.stringify(value.trim()) : "<missing>";
|
|
400
|
+
}
|
|
401
|
+
function escapeRegExp(value) {
|
|
402
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
403
|
+
}
|
|
@@ -2,8 +2,10 @@ import path from "node:path";
|
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { CORE_VCM_ROLE_NAMES } from "../../shared/constants.js";
|
|
4
4
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
5
|
+
import { VcmError } from "../errors.js";
|
|
5
6
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
6
7
|
import { renderMessageEnvelope } from "../templates/message-envelope.js";
|
|
8
|
+
import { parseRouteMessageArtifact } from "./managed-artifact-validation.js";
|
|
7
9
|
const PM_ROLE = "project-manager";
|
|
8
10
|
const DEFAULT_PRE_DISPATCH_SWITCH_DELAY_MS = 500;
|
|
9
11
|
const DEFAULT_AUTO_DISPATCH_ENTER_DELAY_MS = 500;
|
|
@@ -381,6 +383,9 @@ async function listRouteFiles(fs, input) {
|
|
|
381
383
|
}
|
|
382
384
|
const relativePath = path.posix.join(routeDir, entry);
|
|
383
385
|
const content = await fs.readText(resolveRepoPath(repoRoot, relativePath));
|
|
386
|
+
if (!content.trim()) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
384
389
|
const parsed = parseRouteFileContent(content, route.fromRole, route.toRole);
|
|
385
390
|
routeFiles.push({
|
|
386
391
|
path: relativePath,
|
|
@@ -410,71 +415,15 @@ function parseRouteFileName(fileName) {
|
|
|
410
415
|
return undefined;
|
|
411
416
|
}
|
|
412
417
|
function parseRouteFileContent(content, fromRole, toRole) {
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
function splitFrontmatter(content) {
|
|
423
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
424
|
-
if (!match) {
|
|
425
|
-
return { frontmatter: {}, body: content };
|
|
426
|
-
}
|
|
427
|
-
const frontmatter = {};
|
|
428
|
-
for (const line of match[1].split(/\r?\n/)) {
|
|
429
|
-
const delimiter = line.indexOf(":");
|
|
430
|
-
if (delimiter <= 0) {
|
|
431
|
-
continue;
|
|
432
|
-
}
|
|
433
|
-
const key = line.slice(0, delimiter).trim();
|
|
434
|
-
const value = line.slice(delimiter + 1).trim();
|
|
435
|
-
if (key) {
|
|
436
|
-
frontmatter[key] = value;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
return {
|
|
440
|
-
frontmatter,
|
|
441
|
-
body: content.slice(match[0].length)
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
function parseMessageType(value) {
|
|
445
|
-
const validTypes = [
|
|
446
|
-
"user-request",
|
|
447
|
-
"task",
|
|
448
|
-
"question",
|
|
449
|
-
"blocked",
|
|
450
|
-
"result",
|
|
451
|
-
"finding",
|
|
452
|
-
"review-request",
|
|
453
|
-
"revise",
|
|
454
|
-
"cancel"
|
|
455
|
-
];
|
|
456
|
-
return value && validTypes.includes(value)
|
|
457
|
-
? value
|
|
458
|
-
: undefined;
|
|
459
|
-
}
|
|
460
|
-
function parseArtifactRefs(frontmatter) {
|
|
461
|
-
const refs = frontmatter.artifact_refs ?? frontmatter.artifactRefs ?? frontmatter.related_artifact;
|
|
462
|
-
if (!refs) {
|
|
463
|
-
return [];
|
|
464
|
-
}
|
|
465
|
-
return refs
|
|
466
|
-
.split(",")
|
|
467
|
-
.map((entry) => entry.trim())
|
|
468
|
-
.filter(Boolean);
|
|
469
|
-
}
|
|
470
|
-
function getDefaultMessageType(fromRole, toRole) {
|
|
471
|
-
if (fromRole === PM_ROLE && toRole !== PM_ROLE) {
|
|
472
|
-
return "task";
|
|
473
|
-
}
|
|
474
|
-
if (fromRole !== PM_ROLE && toRole === PM_ROLE) {
|
|
475
|
-
return "result";
|
|
418
|
+
const result = parseRouteMessageArtifact(content);
|
|
419
|
+
if (!result.parsed || result.errors.length > 0) {
|
|
420
|
+
throw new VcmError({
|
|
421
|
+
code: "ROUTE_MESSAGE_INVALID",
|
|
422
|
+
message: `Invalid route message ${fromRole} -> ${toRole}: ${result.errors.join(" ")}`,
|
|
423
|
+
statusCode: 422
|
|
424
|
+
});
|
|
476
425
|
}
|
|
477
|
-
return
|
|
426
|
+
return result.parsed;
|
|
478
427
|
}
|
|
479
428
|
function isAllowedRoute(fromRole, toRole) {
|
|
480
429
|
return (fromRole === PM_ROLE && toRole !== PM_ROLE) || (fromRole !== PM_ROLE && toRole === PM_ROLE);
|