vibe-coding-master 0.7.46 → 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/README.md +2 -2
- package/dist/backend/cli/install-vcm-harness.js +30 -0
- 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/harness-service.js +16 -0
- 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/architect-agent.js +18 -3
- package/dist/backend/templates/harness/architect-evidence-worker-agent.js +44 -0
- package/dist/backend/templates/harness/architect-scaffold-worker-agent.js +3 -1
- package/dist/backend/templates/harness/architect-validation-worker-agent.js +36 -0
- 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/backend/templates/harness/vcm-route-message-skill.js +2 -2
- package/dist/shared/validation/artifact-registry.js +68 -0
- package/package.json +1 -1
- package/scripts/harness-tools/vcm-artifact +1 -34
- package/scripts/harness-tools/vcm-subagent-guard +9 -4
|
@@ -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
|
}
|
|
@@ -5,7 +5,9 @@ import { renderArchitectHarnessRules } from "../templates/harness/architect-agen
|
|
|
5
5
|
import { CODE_ROLE_DISALLOWED_TOOLS, REVIEWER_DISALLOWED_TOOLS } from "../role-tool-policy.js";
|
|
6
6
|
import { renderCoderHarnessRules } from "../templates/harness/coder-agent.js";
|
|
7
7
|
import { renderCoderWorkerHarnessRules } from "../templates/harness/coder-worker-agent.js";
|
|
8
|
+
import { renderArchitectEvidenceWorkerHarnessRules } from "../templates/harness/architect-evidence-worker-agent.js";
|
|
8
9
|
import { renderArchitectScaffoldWorkerHarnessRules } from "../templates/harness/architect-scaffold-worker-agent.js";
|
|
10
|
+
import { renderArchitectValidationWorkerHarnessRules } from "../templates/harness/architect-validation-worker-agent.js";
|
|
9
11
|
import { renderReviewerAgentRules, renderRequestGateReviewTool, renderTranslatorAgentRules, renderVcmGateReviewSkillRules } from "../templates/harness/gate-review.js";
|
|
10
12
|
import { renderHarnessEngineerHarnessRules } from "../templates/harness/harness-engineer-agent.js";
|
|
11
13
|
import { renderRootClaudeHarnessRules } from "../templates/harness/claude-root.js";
|
|
@@ -255,6 +257,13 @@ const HARNESS_FILES = [
|
|
|
255
257
|
frontmatter: renderAgentFrontmatter("vcm-coder-worker", "Bounded VCM implementation worker for assigned modules, files, and VCM:CODE markers from Coder.", { model: "inherit" }),
|
|
256
258
|
renderRules: renderCoderWorkerHarnessRules
|
|
257
259
|
},
|
|
260
|
+
{
|
|
261
|
+
kind: "agent-architect-evidence-worker",
|
|
262
|
+
path: ".claude/agents/vcm-architect-evidence-worker.md",
|
|
263
|
+
title: "VCM Architect Evidence Worker Agent",
|
|
264
|
+
frontmatter: renderAgentFrontmatter("vcm-architect-evidence-worker", "Foreground Architect worker for bounded project evidence collection.", { tools: "Read, Grep, Glob, Write", model: "opus", effort: "xhigh" }),
|
|
265
|
+
renderRules: renderArchitectEvidenceWorkerHarnessRules
|
|
266
|
+
},
|
|
258
267
|
{
|
|
259
268
|
kind: "agent-architect-scaffold-worker",
|
|
260
269
|
path: ".claude/agents/vcm-architect-scaffold-worker.md",
|
|
@@ -262,6 +271,13 @@ const HARNESS_FILES = [
|
|
|
262
271
|
frontmatter: renderAgentFrontmatter("vcm-architect-scaffold-worker", "Foreground Architect worker for exact scaffold execution and scaffold validation.", { model: "opus", effort: "xhigh" }),
|
|
263
272
|
renderRules: renderArchitectScaffoldWorkerHarnessRules
|
|
264
273
|
},
|
|
274
|
+
{
|
|
275
|
+
kind: "agent-architect-validation-worker",
|
|
276
|
+
path: ".claude/agents/vcm-architect-validation-worker.md",
|
|
277
|
+
title: "VCM Architect Validation Worker Agent",
|
|
278
|
+
frontmatter: renderAgentFrontmatter("vcm-architect-validation-worker", "Foreground Architect worker for exact assigned command execution and evidence capture.", { tools: "Read, Grep, Glob, Bash, Write", model: "opus", effort: "xhigh" }),
|
|
279
|
+
renderRules: renderArchitectValidationWorkerHarnessRules
|
|
280
|
+
},
|
|
265
281
|
{
|
|
266
282
|
kind: "tool-request-gate-review",
|
|
267
283
|
path: ".ai/tools/request-gate-review",
|
|
@@ -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);
|
|
@@ -35,6 +35,7 @@ export function createRuntimeRecoveryService(deps) {
|
|
|
35
35
|
await recoverMessages(taskRepoRoot, config.stateRoot, task.taskSlug, recoveredAt, context);
|
|
36
36
|
await recoverGateReview(taskRepoRoot, recoveredAt, context);
|
|
37
37
|
await cleanupCoderWorkers(taskRepoRoot, context);
|
|
38
|
+
await deps.architectRestartService?.recoverTask(repoRoot, task.taskSlug);
|
|
38
39
|
if ((roundRecovered || task.status === "running") && !hasLiveTaskSession(task.taskSlug)) {
|
|
39
40
|
await deps.taskService.updateTaskStatus(repoRoot, task.taskSlug, "stopped");
|
|
40
41
|
}
|
|
@@ -5,7 +5,9 @@ export function createTaskCloseService(deps) {
|
|
|
5
5
|
async closeTask(repoRoot, taskSlug) {
|
|
6
6
|
const task = await deps.taskService.markTaskCleaned(repoRoot, taskSlug);
|
|
7
7
|
const warnings = [];
|
|
8
|
-
deps.architectRestartService
|
|
8
|
+
if (deps.architectRestartService) {
|
|
9
|
+
await bestEffort("Unable to clear Architect restart state", () => deps.architectRestartService.clear(repoRoot, taskSlug), warnings);
|
|
10
|
+
}
|
|
9
11
|
await stopTaskRoleSessions(repoRoot, taskSlug, warnings);
|
|
10
12
|
await bestEffort("Unable to stop task translation runtime", () => deps.translationService.stopTask(getTaskRuntimeRepoRoot(task), taskSlug, { clearCache: true }), warnings);
|
|
11
13
|
await bestEffort("Unable to clear task round runtime", () => deps.roundService.stopTask(taskSlug), warnings);
|
|
@@ -38,6 +38,21 @@ ${renderRoleMemoryRules("architect")}
|
|
|
38
38
|
- Before ending any turn, ensure all information required to continue the current Architect work is present in the current artifacts.
|
|
39
39
|
- Keep artifacts current and self-contained. Replace superseded content instead of appending conversation history or investigation logs.
|
|
40
40
|
|
|
41
|
+
### Architect Worker Delegation
|
|
42
|
+
|
|
43
|
+
- Architect may invoke only \`vcm-architect-evidence-worker\`, \`vcm-architect-scaffold-worker\`, and \`vcm-architect-validation-worker\`.
|
|
44
|
+
- Every worker must run in the foreground and return before the current Architect turn continues. Do not run workers in the background or end the turn while a worker is active.
|
|
45
|
+
- Give each worker an exact bounded assignment, repo-relative paths, questions or commands, and one report path. Pass paths instead of copying full source, documents, or plans into the worker prompt.
|
|
46
|
+
- Worker output is evidence or execution output, never an architecture decision. Architect owns every conclusion, plan, change boundary, validation interpretation, and final claim.
|
|
47
|
+
- Use \`vcm-architect-evidence-worker\` for bounded bulk reading when relevant evidence spans multiple files or modules. Evidence workers may run in parallel only when their read scopes are disjoint.
|
|
48
|
+
- Evidence workers write reports under \`.ai/vcm/architect-workers/evidence/\`. Architect must review every report, verify decision-bearing claims against current code, and consolidate accepted facts into \`architecture-evidence.md\`.
|
|
49
|
+
- In Planning Code Reading, supporting non-decision-bearing read requirements may be satisfied by an accepted evidence-worker report; requirements that say Architect must personally read or verify may not.
|
|
50
|
+
- Use \`vcm-architect-scaffold-worker\` after the plan and Scaffold Manifest are complete for exact scaffold execution and mechanical text or configuration changes already fixed by the plan.
|
|
51
|
+
- Use \`vcm-architect-validation-worker\` for exact non-interactive commands already selected by Architect. The worker does not select validation scope, modify tests or code, diagnose failures, or decide whether validation is sufficient.
|
|
52
|
+
- Do not rerun a green command already reported by another Architect worker merely to execute it in the main Architect context.
|
|
53
|
+
- Validation workers write reports under \`.ai/vcm/architect-workers/validation/\`. Architect must interpret the raw results and copy required evidence into the owning Architect artifact.
|
|
54
|
+
- Remove \`.ai/vcm/architect-workers/\` after its accepted facts and command results have been consolidated into Architect-owned artifacts.
|
|
55
|
+
|
|
41
56
|
### Architecture Interview
|
|
42
57
|
|
|
43
58
|
- Before the first Architecture Planning step of Code-Change Flow, use \`vcm-architecture-interview\` and complete \`.ai/vcm/handoffs/architecture-brief.md\` and \`.ai/vcm/handoffs/architecture-evidence.md\`.
|
|
@@ -57,10 +72,11 @@ ${renderRoleMemoryRules("architect")}
|
|
|
57
72
|
### Planning Code Reading
|
|
58
73
|
|
|
59
74
|
- Do not plan from session memory, architecture docs, generated context, or code comments alone. Re-read current-worktree source and verify actual behavior from implementation.
|
|
60
|
-
- Use \`vcm-code-navigation\` for symbol definitions, implementations, references, call hierarchies, and bounded behavior paths. Start from generated indexes, use LSP semantic navigation, then read every
|
|
75
|
+
- Use \`vcm-code-navigation\` for symbol definitions, implementations, references, call hierarchies, and bounded behavior paths. Start from generated indexes, use LSP semantic navigation, then personally read every decision-bearing callable unit in full.
|
|
76
|
+
- Delegate bounded supporting implementation and document reading to \`vcm-architect-evidence-worker\` when it would otherwise add substantial raw context. Worker reports do not replace Architect's LSP verification of decision-bearing symbols, relationships, public surfaces, ownership, lifecycle, failure paths, contradictions, or unresolved evidence.
|
|
61
77
|
- If LSP cannot resolve a required project-owned relationship, record the limitation in \`architecture-evidence.md\` and leave it unresolved.
|
|
62
78
|
- Define the planning boundary as the affected feature or module and identify every existing or intended observable entry point for the behavior being changed.
|
|
63
|
-
-
|
|
79
|
+
- Personally read the complete implementation of each decision-bearing existing entry point. Supporting entry points may be covered by accepted evidence-worker reports when their facts do not determine the architecture decision.
|
|
64
80
|
- Follow every project-owned call path the plan will change through cross-module calls, state reads and writes, persistence, side effects, completion and failure signals, and consumers.
|
|
65
81
|
- For every cross-file or public callable surface the plan will add or change, read its current project-owned callers and consumers.
|
|
66
82
|
- When the plan changes state ownership or lifecycle behavior, read the relevant project-owned creators, readers, writers, completion handlers, failure handlers, cancellation handlers, retry handlers, and recovery handlers.
|
|
@@ -125,7 +141,6 @@ ${renderRoleMemoryRules("architect")}
|
|
|
125
141
|
#### Code Scaffolding
|
|
126
142
|
|
|
127
143
|
- Use the Agent tool to invoke \`vcm-architect-scaffold-worker\` in the foreground after the plan and Scaffold Manifest are complete. Give it the exact plan path and require it to return before this Architect turn continues.
|
|
128
|
-
- Do not invoke any other subagent.
|
|
129
144
|
- Use one scaffold worker. Do not run it in the background or end the Architect turn while it is active.
|
|
130
145
|
- Review the worker commit, actual diff, callable surfaces, marker placement, ledger reconciliation, and L0 results yourself. Architect owns every final scaffold claim and must correct any worker error before marking planning complete.
|
|
131
146
|
- Create or update only the minimum module/file scaffolding needed to make boundaries, callable surfaces, and placeholders unambiguous. Minimum limits depth (no business implementation), never breadth: every \`create\`, \`change\`, and \`delete\` item must be scaffolded.
|