vibe-coding-master 0.7.27 → 0.7.29
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 +31 -10
- package/dist/backend/api/harness-routes.js +0 -1
- package/dist/backend/cli/install-vcm-harness.js +37 -7
- package/dist/backend/server.js +9 -6
- package/dist/backend/services/architect-restart-service.js +51 -3
- package/dist/backend/services/auto-memory-service.js +118 -61
- package/dist/backend/services/claude-hook-service.js +12 -2
- package/dist/backend/services/gate-review-service.js +35 -18
- package/dist/backend/services/harness-feedback-service.js +138 -7
- package/dist/backend/services/memory-proposal-validation.js +80 -0
- package/dist/backend/services/memory-review-paths.js +7 -0
- package/dist/backend/services/memory-review-validation.js +94 -0
- package/dist/backend/templates/handoff.js +20 -16
- package/dist/backend/templates/harness/architect-agent.js +3 -2
- package/dist/backend/templates/harness/claude-root.js +1 -1
- package/dist/backend/templates/harness/coder-agent.js +12 -1
- package/dist/backend/templates/harness/coder-worker-agent.js +12 -1
- package/dist/backend/templates/harness/harness-engineer-agent.js +53 -12
- package/dist/backend/templates/harness/restart-architect-skill.js +12 -2
- package/dist/backend/templates/harness/role-memory.js +2 -2
- package/dist/backend/templates/harness/tester-agent.js +3 -3
- package/dist/backend/templates/harness/vcm-architecture-interview-skill.js +2 -2
- package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +1 -1
- package/dist/backend/templates/harness/vcm-propose-memory-skill.js +55 -7
- package/dist/backend/templates/harness/vcm-route-message-skill.js +2 -5
- package/dist/shared/validation/artifact-check.js +78 -64
- package/dist/shared/validation/artifact-contract.js +22 -0
- package/package.json +1 -1
- package/scripts/uninstall-vcm-harness.mjs +5 -0
|
@@ -1014,10 +1014,12 @@ async function readArchitectureBriefError(fs, taskRepoRoot) {
|
|
|
1014
1014
|
const content = await fs.readText(absolutePath);
|
|
1015
1015
|
const check = checkMarkdownArtifact("architecture-brief", relativePath, content);
|
|
1016
1016
|
if (check.status !== "ok") {
|
|
1017
|
-
return `${relativePath} is incomplete
|
|
1017
|
+
return `${relativePath} is incomplete and cannot start architecture-plan review. ${formatArtifactCheckFailure(check)}`;
|
|
1018
1018
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1019
|
+
const status = /^\s*Architecture Brief Status\s*:\s*(.+?)\s*$/im.exec(content)?.[1]?.trim();
|
|
1020
|
+
if (status?.toLowerCase() !== "confirmed") {
|
|
1021
|
+
return `${relativePath} is not confirmed and cannot start architecture-plan review. `
|
|
1022
|
+
+ `Architecture Brief Status must be exactly "confirmed"; found ${renderFoundValue(status)}.`;
|
|
1021
1023
|
}
|
|
1022
1024
|
return undefined;
|
|
1023
1025
|
}
|
|
@@ -1032,12 +1034,8 @@ async function readValidationReportError(fs, taskRepoRoot) {
|
|
|
1032
1034
|
if (check.status === "ok") {
|
|
1033
1035
|
return undefined;
|
|
1034
1036
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
check.invalidFields.length > 0 ? check.invalidFields.join(" ") : "",
|
|
1038
|
-
check.hasPlaceholder ? "contains placeholders" : ""
|
|
1039
|
-
].filter(Boolean).join("; ");
|
|
1040
|
-
return `${relativePath} is incomplete and cannot start validation-adequacy review.${details ? ` ${details}` : ""}`;
|
|
1037
|
+
return `${relativePath} is incomplete and cannot start validation-adequacy review. `
|
|
1038
|
+
+ formatValidationArtifactFailure(check, content);
|
|
1041
1039
|
}
|
|
1042
1040
|
async function readArchitectureEvidenceError(fs, taskRepoRoot) {
|
|
1043
1041
|
const relativePath = ".ai/vcm/handoffs/architecture-evidence.md";
|
|
@@ -1049,8 +1047,10 @@ async function readArchitectureEvidenceError(fs, taskRepoRoot) {
|
|
|
1049
1047
|
if (content.trim().length === 0) {
|
|
1050
1048
|
return `${relativePath} is empty. Complete architecture evidence before requesting architecture-plan review.`;
|
|
1051
1049
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1050
|
+
const status = /^\s*Architecture Evidence Status\s*:\s*(.+?)\s*$/im.exec(content)?.[1]?.trim();
|
|
1051
|
+
if (status?.toLowerCase() !== "complete") {
|
|
1052
|
+
return `${relativePath} is incomplete and cannot start architecture-plan review. `
|
|
1053
|
+
+ `Architecture Evidence Status must be exactly "complete"; found ${renderFoundValue(status)}.`;
|
|
1054
1054
|
}
|
|
1055
1055
|
return undefined;
|
|
1056
1056
|
}
|
|
@@ -1260,18 +1260,35 @@ async function validateValidationApprovalInput(fs, taskRepoRoot) {
|
|
|
1260
1260
|
if (check.status === "ok") {
|
|
1261
1261
|
return;
|
|
1262
1262
|
}
|
|
1263
|
-
const details = [
|
|
1264
|
-
`status=${check.status}`,
|
|
1265
|
-
check.missingHeadings.length > 0 ? `missing headings: ${check.missingHeadings.join(", ")}` : "",
|
|
1266
|
-
check.invalidFields.length > 0 ? check.invalidFields.join(" ") : "",
|
|
1267
|
-
check.hasPlaceholder ? "contains placeholders" : ""
|
|
1268
|
-
].filter(Boolean).join("; ");
|
|
1269
1263
|
throw new VcmError({
|
|
1270
1264
|
code: "GATE_REVIEW_VALIDATION_INPUT_INCOMPLETE",
|
|
1271
|
-
message: `Validation-adequacy cannot approve incomplete Tester evidence in ${relativePath}.
|
|
1265
|
+
message: `Validation-adequacy cannot approve incomplete Tester evidence in ${relativePath}. `
|
|
1266
|
+
+ formatValidationArtifactFailure(check, content),
|
|
1272
1267
|
statusCode: 500
|
|
1273
1268
|
});
|
|
1274
1269
|
}
|
|
1270
|
+
function formatValidationArtifactFailure(check, content) {
|
|
1271
|
+
return /^\s*Test Result\s*:\s*incomplete\s*$/im.test(content ?? "")
|
|
1272
|
+
? 'Test Result must be exactly one of "pass|fail"; found "incomplete".'
|
|
1273
|
+
: formatArtifactCheckFailure(check);
|
|
1274
|
+
}
|
|
1275
|
+
function formatArtifactCheckFailure(check) {
|
|
1276
|
+
const details = [
|
|
1277
|
+
check.status === "missing" ? "Artifact is missing." : "",
|
|
1278
|
+
check.status === "empty" ? "Artifact is empty." : "",
|
|
1279
|
+
check.missingHeadings.length > 0
|
|
1280
|
+
? `Missing headings: ${check.missingHeadings.join(", ")}.`
|
|
1281
|
+
: "",
|
|
1282
|
+
...check.invalidFields,
|
|
1283
|
+
check.hasPlaceholder ? "Replace every standalone TBD, Not run yet, or draft-status placeholder." : ""
|
|
1284
|
+
].filter(Boolean);
|
|
1285
|
+
return details.length > 0
|
|
1286
|
+
? details.join(" ")
|
|
1287
|
+
: "Artifact is not in a gate-ready terminal state.";
|
|
1288
|
+
}
|
|
1289
|
+
function renderFoundValue(value) {
|
|
1290
|
+
return value && value.trim().length > 0 ? JSON.stringify(value.trim()) : "<missing>";
|
|
1291
|
+
}
|
|
1275
1292
|
function validateRequestChangeFindings(findings) {
|
|
1276
1293
|
if (findings.length === 0) {
|
|
1277
1294
|
throw new VcmError({
|
|
@@ -49,7 +49,7 @@ export function createHarnessFeedbackService(deps) {
|
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
51
|
const existingMarker = await loadTaskRetrospectiveMarker(repoRoot, taskSlug);
|
|
52
|
-
if (existingMarker) {
|
|
52
|
+
if (existingMarker && existingMarker.status !== "failed") {
|
|
53
53
|
throw new VcmError({
|
|
54
54
|
code: "TASK_HARNESS_RETROSPECTIVE_EXISTS",
|
|
55
55
|
message: `Task Harness Retrospective has already been triggered for task: ${taskSlug}`,
|
|
@@ -77,19 +77,82 @@ export function createHarnessFeedbackService(deps) {
|
|
|
77
77
|
const session = await ensureIdleHarnessEngineer(repoRoot, taskSlug);
|
|
78
78
|
const timestamp = now();
|
|
79
79
|
const analysisPath = `${TASK_RETROSPECTIVE_DIR}/${sanitizeFeedbackId(taskSlug)}.md`;
|
|
80
|
-
|
|
80
|
+
const analysisAbsolutePath = resolveRepoPath(repoRoot, analysisPath);
|
|
81
|
+
const memoryReview = await deps.autoMemoryService?.prepareTaskRetrospectiveReview(input.taskRepoRoot, analysisAbsolutePath);
|
|
82
|
+
const marker = {
|
|
81
83
|
version: 1,
|
|
82
84
|
taskSlug,
|
|
83
85
|
trigger: input.trigger,
|
|
84
|
-
status: "
|
|
86
|
+
status: "running",
|
|
85
87
|
analysisPath,
|
|
86
88
|
finalAcceptanceHash: `sha256:${sha256(finalAcceptanceContent)}`,
|
|
89
|
+
...(memoryReview ? { memoryRunId: memoryReview.runId } : {}),
|
|
87
90
|
createdAt: timestamp,
|
|
88
91
|
updatedAt: timestamp
|
|
89
|
-
}
|
|
90
|
-
|
|
92
|
+
};
|
|
93
|
+
const pendingFeedback = await listPendingFeedback(repoRoot);
|
|
94
|
+
try {
|
|
95
|
+
await persistTaskRetrospectiveMarker(repoRoot, marker);
|
|
96
|
+
await submitTerminalInput(deps.runtime, session.id, buildTaskRetrospectivePrompt(repoRoot, analysisPath, pendingFeedback.map((item) => item.path), memoryReview));
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (memoryReview) {
|
|
100
|
+
await deps.autoMemoryService?.cancelTaskRetrospectiveReview(input.taskRepoRoot, memoryReview.runId);
|
|
101
|
+
}
|
|
102
|
+
const failedAt = now();
|
|
103
|
+
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
104
|
+
...marker,
|
|
105
|
+
status: "failed",
|
|
106
|
+
failedAt,
|
|
107
|
+
updatedAt: failedAt,
|
|
108
|
+
error: errorMessage(error)
|
|
109
|
+
});
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
91
112
|
return getState(repoRoot);
|
|
92
113
|
}
|
|
114
|
+
async function handleTaskRetrospectiveHook(repoRoot, input) {
|
|
115
|
+
const marker = await loadTaskRetrospectiveMarker(repoRoot, input.taskSlug);
|
|
116
|
+
if (!marker || (marker.status !== "running" && marker.status !== "triggered")) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
if (input.eventName === "UserPromptSubmit" || input.eventName === "PostCompact") {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
const timestamp = now();
|
|
123
|
+
if (input.eventName === "StopFailure") {
|
|
124
|
+
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
125
|
+
...marker,
|
|
126
|
+
status: "failed",
|
|
127
|
+
failedAt: timestamp,
|
|
128
|
+
updatedAt: timestamp,
|
|
129
|
+
error: "Harness Engineer Task Harness Retrospective turn failed."
|
|
130
|
+
});
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
const reportPath = resolveRepoPath(repoRoot, marker.analysisPath);
|
|
134
|
+
const reportReady = await deps.fs.pathExists(reportPath)
|
|
135
|
+
&& Boolean((await deps.fs.readText(reportPath)).trim());
|
|
136
|
+
if (!reportReady || (marker.memoryRunId && !input.memoryReviewSucceeded)) {
|
|
137
|
+
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
138
|
+
...marker,
|
|
139
|
+
status: "failed",
|
|
140
|
+
failedAt: timestamp,
|
|
141
|
+
updatedAt: timestamp,
|
|
142
|
+
error: !reportReady
|
|
143
|
+
? "Harness Engineer did not write the required Task Harness Retrospective report."
|
|
144
|
+
: "Task Harness Retrospective memory review failed."
|
|
145
|
+
});
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
149
|
+
...marker,
|
|
150
|
+
status: "completed",
|
|
151
|
+
completedAt: timestamp,
|
|
152
|
+
updatedAt: timestamp
|
|
153
|
+
});
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
93
156
|
async function assertHarnessEngineerAvailable(_repoRoot) {
|
|
94
157
|
return undefined;
|
|
95
158
|
}
|
|
@@ -162,12 +225,76 @@ export function createHarnessFeedbackService(deps) {
|
|
|
162
225
|
summary: metadata.summary
|
|
163
226
|
};
|
|
164
227
|
}
|
|
165
|
-
function buildTaskRetrospectivePrompt(repoRoot, analysisPath) {
|
|
228
|
+
function buildTaskRetrospectivePrompt(repoRoot, analysisPath, pendingFeedbackPaths, memoryReview) {
|
|
229
|
+
const pendingFeedback = pendingFeedbackPaths.length > 0
|
|
230
|
+
? pendingFeedbackPaths.map((feedbackPath) => `- ${resolveRepoPath(repoRoot, feedbackPath)}`)
|
|
231
|
+
: ["none"];
|
|
166
232
|
return [
|
|
167
233
|
"[VCM Task Harness Retrospective]",
|
|
168
234
|
"",
|
|
169
235
|
"Review the completed task from the current active task worktree.",
|
|
170
236
|
"",
|
|
237
|
+
`Pending Feedback Directory: ${resolveRepoPath(repoRoot, PENDING_DIR)}`,
|
|
238
|
+
"",
|
|
239
|
+
"Pending Feedback:",
|
|
240
|
+
...pendingFeedback,
|
|
241
|
+
...(pendingFeedbackPaths.length > 0
|
|
242
|
+
? [
|
|
243
|
+
"",
|
|
244
|
+
"Process every listed feedback inside this retrospective. Record every disposition in the retrospective report, then delete the processed feedback files before ending the turn."
|
|
245
|
+
]
|
|
246
|
+
: []),
|
|
247
|
+
...(memoryReview
|
|
248
|
+
? [
|
|
249
|
+
"",
|
|
250
|
+
"Auto Memory Review:",
|
|
251
|
+
`Role drafts: ${memoryReview.roleDraftsPath}`,
|
|
252
|
+
`Current memory snapshot: ${memoryReview.currentMemoryPath}`,
|
|
253
|
+
...(memoryReview.planningCandidatePath
|
|
254
|
+
? [`Architect planning-session candidate: ${memoryReview.planningCandidatePath}`]
|
|
255
|
+
: []),
|
|
256
|
+
`Write the complete reviewed memory set to: ${memoryReview.reviewedMemoryPath}`,
|
|
257
|
+
"",
|
|
258
|
+
"Review every memory candidate against final task evidence while performing this retrospective.",
|
|
259
|
+
"Each snapshot file contains only the matching <VCM-memory> block content. Edit every existing reviewed-memory file in place.",
|
|
260
|
+
"Before evaluating proposals, review every substantive entry in every current memory snapshot against current code, documentation, and final task evidence.",
|
|
261
|
+
"For each existing entry, decide retain, update, remove, or move-to-durable-doc. Record the decision reason, the impact of removing it, and whether memory or a durable document is the correct source.",
|
|
262
|
+
"Complete this full existing-memory review even when every proposal says no-change.",
|
|
263
|
+
"Then evaluate every proposal, including its stated need, absence impact, and durable-document disposition.",
|
|
264
|
+
"Keep only verified, durable, reusable project knowledge. Merge duplicates and keep role-specific knowledge in the matching role file.",
|
|
265
|
+
"Do not record task narrative, temporary state, unverified conclusions, or Harness rules in memory.",
|
|
266
|
+
"Existing-memory Target must be shared or the exact role name. Decision must be retain, update, remove, or move-to-durable-doc.",
|
|
267
|
+
"Durable doc disposition must be memory, durable-doc, or memory-reference. Use Durable doc path: none with memory and an actual path with the other dispositions.",
|
|
268
|
+
"Do not keep full content in memory when durable-doc is correct. Use memory-reference only when an ongoing role needs the document pointer.",
|
|
269
|
+
"Use none as the complete Existing Memory Decisions body only when no substantive existing memory entry exists.",
|
|
270
|
+
"Use this exact block in the retrospective report and replace each option or placeholder with one allowed value or a concise summary:",
|
|
271
|
+
"",
|
|
272
|
+
"## Memory Review",
|
|
273
|
+
"Existing memory reviewed: complete",
|
|
274
|
+
"",
|
|
275
|
+
"### Proposal Dispositions",
|
|
276
|
+
...memoryReview.proposalRoles.map((role) => `- ${role}: accepted|rejected|no-change`),
|
|
277
|
+
"",
|
|
278
|
+
"### Existing Memory Decisions",
|
|
279
|
+
"#### Item 1",
|
|
280
|
+
"Target: shared",
|
|
281
|
+
"Existing: <exact existing memory entry>",
|
|
282
|
+
"Decision: retain",
|
|
283
|
+
"Reason: <why this decision is correct>",
|
|
284
|
+
"Impact if removed: <specific future role or task failure>",
|
|
285
|
+
"Durable doc disposition: memory",
|
|
286
|
+
"Durable doc path: none",
|
|
287
|
+
"Evidence: <current code, durable documentation, or final task evidence>",
|
|
288
|
+
"",
|
|
289
|
+
"### Existing Memory Changes",
|
|
290
|
+
"- retained: <summary or none>",
|
|
291
|
+
"- updated: <summary or none>",
|
|
292
|
+
"- removed: <summary or none>",
|
|
293
|
+
"",
|
|
294
|
+
"Reviewed memory set: complete"
|
|
295
|
+
]
|
|
296
|
+
: []),
|
|
297
|
+
"",
|
|
171
298
|
`Write the analysis to Result Path: ${resolveRepoPath(repoRoot, analysisPath)}`,
|
|
172
299
|
"End your turn after writing the result."
|
|
173
300
|
].join("\n");
|
|
@@ -190,7 +317,7 @@ export function createHarnessFeedbackService(deps) {
|
|
|
190
317
|
return deps.fs.readJson(markerPath);
|
|
191
318
|
}
|
|
192
319
|
async function persistTaskRetrospectiveMarker(repoRoot, marker) {
|
|
193
|
-
const markerPath = resolveRepoPath(repoRoot, getTaskRetrospectiveMarkerPath(
|
|
320
|
+
const markerPath = resolveRepoPath(repoRoot, getTaskRetrospectiveMarkerPath(marker.taskSlug));
|
|
194
321
|
await deps.fs.ensureDir(path.dirname(markerPath));
|
|
195
322
|
await deps.fs.writeJsonAtomic(markerPath, marker);
|
|
196
323
|
}
|
|
@@ -214,6 +341,7 @@ export function createHarnessFeedbackService(deps) {
|
|
|
214
341
|
getState,
|
|
215
342
|
sendPendingFeedback,
|
|
216
343
|
startTaskRetrospective,
|
|
344
|
+
handleTaskRetrospectiveHook,
|
|
217
345
|
assertHarnessEngineerAvailable
|
|
218
346
|
};
|
|
219
347
|
}
|
|
@@ -245,3 +373,6 @@ function sanitizeFeedbackId(value) {
|
|
|
245
373
|
function sha256(content) {
|
|
246
374
|
return createHash("sha256").update(content).digest("hex");
|
|
247
375
|
}
|
|
376
|
+
function errorMessage(error) {
|
|
377
|
+
return error instanceof Error ? error.message : String(error);
|
|
378
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const OPERATIONS = ["Add", "Update", "Remove"];
|
|
2
|
+
export function validateMemoryProposal(content) {
|
|
3
|
+
if (!/^# Memory Proposal\s*$/m.test(content)) {
|
|
4
|
+
return "is missing the # Memory Proposal heading";
|
|
5
|
+
}
|
|
6
|
+
const decisionMatches = [...content.matchAll(/^Decision:[ \t]*(update|no-change)[ \t]*$/gm)];
|
|
7
|
+
if (decisionMatches.length !== 1) {
|
|
8
|
+
return "must contain exactly one Decision: update or Decision: no-change field";
|
|
9
|
+
}
|
|
10
|
+
const levelTwoHeadings = [...content.matchAll(/^## ([^\r\n]+?)[ \t]*$/gm)];
|
|
11
|
+
const sections = [...content.matchAll(/^## (Add|Update|Remove)[ \t]*$/gm)];
|
|
12
|
+
if (levelTwoHeadings.length !== OPERATIONS.length
|
|
13
|
+
|| sections.length !== OPERATIONS.length
|
|
14
|
+
|| sections.some((section, index) => section[1] !== OPERATIONS[index])) {
|
|
15
|
+
return "must contain only the Add, Update, and Remove sections, exactly once and in that order";
|
|
16
|
+
}
|
|
17
|
+
let itemCount = 0;
|
|
18
|
+
for (let index = 0; index < sections.length; index += 1) {
|
|
19
|
+
const section = sections[index];
|
|
20
|
+
const operation = section[1];
|
|
21
|
+
const bodyStart = (section.index ?? 0) + section[0].length;
|
|
22
|
+
const bodyEnd = index + 1 < sections.length
|
|
23
|
+
? sections[index + 1].index ?? content.length
|
|
24
|
+
: content.length;
|
|
25
|
+
const result = validateOperationBody(operation, content.slice(bodyStart, bodyEnd));
|
|
26
|
+
if (typeof result === "string") {
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
itemCount += result;
|
|
30
|
+
}
|
|
31
|
+
const decision = decisionMatches[0][1];
|
|
32
|
+
if (decision === "no-change" && itemCount !== 0) {
|
|
33
|
+
return "uses Decision: no-change but contains a memory item";
|
|
34
|
+
}
|
|
35
|
+
if (decision === "update" && itemCount === 0) {
|
|
36
|
+
return "uses Decision: update without a structured memory item";
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
function validateOperationBody(operation, rawBody) {
|
|
41
|
+
const body = rawBody.trim();
|
|
42
|
+
if (body === "none") {
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
const itemHeadings = [...body.matchAll(/^### Item \d+\s*$/gm)];
|
|
46
|
+
if (itemHeadings.length === 0 || body.slice(0, itemHeadings[0].index).trim()) {
|
|
47
|
+
return `${operation} must contain none or one or more ### Item N blocks`;
|
|
48
|
+
}
|
|
49
|
+
for (let index = 0; index < itemHeadings.length; index += 1) {
|
|
50
|
+
const heading = itemHeadings[index];
|
|
51
|
+
const itemStart = (heading.index ?? 0) + heading[0].length;
|
|
52
|
+
const itemEnd = index + 1 < itemHeadings.length
|
|
53
|
+
? itemHeadings[index + 1].index ?? body.length
|
|
54
|
+
: body.length;
|
|
55
|
+
const item = body.slice(itemStart, itemEnd).trim();
|
|
56
|
+
const expectedPattern = operation === "Add"
|
|
57
|
+
? /^Target:[ \t]*(shared|current-role)[ \t]*\nContent:[ \t]*(\S.*)[ \t]*\nReason:[ \t]*(\S.*)[ \t]*\nImpact if absent:[ \t]*(\S.*)[ \t]*\nDurable doc disposition:[ \t]*(memory|durable-doc|memory-reference)[ \t]*\nDurable doc path:[ \t]*(\S.*)[ \t]*\nEvidence:[ \t]*(\S.*)[ \t]*$/
|
|
58
|
+
: operation === "Update"
|
|
59
|
+
? /^Target:[ \t]*(shared|current-role)[ \t]*\nExisting:[ \t]*(\S.*)[ \t]*\nContent:[ \t]*(\S.*)[ \t]*\nReason:[ \t]*(\S.*)[ \t]*\nImpact if absent:[ \t]*(\S.*)[ \t]*\nDurable doc disposition:[ \t]*(memory|durable-doc|memory-reference)[ \t]*\nDurable doc path:[ \t]*(\S.*)[ \t]*\nEvidence:[ \t]*(\S.*)[ \t]*$/
|
|
60
|
+
: /^Target:[ \t]*(shared|current-role)[ \t]*\nExisting:[ \t]*(\S.*)[ \t]*\nEvidence:[ \t]*(\S.*)[ \t]*$/;
|
|
61
|
+
const fieldMatch = expectedPattern.exec(item);
|
|
62
|
+
if (!fieldMatch) {
|
|
63
|
+
const fields = operation === "Add"
|
|
64
|
+
? "Target, Content, Reason, Impact if absent, Durable doc disposition, Durable doc path, and Evidence"
|
|
65
|
+
: operation === "Update"
|
|
66
|
+
? "Target, Existing, Content, Reason, Impact if absent, Durable doc disposition, Durable doc path, and Evidence"
|
|
67
|
+
: "Target, Existing, and Evidence";
|
|
68
|
+
return `${operation} ${heading[0].trim()} must contain one-line ${fields} fields in that order`;
|
|
69
|
+
}
|
|
70
|
+
if (operation === "Add" || operation === "Update") {
|
|
71
|
+
const disposition = fieldMatch[operation === "Add" ? 5 : 6];
|
|
72
|
+
const durableDocPath = fieldMatch[operation === "Add" ? 6 : 7];
|
|
73
|
+
if ((disposition === "memory" && durableDocPath !== "none")
|
|
74
|
+
|| (disposition !== "memory" && durableDocPath === "none")) {
|
|
75
|
+
return `${operation} ${heading[0].trim()} must use Durable doc path: none only with Durable doc disposition: memory`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return itemHeadings.length;
|
|
80
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const MEMORY_REVIEW_ROOT = ".ai/vcm/memory-review";
|
|
2
|
+
export const MEMORY_REVIEW_RUNS_ROOT = `${MEMORY_REVIEW_ROOT}/runs`;
|
|
3
|
+
export const MEMORY_REVIEW_STATE_PATH = `${MEMORY_REVIEW_ROOT}/state.json`;
|
|
4
|
+
export const ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH = `${MEMORY_REVIEW_ROOT}/candidates/architect/planning.md`;
|
|
5
|
+
export function architectPlanningCandidateSnapshotPath(runId) {
|
|
6
|
+
return `${MEMORY_REVIEW_RUNS_ROOT}/${runId}/sources/architect-planning.md`;
|
|
7
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export function validateMemoryReviewReport(content, proposalRoles, hasExistingMemory = false) {
|
|
2
|
+
const memoryReview = /^## Memory Review\s*$/m.exec(content);
|
|
3
|
+
if (!memoryReview || memoryReview.index === undefined) {
|
|
4
|
+
return "is missing the ## Memory Review section";
|
|
5
|
+
}
|
|
6
|
+
const sectionStart = memoryReview.index + memoryReview[0].length;
|
|
7
|
+
const nextSection = /^## (?!#)/m.exec(content.slice(sectionStart));
|
|
8
|
+
const section = content.slice(sectionStart, nextSection?.index === undefined ? content.length : sectionStart + nextSection.index);
|
|
9
|
+
if (!/^Existing memory reviewed:[ \t]*complete[ \t]*$/m.test(section)) {
|
|
10
|
+
return "must declare Existing memory reviewed: complete";
|
|
11
|
+
}
|
|
12
|
+
if (!/^Reviewed memory set:[ \t]*complete[ \t]*$/m.test(section)) {
|
|
13
|
+
return "must declare Reviewed memory set: complete";
|
|
14
|
+
}
|
|
15
|
+
const dispositions = extractReportSubsection(section, "Proposal Dispositions", "Existing Memory Decisions");
|
|
16
|
+
if (dispositions === undefined) {
|
|
17
|
+
return "is missing the Proposal Dispositions subsection";
|
|
18
|
+
}
|
|
19
|
+
for (const role of proposalRoles) {
|
|
20
|
+
const matches = dispositions.match(new RegExp(`^- ${escapeRegExp(role)}:[ \\t]*(accepted|rejected|no-change)[ \\t]*$`, "gm"));
|
|
21
|
+
if (matches?.length !== 1) {
|
|
22
|
+
return `must record exactly one accepted, rejected, or no-change disposition for ${role}`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const existingDecisions = extractReportSubsection(section, "Existing Memory Decisions", "Existing Memory Changes");
|
|
26
|
+
if (existingDecisions === undefined) {
|
|
27
|
+
return "is missing the Existing Memory Decisions subsection";
|
|
28
|
+
}
|
|
29
|
+
const existingDecisionError = validateExistingMemoryDecisions(existingDecisions, hasExistingMemory);
|
|
30
|
+
if (existingDecisionError) {
|
|
31
|
+
return existingDecisionError;
|
|
32
|
+
}
|
|
33
|
+
const existingChanges = extractReportSubsection(section, "Existing Memory Changes");
|
|
34
|
+
if (existingChanges === undefined) {
|
|
35
|
+
return "is missing the Existing Memory Changes subsection";
|
|
36
|
+
}
|
|
37
|
+
for (const field of ["retained", "updated", "removed"]) {
|
|
38
|
+
const matches = existingChanges.match(new RegExp(`^- ${field}:[ \\t]*\\S.*$`, "gm"));
|
|
39
|
+
if (matches?.length !== 1) {
|
|
40
|
+
return `must record exactly one non-empty ${field} summary`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
function validateExistingMemoryDecisions(content, hasExistingMemory) {
|
|
46
|
+
const body = content.trim();
|
|
47
|
+
if (body === "none") {
|
|
48
|
+
return hasExistingMemory
|
|
49
|
+
? "Existing Memory Decisions cannot be none while substantive existing memory is present"
|
|
50
|
+
: undefined;
|
|
51
|
+
}
|
|
52
|
+
const itemHeadings = [...body.matchAll(/^#### Item \d+[ \t]*$/gm)];
|
|
53
|
+
if (itemHeadings.length === 0 || body.slice(0, itemHeadings[0].index).trim()) {
|
|
54
|
+
return "Existing Memory Decisions must contain none or one or more #### Item N blocks";
|
|
55
|
+
}
|
|
56
|
+
for (let index = 0; index < itemHeadings.length; index += 1) {
|
|
57
|
+
const heading = itemHeadings[index];
|
|
58
|
+
const itemStart = (heading.index ?? 0) + heading[0].length;
|
|
59
|
+
const itemEnd = index + 1 < itemHeadings.length
|
|
60
|
+
? itemHeadings[index + 1].index ?? body.length
|
|
61
|
+
: body.length;
|
|
62
|
+
const item = body.slice(itemStart, itemEnd).trim();
|
|
63
|
+
const match = /^Target:[ \t]*(shared|project-manager|architect|coder|tester|reviewer|harness-engineer)[ \t]*\nExisting:[ \t]*(\S.*)[ \t]*\nDecision:[ \t]*(retain|update|remove|move-to-durable-doc)[ \t]*\nReason:[ \t]*(\S.*)[ \t]*\nImpact if removed:[ \t]*(\S.*)[ \t]*\nDurable doc disposition:[ \t]*(memory|durable-doc|memory-reference)[ \t]*\nDurable doc path:[ \t]*(\S.*)[ \t]*\nEvidence:[ \t]*(\S.*)[ \t]*$/.exec(item);
|
|
64
|
+
if (!match) {
|
|
65
|
+
return `Existing Memory Decisions ${heading[0].trim()} must contain one-line Target, Existing, Decision, Reason, Impact if removed, Durable doc disposition, Durable doc path, and Evidence fields in that order`;
|
|
66
|
+
}
|
|
67
|
+
const disposition = match[6];
|
|
68
|
+
const durableDocPath = match[7];
|
|
69
|
+
if ((disposition === "memory" && durableDocPath !== "none")
|
|
70
|
+
|| (disposition !== "memory" && durableDocPath === "none")) {
|
|
71
|
+
return `Existing Memory Decisions ${heading[0].trim()} must use Durable doc path: none only with Durable doc disposition: memory`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
function extractReportSubsection(content, heading, nextHeading) {
|
|
77
|
+
const start = new RegExp(`^### ${escapeRegExp(heading)}\\s*$`, "m").exec(content);
|
|
78
|
+
if (!start || start.index === undefined) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const bodyStart = start.index + start[0].length;
|
|
82
|
+
if (nextHeading) {
|
|
83
|
+
const end = new RegExp(`^### ${escapeRegExp(nextHeading)}\\s*$`, "m")
|
|
84
|
+
.exec(content.slice(bodyStart));
|
|
85
|
+
return end?.index === undefined
|
|
86
|
+
? undefined
|
|
87
|
+
: content.slice(bodyStart, bodyStart + end.index);
|
|
88
|
+
}
|
|
89
|
+
const reviewedSet = /^Reviewed memory set:/m.exec(content.slice(bodyStart));
|
|
90
|
+
return content.slice(bodyStart, reviewedSet?.index === undefined ? content.length : bodyStart + reviewedSet.index);
|
|
91
|
+
}
|
|
92
|
+
function escapeRegExp(value) {
|
|
93
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
94
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { ARCHITECTURE_BRIEF_STATUSES, ARCHITECTURE_PLAN_RESULTS, DOCS_SYNC_DECISIONS, FINAL_ACCEPTANCE_DECISIONS, L3_ACTIONS, L3_REQUIRED_VALUES, STRICT_NONE_VALUE, TEST_RESULTS, renderArtifactOptions } from "../../shared/validation/artifact-contract.js";
|
|
1
2
|
export function renderArchitectureBriefTemplate(taskSlug) {
|
|
2
3
|
return `# Architecture Brief: ${taskSlug}
|
|
3
4
|
|
|
4
|
-
Architecture Brief Status:
|
|
5
|
+
Architecture Brief Status: ${renderArtifactOptions(ARCHITECTURE_BRIEF_STATUSES)}
|
|
5
6
|
|
|
6
7
|
## Accepted Outcome
|
|
7
8
|
|
|
@@ -17,7 +18,7 @@ TBD
|
|
|
17
18
|
|
|
18
19
|
## Unresolved User Decisions
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
${STRICT_NONE_VALUE}
|
|
21
22
|
|
|
22
23
|
## User Confirmation
|
|
23
24
|
|
|
@@ -27,7 +28,7 @@ TBD
|
|
|
27
28
|
export function renderArchitecturePlanTemplate(taskSlug) {
|
|
28
29
|
return `# Architecture Plan: ${taskSlug}
|
|
29
30
|
|
|
30
|
-
Planning Result:
|
|
31
|
+
Planning Result: ${renderArtifactOptions(ARCHITECTURE_PLAN_RESULTS)}
|
|
31
32
|
|
|
32
33
|
## Accepted Scope
|
|
33
34
|
|
|
@@ -100,10 +101,13 @@ TBD
|
|
|
100
101
|
Task-specific context and coder guidance go here, not in source-code comments.
|
|
101
102
|
Source-code comments should only describe durable behavior, contracts, invariants,
|
|
102
103
|
error boundaries, or non-obvious logic that should remain useful after this task.
|
|
104
|
+
Use an ID matching \`[A-Z]{2,6}-[0-9]{1,4}\`, choose exactly one Action
|
|
105
|
+
(\`create\`, \`change\`, or \`delete\`), put the repo-relative File path in
|
|
106
|
+
backticks, and enumerate every implementation item explicitly.
|
|
103
107
|
|
|
104
108
|
| ID | Action | File | Symbol Or Site | Coder Work | Allowed Implementation Freedom | Behavior / Contract Proof Point |
|
|
105
109
|
| --- | --- | --- | --- | --- | --- | --- |
|
|
106
|
-
|
|
|
110
|
+
| <ID> | <create|change|delete> | \`<repo-relative-file>\` | TBD | TBD | TBD | TBD |
|
|
107
111
|
|
|
108
112
|
## Scaffold Build Evidence
|
|
109
113
|
|
|
@@ -143,7 +147,7 @@ At task close, promote still-relevant confirmed issues to \`docs/known-issues.md
|
|
|
143
147
|
export function renderTestReportTemplate(taskSlug) {
|
|
144
148
|
return `# Test Report: ${taskSlug}
|
|
145
149
|
|
|
146
|
-
Test Result:
|
|
150
|
+
Test Result: ${renderArtifactOptions(TEST_RESULTS)}
|
|
147
151
|
|
|
148
152
|
## Evidence Reviewed
|
|
149
153
|
|
|
@@ -165,11 +169,11 @@ TBD
|
|
|
165
169
|
|
|
166
170
|
### Remaining Validation
|
|
167
171
|
|
|
168
|
-
|
|
172
|
+
${STRICT_NONE_VALUE}
|
|
169
173
|
|
|
170
174
|
## L3 Coverage
|
|
171
175
|
|
|
172
|
-
L3 Required:
|
|
176
|
+
L3 Required: ${renderArtifactOptions(L3_REQUIRED_VALUES)}
|
|
173
177
|
|
|
174
178
|
### Trigger Assessment
|
|
175
179
|
|
|
@@ -179,7 +183,7 @@ TBD
|
|
|
179
183
|
|
|
180
184
|
| Flow | Trigger | Case ID | Test File | Entry Point | Final Observable Result | Action | Result |
|
|
181
185
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
182
|
-
| TBD | TBD | TBD | TBD | TBD | TBD |
|
|
186
|
+
| TBD | TBD | TBD | TBD | TBD | TBD | <${renderArtifactOptions(L3_ACTIONS)}> | TBD |
|
|
183
187
|
|
|
184
188
|
### L3 Commands And Evidence
|
|
185
189
|
|
|
@@ -199,27 +203,27 @@ TBD
|
|
|
199
203
|
|
|
200
204
|
## Failed Expectations
|
|
201
205
|
|
|
202
|
-
|
|
206
|
+
${STRICT_NONE_VALUE}
|
|
203
207
|
|
|
204
208
|
## Reproduction Steps
|
|
205
209
|
|
|
206
|
-
|
|
210
|
+
${STRICT_NONE_VALUE}
|
|
207
211
|
|
|
208
212
|
## Skipped Checks With Reasons
|
|
209
213
|
|
|
210
|
-
|
|
214
|
+
${STRICT_NONE_VALUE}
|
|
211
215
|
|
|
212
216
|
## Coverage Gaps
|
|
213
217
|
|
|
214
|
-
|
|
218
|
+
${STRICT_NONE_VALUE}
|
|
215
219
|
|
|
216
220
|
## Blocking Validation Issues
|
|
217
221
|
|
|
218
|
-
|
|
222
|
+
${STRICT_NONE_VALUE}
|
|
219
223
|
|
|
220
224
|
## User Approval Evidence
|
|
221
225
|
|
|
222
|
-
|
|
226
|
+
${STRICT_NONE_VALUE}
|
|
223
227
|
`;
|
|
224
228
|
}
|
|
225
229
|
export function renderCoderCompletionTemplate(taskSlug) {
|
|
@@ -348,7 +352,7 @@ TBD
|
|
|
348
352
|
|
|
349
353
|
## Decision
|
|
350
354
|
|
|
351
|
-
|
|
355
|
+
${renderArtifactOptions(DOCS_SYNC_DECISIONS)}
|
|
352
356
|
`;
|
|
353
357
|
}
|
|
354
358
|
export function renderFinalAcceptanceTemplate(taskSlug) {
|
|
@@ -356,7 +360,7 @@ export function renderFinalAcceptanceTemplate(taskSlug) {
|
|
|
356
360
|
|
|
357
361
|
## Decision
|
|
358
362
|
|
|
359
|
-
|
|
363
|
+
${renderArtifactOptions(FINAL_ACCEPTANCE_DECISIONS)}
|
|
360
364
|
|
|
361
365
|
## Evidence Reviewed
|
|
362
366
|
|
|
@@ -78,7 +78,7 @@ ${renderRoleMemoryRules("architect")}
|
|
|
78
78
|
- \`Architecture Decision\`: use the required Changed Behavior Flow, Ownership, Data Flow, Lifecycle, Boundaries, Invariants, Failure Model, and Decision Rationale subsections. Describe why the design fits verified current code.
|
|
79
79
|
- \`Module/File Plan\`: list each affected module, changed or created file, file responsibility, why it is in scope, expected change, dependency direction, user-visible behavior change, durable comment needs, and every non-private callable surface intended for use outside its file. For every ledger item that consumes or sources cross-module data, name the module and symbol that owns or produces the data, trace the source-to-consumer path, and identify every field, parameter, accessor, trait method, command field, dependency, or other cross-file surface required by that path.
|
|
80
80
|
- \`Public Surface Impact\`: state changed APIs, routes, commands, events, exports, storage formats, configuration, UI behavior, visibility changes, side effects, error boundaries, expected callers, or explicitly state none.
|
|
81
|
-
- \`Scaffold Manifest\`: an item ledger — one entry per implementation item. An item is one
|
|
81
|
+
- \`Scaffold Manifest\`: an item ledger — one entry per implementation item. Use columns in the exact order \`ID | Action | File | ...\`; use an ID matching \`AA-1\` through \`AAAAAA-9999\`, an Action of exactly \`create\`, \`change\`, or \`delete\`, and a backticked repo-relative File path. An item is one created body or surface, one required change site — one contiguous edit region inside an existing body or surface — or one deletion of a body, site, or file. An item not in the ledger is not in the plan; coder must not implement it.
|
|
82
82
|
- Each ledger entry carries, in this column order: a unique stable ID such as \`SCF-001\`, action, exact file path, symbol or site, coder work, allowed implementation freedom, and a behavior/contract proof point. Per-file evidence, why-in-scope, and durable-comment needs live in the Module/File Plan, not in the ledger. Open-ended coverage language ("as work proceeds", "replicate", "etc.", "and others") is forbidden anywhere in the ledger.
|
|
83
83
|
- IDs and markers correspond one to one: every \`create\`, \`change\`, and \`delete\` entry has exactly one \`VCM:CODE <ID>\` marker pre-placed at its declared file and site; a \`delete\` marker sits on the code to be removed and leaves with it.
|
|
84
84
|
- The Scaffold Manifest is complete only when the ledger ID set and the tree's \`VCM:CODE\` ID set are equal, each ID appears exactly once on each side, and each marker sits in its declared file (\`.ai/tools/check-scaffold-ledger\` automates the check). Any mismatch means the plan is not complete.
|
|
@@ -116,7 +116,8 @@ ${renderRoleMemoryRules("architect")}
|
|
|
116
116
|
#### Planning Completion
|
|
117
117
|
|
|
118
118
|
- After the complete plan, scaffold, reconciliation, L0 evidence, and commits are ready, use the \`restart-architect\` skill before writing the completed Architect-to-PM route message.
|
|
119
|
-
-
|
|
119
|
+
- If VCM returns a \`memoryCandidatePath\`, use \`vcm-propose-memory\` to write the planning-session memory candidate to that path before routing. Include only verified, durable, reusable project knowledge from planning; do not record task narrative, temporary state, unverified conclusions, or Harness rules.
|
|
120
|
+
- After the required candidate is written, write the route message with both architecture artifacts and the plan, then end the turn. VCM keeps this session for any architecture-plan Gate revision and restarts it only after the Gate is accepted. Do not wait for or inspect the replacement session.
|
|
120
121
|
|
|
121
122
|
### Complete Task Planning
|
|
122
123
|
|
|
@@ -7,7 +7,7 @@ export function renderRootClaudeHarnessRules() {
|
|
|
7
7
|
- Project-manager uses \`vcm-task-state\` to declare the current workflow checkpoint. This state is recoverable context only; flow rules and task artifacts remain authoritative.
|
|
8
8
|
- Use \`vcm-long-running-validation\` for long-running validation. Follow the background job limits below.
|
|
9
9
|
- Use \`vcm-report-harness-issue\` when you notice a reusable VCM harness problem. Record feedback; do not contact Harness Engineer directly.
|
|
10
|
-
- The root \`<VCM-memory>\` block is shared project memory. Treat every \`<VCM-memory>\` block as read-only and use \`vcm-propose-memory\` only when VCM assigns
|
|
10
|
+
- The root \`<VCM-memory>\` block is shared project memory. Treat every \`<VCM-memory>\` block as read-only and use \`vcm-propose-memory\` only when VCM assigns an exact memory proposal or candidate path.
|
|
11
11
|
- Only the user may approve scope reduction, skipped required validation, Gate Review skip or override, skipped required docs sync, accepted unresolved task-scope risk, or weakening of baseline Harness rules. PM may record and route the user's approval but cannot grant it.
|
|
12
12
|
- Project-manager runs \`vcm-gate-review\` unconditionally at every Gate Review trigger point and on VCM Gate Review callbacks; the tool reports the authoritative enable state.
|
|
13
13
|
|
|
@@ -46,7 +46,18 @@ ${renderRoleMemoryRules("coder")}
|
|
|
46
46
|
- Use workers when the task has at least 20 \`VCM:CODE\` markers and the marker distribution can form at least two worker-sized groups.
|
|
47
47
|
- Under a complete scaffold, marker implementations are order-independent — signatures, types, and cross-item contracts are frozen by the scaffold — so never serialize worker-sized groups for presumed implementation-order dependencies. When a group's module-scoped checks need peers that are still unimplemented, narrow that worker's assigned validation scope instead of serializing.
|
|
48
48
|
- An item counts as blocked only when a genuine implementation attempt has produced objective compile/check evidence already reported under the failure rules; prediction never blocks an item. A blocked marker item never exempts the remaining markers from worker dispatch.
|
|
49
|
-
- Before invoking workers, count \`VCM:CODE\` markers by module and create one runtime state file per worker under \`.ai/vcm/coder-workers/tasks/<worker-id>.json\` with
|
|
49
|
+
- Before invoking workers, count \`VCM:CODE\` markers by module and create one runtime state file per worker under \`.ai/vcm/coder-workers/tasks/<worker-id>.json\` with exactly this initial shape:
|
|
50
|
+
|
|
51
|
+
\`\`\`json
|
|
52
|
+
{
|
|
53
|
+
"workerId": "<worker-id>",
|
|
54
|
+
"status": "running",
|
|
55
|
+
"reportPath": ".ai/vcm/coder-workers/reports/<worker-id>.md",
|
|
56
|
+
"handled": false
|
|
57
|
+
}
|
|
58
|
+
\`\`\`
|
|
59
|
+
|
|
60
|
+
- After a worker completes, its state must retain those fields, set \`status\` to \`completed\`, and add the exact \`commitHash\` from its report. Only Coder changes \`handled\` to \`true\` after inspecting that report and commit.
|
|
50
61
|
- Create one worker task for each module with more than 10 \`VCM:CODE\` markers.
|
|
51
62
|
- Group modules with 10 or fewer \`VCM:CODE\` markers into one small-modules worker when their combined marker count is more than 10.
|
|
52
63
|
- If the combined small-module marker count is 10 or fewer, Coder handles those modules directly after worker results return.
|