vibe-coding-master 0.7.39 → 0.7.40
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 +11 -6
- package/dist/backend/adapters/claude-adapter.js +1 -6
- package/dist/backend/api/artifact-routes.js +72 -1
- package/dist/backend/api/runtime-state-routes.js +17 -5
- package/dist/backend/api/task-routes.js +6 -0
- package/dist/backend/api/workflow-control-routes.js +56 -0
- package/dist/backend/cli/install-vcm-harness.js +21 -31
- package/dist/backend/server.js +21 -5
- package/dist/backend/services/artifact-service.js +309 -5
- package/dist/backend/services/auto-memory-service.js +2 -2
- package/dist/backend/services/gate-review-service.js +81 -33
- package/dist/backend/services/harness-feedback-service.js +11 -4
- package/dist/backend/services/harness-service.js +17 -31
- package/dist/backend/services/message-service.js +105 -37
- package/dist/backend/services/status-service.js +6 -0
- package/dist/backend/services/workflow-control-service.js +854 -0
- package/dist/backend/templates/handoff.js +158 -3
- package/dist/backend/templates/harness/architect-agent.js +2 -2
- package/dist/backend/templates/harness/check-scaffold-ledger.js +105 -39
- package/dist/backend/templates/harness/claude-root.js +9 -1
- package/dist/backend/templates/harness/coder-agent.js +1 -2
- package/dist/backend/templates/harness/coder-worker-agent.js +1 -1
- package/dist/backend/templates/harness/gate-review.js +3 -4
- package/dist/backend/templates/harness/harness-engineer-agent.js +18 -1
- package/dist/backend/templates/harness/project-manager-agent.js +3 -2
- package/dist/backend/templates/harness/tester-agent.js +1 -0
- package/dist/backend/templates/harness/vcm-architecture-interview-skill.js +5 -0
- package/dist/backend/templates/harness/vcm-code-navigation-skill.js +1 -2
- package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +2 -2
- package/dist/backend/templates/harness/vcm-long-running-validation-skill.js +4 -2
- package/dist/backend/templates/harness/vcm-propose-memory-skill.js +4 -2
- package/dist/backend/templates/harness/vcm-report-harness-issue-skill.js +2 -1
- package/dist/backend/templates/harness/vcm-route-message-skill.js +4 -7
- package/dist/backend/templates/harness/vcm-task-state-skill.js +2 -13
- package/dist/backend/templates/harness/vcm-workflow-review-skill.js +62 -0
- package/dist/backend/templates/message-envelope.js +4 -4
- package/dist/shared/types/workflow.js +7 -1
- package/dist/shared/validation/artifact-check.js +99 -95
- package/dist/shared/validation/artifact-contract.js +9 -0
- package/dist/shared/validation/artifact-registry.js +211 -0
- package/dist-frontend/assets/{index-DkM0mnQD.js → index-Bocc2DWF.js} +34 -34
- package/dist-frontend/assets/{index-CXSOe-NN.css → index-C_XHGNBD.css} +1 -1
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/scripts/harness-tools/run-long-check +38 -8
- package/scripts/harness-tools/vcm-artifact +148 -0
- package/scripts/harness-tools/vcm-bash-guard +42 -47
- package/scripts/harness-tools/watch-job +58 -4
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { DISPATCHABLE_ROLES } from "../../shared/constants.js";
|
|
2
|
+
import { DISPATCHABLE_ROLES, VCM_ROLE_NAMES } from "../../shared/constants.js";
|
|
3
3
|
import { checkMarkdownArtifact } from "../../shared/validation/artifact-check.js";
|
|
4
|
+
import { getArtifactDefinition, isArtifactKind } from "../../shared/validation/artifact-registry.js";
|
|
4
5
|
import { VcmError } from "../errors.js";
|
|
5
6
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
6
|
-
import { renderArchitectureBriefTemplate, renderArchitecturePlanTemplate, renderArchitectDebugTemplate, renderCoderCompletionTemplate, renderDocsSyncReportTemplate, renderFinalAcceptanceTemplate, renderKnownIssuesTemplate, renderMessageRouteTemplate, renderTestReportTemplate } from "../templates/handoff.js";
|
|
7
|
+
import { renderArchitectureBriefTemplate, renderArchitectureDiagnosisTemplate, renderArchitectureEvidenceTemplate, renderArchitecturePlanTemplate, renderArchitectDebugTemplate, renderCoderCompletionTemplate, renderDocsSyncReportTemplate, renderFinalAcceptanceTemplate, renderKnownIssuesTemplate, renderPlanningProgressTemplate, renderMessageRouteTemplate, renderTestReportTemplate, renderWorkflowProgressTemplate } from "../templates/handoff.js";
|
|
7
8
|
import { renderRoleCommandTemplate } from "../templates/role-command.js";
|
|
9
|
+
import { validateMemoryProposal } from "./memory-proposal-validation.js";
|
|
8
10
|
const ARTIFACT_PATH_KEYS = [
|
|
9
11
|
["architecture-brief", "architectureBriefPath"],
|
|
12
|
+
["architecture-evidence", "architectureEvidencePath"],
|
|
13
|
+
["planning-progress", "planningProgressPath"],
|
|
10
14
|
["architecture-plan", "architecturePlanPath"],
|
|
11
15
|
["known-issues", "knownIssuesPath"],
|
|
16
|
+
["coder-completion", "coderCompletionPath"],
|
|
17
|
+
["architect-debug", "architectDebugPath"],
|
|
18
|
+
["architecture-diagnosis", "architectureDiagnosisPath"],
|
|
12
19
|
["test-report", "testReportPath"],
|
|
13
20
|
["docs-sync-report", "docsSyncReportPath"],
|
|
21
|
+
["workflow-progress", "workflowProgressPath"],
|
|
14
22
|
["final-acceptance", "finalAcceptancePath"]
|
|
15
23
|
];
|
|
16
24
|
const ROLE_COMMAND_PLACEHOLDER_PATTERN = /(^|\n)\s*(TBD|status:\s*draft)\s*(\n|$)/i;
|
|
@@ -22,7 +30,7 @@ const DEFAULT_MESSAGE_ROUTES = [
|
|
|
22
30
|
["coder", "project-manager"],
|
|
23
31
|
["tester", "project-manager"]
|
|
24
32
|
];
|
|
25
|
-
export function createArtifactService(fs) {
|
|
33
|
+
export function createArtifactService(fs, deps = {}) {
|
|
26
34
|
return {
|
|
27
35
|
getHandoffPaths(_repoRoot, handoffDir) {
|
|
28
36
|
const roleCommandsDir = path.posix.join(handoffDir, "role-commands");
|
|
@@ -38,10 +46,16 @@ export function createArtifactService(fs) {
|
|
|
38
46
|
},
|
|
39
47
|
messageRoutePaths: getDefaultMessageRoutePaths(messagesDir),
|
|
40
48
|
architectureBriefPath: path.posix.join(handoffDir, "architecture-brief.md"),
|
|
49
|
+
architectureEvidencePath: path.posix.join(handoffDir, "architecture-evidence.md"),
|
|
50
|
+
planningProgressPath: path.posix.join(handoffDir, "planning-progress.md"),
|
|
41
51
|
architecturePlanPath: path.posix.join(handoffDir, "architecture-plan.md"),
|
|
42
52
|
knownIssuesPath: path.posix.join(handoffDir, "known-issues.md"),
|
|
53
|
+
coderCompletionPath: path.posix.join(handoffDir, "coder-completion.md"),
|
|
54
|
+
architectDebugPath: path.posix.join(handoffDir, "architect-debug.md"),
|
|
55
|
+
architectureDiagnosisPath: path.posix.join(handoffDir, "architecture-diagnosis.md"),
|
|
43
56
|
testReportPath: path.posix.join(handoffDir, "test-report.md"),
|
|
44
57
|
docsSyncReportPath: path.posix.join(handoffDir, "docs-sync-report.md"),
|
|
58
|
+
workflowProgressPath: path.posix.join(handoffDir, "workflow-progress.md"),
|
|
45
59
|
finalAcceptancePath: path.posix.join(handoffDir, "final-acceptance.md")
|
|
46
60
|
};
|
|
47
61
|
},
|
|
@@ -62,12 +76,16 @@ export function createArtifactService(fs) {
|
|
|
62
76
|
[paths.roleCommandPaths.coder, renderRoleCommandTemplate(input.taskSlug, "coder", input.repoRoot, input.branch)],
|
|
63
77
|
[paths.roleCommandPaths.tester, renderRoleCommandTemplate(input.taskSlug, "tester", input.repoRoot, input.branch)],
|
|
64
78
|
[paths.architectureBriefPath, renderArchitectureBriefTemplate(input.taskSlug)],
|
|
79
|
+
[paths.architectureEvidencePath, renderArchitectureEvidenceTemplate(input.taskSlug)],
|
|
80
|
+
[paths.planningProgressPath, renderPlanningProgressTemplate(input.taskSlug)],
|
|
65
81
|
[paths.architecturePlanPath, renderArchitecturePlanTemplate(input.taskSlug)],
|
|
66
82
|
[paths.knownIssuesPath, renderKnownIssuesTemplate(input.taskSlug)],
|
|
67
|
-
[
|
|
68
|
-
[
|
|
83
|
+
[paths.coderCompletionPath, renderCoderCompletionTemplate(input.taskSlug)],
|
|
84
|
+
[paths.architectDebugPath, renderArchitectDebugTemplate(input.taskSlug)],
|
|
85
|
+
[paths.architectureDiagnosisPath, renderArchitectureDiagnosisTemplate(input.taskSlug)],
|
|
69
86
|
[paths.testReportPath, renderTestReportTemplate(input.taskSlug)],
|
|
70
87
|
[paths.docsSyncReportPath, renderDocsSyncReportTemplate(input.taskSlug)],
|
|
88
|
+
[paths.workflowProgressPath, renderWorkflowProgressTemplate(input.taskSlug)],
|
|
71
89
|
[paths.finalAcceptancePath, renderFinalAcceptanceTemplate(input.taskSlug)],
|
|
72
90
|
...Object.values(paths.messageRoutePaths).map((messagePath) => [
|
|
73
91
|
messagePath,
|
|
@@ -163,9 +181,295 @@ export function createArtifactService(fs) {
|
|
|
163
181
|
async saveRoleCommand(input) {
|
|
164
182
|
const paths = this.getHandoffPaths(input.repoRoot, input.handoffDir);
|
|
165
183
|
await fs.writeText(resolveRepoPath(input.repoRoot, paths.roleCommandPaths[input.role]), input.content);
|
|
184
|
+
},
|
|
185
|
+
async submitArtifact(input) {
|
|
186
|
+
const normalized = normalizeSubmissionContent(input.content);
|
|
187
|
+
if (!normalized.trim()) {
|
|
188
|
+
throw artifactRejected(input.kind, ["Artifact content is empty."]);
|
|
189
|
+
}
|
|
190
|
+
if (isArtifactKind(input.kind)) {
|
|
191
|
+
const definition = getArtifactDefinition(input.kind);
|
|
192
|
+
if (definition.owner !== input.role) {
|
|
193
|
+
throw new VcmError({
|
|
194
|
+
code: "ARTIFACT_OWNER_MISMATCH",
|
|
195
|
+
message: `${input.kind} is owned by ${definition.owner}, not ${input.role}.`,
|
|
196
|
+
statusCode: 403
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const artifactPath = path.posix.join(input.handoffDir, definition.fileName);
|
|
200
|
+
const check = checkMarkdownArtifact(input.kind, artifactPath, normalized, { mode: input.mode });
|
|
201
|
+
const errors = artifactCheckErrors(check, input.mode);
|
|
202
|
+
if (errors.length > 0) {
|
|
203
|
+
throw artifactRejected(input.kind, errors);
|
|
204
|
+
}
|
|
205
|
+
if (input.kind === "workflow-progress") {
|
|
206
|
+
if (input.mode !== "final") {
|
|
207
|
+
throw artifactRejected(input.kind, ["Workflow Progress must be submitted in final mode."]);
|
|
208
|
+
}
|
|
209
|
+
if (!deps.workflowControlService) {
|
|
210
|
+
throw new VcmError({
|
|
211
|
+
code: "WORKFLOW_CONTROL_UNAVAILABLE",
|
|
212
|
+
message: "Workflow Control is unavailable.",
|
|
213
|
+
statusCode: 503
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const submitted = await deps.workflowControlService.submitProgress({
|
|
217
|
+
taskRepoRoot: input.repoRoot,
|
|
218
|
+
stateRoot: input.stateRoot ?? ".ai/vcm",
|
|
219
|
+
handoffDir: input.handoffDir,
|
|
220
|
+
taskSlug: input.taskSlug
|
|
221
|
+
}, normalized);
|
|
222
|
+
return {
|
|
223
|
+
ok: true,
|
|
224
|
+
kind: input.kind,
|
|
225
|
+
mode: input.mode,
|
|
226
|
+
path: submitted.path,
|
|
227
|
+
status: "accepted"
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
await writeAtomic(fs, resolveRepoPath(input.repoRoot, artifactPath), normalized);
|
|
231
|
+
return {
|
|
232
|
+
ok: true,
|
|
233
|
+
kind: input.kind,
|
|
234
|
+
mode: input.mode,
|
|
235
|
+
path: artifactPath,
|
|
236
|
+
status: check.status
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const dynamic = await validateDynamicArtifact(fs, input, normalized, deps.workflowControlService);
|
|
240
|
+
await writeAtomic(fs, resolveRepoPath(dynamic.root, dynamic.path), normalized);
|
|
241
|
+
return {
|
|
242
|
+
ok: true,
|
|
243
|
+
kind: dynamic.kind,
|
|
244
|
+
mode: input.mode,
|
|
245
|
+
path: dynamic.path,
|
|
246
|
+
status: "accepted"
|
|
247
|
+
};
|
|
166
248
|
}
|
|
167
249
|
};
|
|
168
250
|
}
|
|
251
|
+
function artifactCheckErrors(check, mode) {
|
|
252
|
+
const errors = [
|
|
253
|
+
...check.missingHeadings.map((heading) => `Missing required heading: ${heading}.`),
|
|
254
|
+
...check.invalidFields
|
|
255
|
+
];
|
|
256
|
+
if (mode === "final" && check.hasPlaceholder) {
|
|
257
|
+
errors.push("Final artifacts must not contain TBD, draft, or not-run placeholders.");
|
|
258
|
+
}
|
|
259
|
+
return errors;
|
|
260
|
+
}
|
|
261
|
+
function artifactRejected(kind, errors) {
|
|
262
|
+
return new VcmError({
|
|
263
|
+
code: "ARTIFACT_VALIDATION_FAILED",
|
|
264
|
+
message: `${kind} was not written because validation failed:\n${errors.map((error) => `- ${error}`).join("\n")}`,
|
|
265
|
+
statusCode: 422,
|
|
266
|
+
hint: "Correct the candidate and submit it again with .ai/tools/vcm-artifact."
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
async function validateDynamicArtifact(fs, input, content, workflowControlService) {
|
|
270
|
+
if (input.mode !== "final") {
|
|
271
|
+
throw artifactRejected(input.kind, ["Dynamic artifacts must be submitted in final mode."]);
|
|
272
|
+
}
|
|
273
|
+
const artifactPath = normalizeRelativeArtifactPath(input.artifactPath);
|
|
274
|
+
if (input.kind === "route-message") {
|
|
275
|
+
const route = DEFAULT_MESSAGE_ROUTES.find(([fromRole, toRole]) => artifactPath === path.posix.join(input.handoffDir, "messages", `${fromRole}-${toRole}.md`));
|
|
276
|
+
if (!route || route[0] !== input.role) {
|
|
277
|
+
throw artifactRejected(input.kind, ["Route-message path must name the submitting role as the sender."]);
|
|
278
|
+
}
|
|
279
|
+
validateRouteMessage(content);
|
|
280
|
+
if (input.role === "project-manager") {
|
|
281
|
+
if (!workflowControlService) {
|
|
282
|
+
throw new VcmError({
|
|
283
|
+
code: "WORKFLOW_CONTROL_UNAVAILABLE",
|
|
284
|
+
message: "Workflow Control is unavailable.",
|
|
285
|
+
statusCode: 503
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
await workflowControlService.assertRouteAuthorized({
|
|
289
|
+
taskRepoRoot: input.repoRoot,
|
|
290
|
+
stateRoot: input.stateRoot ?? ".ai/vcm",
|
|
291
|
+
handoffDir: input.handoffDir,
|
|
292
|
+
taskSlug: input.taskSlug,
|
|
293
|
+
routePath: artifactPath,
|
|
294
|
+
targetRole: route[1]
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
return { kind: input.kind, root: input.repoRoot, path: artifactPath };
|
|
298
|
+
}
|
|
299
|
+
if (input.kind === "coder-worker-report") {
|
|
300
|
+
if (input.role !== "coder" || !/^\.ai\/vcm\/coder-workers\/reports\/[A-Za-z0-9._-]+\.md$/.test(artifactPath)) {
|
|
301
|
+
throw artifactRejected(input.kind, ["Coder Worker reports must use the assigned .ai/vcm/coder-workers/reports/<worker-id>.md path."]);
|
|
302
|
+
}
|
|
303
|
+
validateCoderWorkerReport(content);
|
|
304
|
+
return { kind: input.kind, root: input.repoRoot, path: artifactPath };
|
|
305
|
+
}
|
|
306
|
+
if (input.kind === "gate-review-report") {
|
|
307
|
+
if (input.role !== "reviewer" || !/^\.ai\/vcm\/gate-reviews\/requests\/[A-Za-z0-9._-]+\.report\.md$/.test(artifactPath)) {
|
|
308
|
+
throw artifactRejected(input.kind, ["Reviewer must submit the assigned request report path."]);
|
|
309
|
+
}
|
|
310
|
+
await validateGateReviewReport(fs, input.repoRoot, artifactPath, content);
|
|
311
|
+
return { kind: input.kind, root: input.repoRoot, path: artifactPath };
|
|
312
|
+
}
|
|
313
|
+
if (input.kind === "memory-proposal") {
|
|
314
|
+
if (!VCM_ROLE_NAMES.includes(input.role)) {
|
|
315
|
+
throw artifactRejected(input.kind, ["Only a VCM workflow role may submit a Memory Proposal."]);
|
|
316
|
+
}
|
|
317
|
+
if (!/^\.ai\/vcm\/memory-review\/(?:runs\/[A-Za-z0-9._-]+\/drafts|candidates)\/[A-Za-z0-9._-]+\.md$/.test(artifactPath)) {
|
|
318
|
+
throw artifactRejected(input.kind, ["Memory proposal path is outside the VCM memory-review draft locations."]);
|
|
319
|
+
}
|
|
320
|
+
const memoryError = validateMemoryProposal(content);
|
|
321
|
+
if (memoryError) {
|
|
322
|
+
throw artifactRejected(input.kind, [`Memory proposal ${memoryError}.`]);
|
|
323
|
+
}
|
|
324
|
+
return { kind: input.kind, root: input.repoRoot, path: artifactPath };
|
|
325
|
+
}
|
|
326
|
+
if (input.kind === "harness-feedback") {
|
|
327
|
+
if (!VCM_ROLE_NAMES.includes(input.role)) {
|
|
328
|
+
throw artifactRejected(input.kind, ["Only a VCM workflow role may submit Harness Feedback."]);
|
|
329
|
+
}
|
|
330
|
+
if (!/^\.ai\/vcm\/harness-feedback\/pending\/[A-Za-z0-9._-]+\.md$/.test(artifactPath)) {
|
|
331
|
+
throw artifactRejected(input.kind, ["Harness Feedback path must be under .ai/vcm/harness-feedback/pending/. "]);
|
|
332
|
+
}
|
|
333
|
+
validateHarnessFeedback(content);
|
|
334
|
+
return { kind: input.kind, root: input.baseRepoRoot, path: artifactPath };
|
|
335
|
+
}
|
|
336
|
+
if (input.kind === "retrospective-report") {
|
|
337
|
+
if (input.role !== "harness-engineer") {
|
|
338
|
+
throw artifactRejected(input.kind, ["Only Harness Engineer may submit a retrospective report."]);
|
|
339
|
+
}
|
|
340
|
+
if (!/^\.ai\/vcm\/harness-feedback\/task-retrospectives\/[A-Za-z0-9._-]+\.md$/.test(artifactPath)) {
|
|
341
|
+
throw artifactRejected(input.kind, ["Retrospective report path must be under .ai/vcm/harness-feedback/task-retrospectives/."]);
|
|
342
|
+
}
|
|
343
|
+
validateRetrospectiveReport(content);
|
|
344
|
+
return { kind: input.kind, root: input.baseRepoRoot, path: artifactPath };
|
|
345
|
+
}
|
|
346
|
+
throw new VcmError({
|
|
347
|
+
code: "ARTIFACT_KIND_INVALID",
|
|
348
|
+
message: `Unknown managed artifact kind: ${input.kind}`,
|
|
349
|
+
statusCode: 400
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
function validateRetrospectiveReport(content) {
|
|
353
|
+
const errors = getRetrospectiveReportErrors(content);
|
|
354
|
+
if (errors.length > 0)
|
|
355
|
+
throw artifactRejected("retrospective-report", errors);
|
|
356
|
+
}
|
|
357
|
+
export function getRetrospectiveReportErrors(content) {
|
|
358
|
+
const errors = [];
|
|
359
|
+
if (!/^# Task Harness Retrospective(?::\s*.+)?\s*$/m.test(content)) {
|
|
360
|
+
errors.push("Retrospective report requires '# Task Harness Retrospective: <task>'.");
|
|
361
|
+
}
|
|
362
|
+
for (const heading of ["Findings", "Feedback Dispositions", "Recommended Harness Changes", "VCM Issue Drafts"]) {
|
|
363
|
+
if (!new RegExp(`^## ${escapeRegExp(heading)}\\s*$`, "m").test(content)) {
|
|
364
|
+
errors.push(`Missing required section: ${heading}.`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return errors;
|
|
368
|
+
}
|
|
369
|
+
function validateCoderWorkerReport(content) {
|
|
370
|
+
const errors = [];
|
|
371
|
+
if (!/^# Coder Worker Report:\s*\S.+$/m.test(content)) {
|
|
372
|
+
errors.push("Coder Worker report requires '# Coder Worker Report: <worker-id>'.");
|
|
373
|
+
}
|
|
374
|
+
if (!/^Worker State:\s*completed\s*$/m.test(content)) {
|
|
375
|
+
errors.push("Worker State must be completed.");
|
|
376
|
+
}
|
|
377
|
+
if (!/^Implementation Result:\s*(success|has_failed_items)\s*$/m.test(content)) {
|
|
378
|
+
errors.push("Implementation Result must be success|has_failed_items.");
|
|
379
|
+
}
|
|
380
|
+
for (const heading of [
|
|
381
|
+
"Assigned Scope", "Item Dispositions", "Files Changed", "Tests Added Or Updated",
|
|
382
|
+
"L0/L1 Checks", "Commit", "Skipped Assigned Checks", "Objective Failures"
|
|
383
|
+
]) {
|
|
384
|
+
if (!new RegExp(`^## ${escapeRegExp(heading)}\\s*$`, "m").test(content)) {
|
|
385
|
+
errors.push(`Missing required section: ${heading}.`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (errors.length > 0)
|
|
389
|
+
throw artifactRejected("coder-worker-report", errors);
|
|
390
|
+
}
|
|
391
|
+
function validateRouteMessage(content) {
|
|
392
|
+
const type = /^type:\s*(\S+)\s*$/m.exec(content)?.[1];
|
|
393
|
+
const allowed = new Set(["task", "question", "revise", "cancel", "result", "blocked", "finding"]);
|
|
394
|
+
if (!type || !allowed.has(type)) {
|
|
395
|
+
throw artifactRejected("route-message", ["Frontmatter type must be one of task|question|revise|cancel|result|blocked|finding."]);
|
|
396
|
+
}
|
|
397
|
+
if (!content.startsWith("---\n") || !/\n---\n/.test(content) || !content.split(/\n---\n/, 2)[1]?.trim()) {
|
|
398
|
+
throw artifactRejected("route-message", ["Route message requires frontmatter and a non-empty body."]);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async function validateGateReviewReport(fs, repoRoot, artifactPath, content) {
|
|
402
|
+
const requestId = path.posix.basename(artifactPath, ".report.md");
|
|
403
|
+
const requestPath = path.posix.join(".ai/vcm/gate-reviews/requests", `${requestId}.json`);
|
|
404
|
+
const absoluteRequestPath = resolveRepoPath(repoRoot, requestPath);
|
|
405
|
+
if (!(await fs.pathExists(absoluteRequestPath))) {
|
|
406
|
+
throw artifactRejected("gate-review-report", [`Gate request does not exist: ${requestPath}.`]);
|
|
407
|
+
}
|
|
408
|
+
const request = await fs.readJson(absoluteRequestPath);
|
|
409
|
+
const fields = Object.fromEntries(["Gate", "Request", "Decision", "Summary"].map((field) => [
|
|
410
|
+
field,
|
|
411
|
+
new RegExp(`^${field}:\\s*(.+?)\\s*$`, "mi").exec(content)?.[1]?.trim()
|
|
412
|
+
]));
|
|
413
|
+
const errors = [];
|
|
414
|
+
if (fields.Gate !== request.gate)
|
|
415
|
+
errors.push(`Gate must be ${request.gate ?? "the requested gate"}.`);
|
|
416
|
+
if (fields.Request !== request.requestId)
|
|
417
|
+
errors.push(`Request must be ${request.requestId ?? requestId}.`);
|
|
418
|
+
if (fields.Decision !== "approve" && fields.Decision !== "request_changes")
|
|
419
|
+
errors.push("Decision must be approve|request_changes.");
|
|
420
|
+
if (!fields.Summary)
|
|
421
|
+
errors.push("Summary is required.");
|
|
422
|
+
const requiredSection = request.gate === "architecture-plan"
|
|
423
|
+
? "Architecture Analysis"
|
|
424
|
+
: request.gate === "validation-adequacy"
|
|
425
|
+
? "Validation Analysis"
|
|
426
|
+
: "Code Diff Analysis";
|
|
427
|
+
if (!new RegExp(`^## ${escapeRegExp(requiredSection)}\\s*$`, "m").test(content)) {
|
|
428
|
+
errors.push(`Missing required section: ${requiredSection}.`);
|
|
429
|
+
}
|
|
430
|
+
if (!/^## Findings\s*$/m.test(content))
|
|
431
|
+
errors.push("Missing required section: Findings.");
|
|
432
|
+
if (errors.length > 0)
|
|
433
|
+
throw artifactRejected("gate-review-report", errors);
|
|
434
|
+
}
|
|
435
|
+
function validateHarnessFeedback(content) {
|
|
436
|
+
const errors = [];
|
|
437
|
+
if (!/^#\s+\S.+$/m.test(content))
|
|
438
|
+
errors.push("Harness Feedback requires a title.");
|
|
439
|
+
for (const field of [
|
|
440
|
+
"Reporter role", "Task slug", "Summary", "Observed problem", "Expected behavior",
|
|
441
|
+
"Evidence", "Suspected harness area", "Impact", "Urgency"
|
|
442
|
+
]) {
|
|
443
|
+
if (!new RegExp(`^- ${escapeRegExp(field)}:\\s*\\S`, "m").test(content)) {
|
|
444
|
+
errors.push(`Harness Feedback is missing ${field}.`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (!/^- Urgency:\s*(low|medium|high)\s*$/m.test(content)) {
|
|
448
|
+
errors.push("Urgency must be low|medium|high.");
|
|
449
|
+
}
|
|
450
|
+
if (errors.length > 0)
|
|
451
|
+
throw artifactRejected("harness-feedback", errors);
|
|
452
|
+
}
|
|
453
|
+
function normalizeRelativeArtifactPath(value) {
|
|
454
|
+
const normalized = value?.replaceAll("\\", "/").replace(/^\.\//, "").trim();
|
|
455
|
+
if (!normalized || path.posix.isAbsolute(normalized) || normalized.split("/").includes("..")) {
|
|
456
|
+
throw artifactRejected("dynamic artifact", ["A safe repository-relative --path is required."]);
|
|
457
|
+
}
|
|
458
|
+
return normalized;
|
|
459
|
+
}
|
|
460
|
+
function normalizeSubmissionContent(content) {
|
|
461
|
+
return `${content.replace(/\r\n/g, "\n").trimEnd()}\n`;
|
|
462
|
+
}
|
|
463
|
+
async function writeAtomic(fs, absolutePath, content) {
|
|
464
|
+
if (fs.writeTextAtomic) {
|
|
465
|
+
await fs.writeTextAtomic(absolutePath, content);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
await fs.writeText(absolutePath, content);
|
|
469
|
+
}
|
|
470
|
+
function escapeRegExp(value) {
|
|
471
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
472
|
+
}
|
|
169
473
|
function getLegacyRoleCommandPath(roleCommandsDir, role) {
|
|
170
474
|
return path.posix.join(roleCommandsDir, `${role}-command.md`);
|
|
171
475
|
}
|
|
@@ -905,12 +905,12 @@ function buildRoleDraftPrompt(taskRepoRoot, state, draft, planningCandidatePath)
|
|
|
905
905
|
"Review that candidate against final task evidence. Carry forward only facts that remain verified after implementation and testing."
|
|
906
906
|
]
|
|
907
907
|
: []),
|
|
908
|
-
`
|
|
908
|
+
`Assigned proposal path: ${resolveRepoPath(taskRepoRoot, draft.path)}`
|
|
909
909
|
];
|
|
910
910
|
return [
|
|
911
911
|
...prompt,
|
|
912
912
|
"",
|
|
913
|
-
"End the turn after
|
|
913
|
+
"End the turn after VCM accepts the proposal."
|
|
914
914
|
].join("\n");
|
|
915
915
|
}
|
|
916
916
|
function requireMemoryFileDefinition(filePath) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { CODE_DIFF_FINDING_SCOPES, CODE_DIFF_SOURCES, GATE_REVIEW_GATES } from "../../shared/types/gate-review.js";
|
|
4
|
-
import { checkMarkdownArtifact } from "../../shared/validation/artifact-check.js";
|
|
4
|
+
import { checkMarkdownArtifact, readArtifactSectionValue } from "../../shared/validation/artifact-check.js";
|
|
5
5
|
import { VcmError } from "../errors.js";
|
|
6
6
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
7
7
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
@@ -292,6 +292,32 @@ export function createGateReviewService(deps) {
|
|
|
292
292
|
message: `${coreInput.path} is ${coreInput.status}.`
|
|
293
293
|
};
|
|
294
294
|
}
|
|
295
|
+
if (gate === "architecture-plan") {
|
|
296
|
+
const architecturePlanError = await readArchitecturePlanError(deps.fs, context.taskRepoRoot);
|
|
297
|
+
if (architecturePlanError) {
|
|
298
|
+
index = applyGateState(index, gate, {
|
|
299
|
+
status: "failed",
|
|
300
|
+
decision: undefined,
|
|
301
|
+
error: architecturePlanError,
|
|
302
|
+
exceptionReason: undefined,
|
|
303
|
+
requestId: undefined,
|
|
304
|
+
requestPath: undefined,
|
|
305
|
+
inputHash: undefined,
|
|
306
|
+
requestedAt: undefined,
|
|
307
|
+
startedAt: undefined,
|
|
308
|
+
completedAt: now(),
|
|
309
|
+
callbackStatus: "not_sent",
|
|
310
|
+
callbackError: undefined
|
|
311
|
+
}, now(), true);
|
|
312
|
+
await saveIndex(deps.fs, context.taskRepoRoot, index);
|
|
313
|
+
return {
|
|
314
|
+
status: "failed_to_start",
|
|
315
|
+
gate,
|
|
316
|
+
record: index.gates[gate],
|
|
317
|
+
message: architecturePlanError
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
295
321
|
if (gate === "validation-adequacy") {
|
|
296
322
|
const validationReportError = await readValidationReportError(deps.fs, context.taskRepoRoot);
|
|
297
323
|
if (validationReportError) {
|
|
@@ -319,7 +345,7 @@ export function createGateReviewService(deps) {
|
|
|
319
345
|
}
|
|
320
346
|
}
|
|
321
347
|
if (gate === "code-diff") {
|
|
322
|
-
const prerequisiteError = await readCodeDiffPrerequisiteError(deps, context, index);
|
|
348
|
+
const prerequisiteError = await readCodeDiffPrerequisiteError(deps, context, index, codeDiffSource);
|
|
323
349
|
if (prerequisiteError) {
|
|
324
350
|
index = applyGateState(index, gate, {
|
|
325
351
|
status: "failed",
|
|
@@ -1234,19 +1260,18 @@ async function readCoreInputArtifact(fs, taskRepoRoot, gate) {
|
|
|
1234
1260
|
async function readArchitectureBriefError(fs, taskRepoRoot) {
|
|
1235
1261
|
const relativePath = ".ai/vcm/handoffs/architecture-brief.md";
|
|
1236
1262
|
const absolutePath = resolveRepoPath(taskRepoRoot, relativePath);
|
|
1237
|
-
|
|
1263
|
+
const content = await fs.pathExists(absolutePath) ? await fs.readText(absolutePath) : null;
|
|
1264
|
+
if (content === null)
|
|
1238
1265
|
return `${relativePath} is missing. Complete Architect Interview before architecture planning.`;
|
|
1239
|
-
}
|
|
1240
|
-
const content = await fs.readText(absolutePath);
|
|
1241
|
-
const check = checkMarkdownArtifact("architecture-brief", relativePath, content);
|
|
1242
|
-
if (check.status !== "ok") {
|
|
1243
|
-
return `${relativePath} is incomplete and cannot start architecture-plan review. ${formatArtifactCheckFailure(check)}`;
|
|
1244
|
-
}
|
|
1245
1266
|
const status = /^\s*Architecture Brief Status\s*:\s*(.+?)\s*$/im.exec(content)?.[1]?.trim();
|
|
1246
1267
|
if (status?.toLowerCase() !== "confirmed") {
|
|
1247
1268
|
return `${relativePath} is not confirmed and cannot start architecture-plan review. `
|
|
1248
1269
|
+ `Architecture Brief Status must be exactly "confirmed"; found ${renderFoundValue(status)}.`;
|
|
1249
1270
|
}
|
|
1271
|
+
const check = checkMarkdownArtifact("architecture-brief", relativePath, content);
|
|
1272
|
+
if (check.status !== "ok") {
|
|
1273
|
+
return `${relativePath} is incomplete and cannot start architecture-plan review. ${formatArtifactCheckFailure(check)}`;
|
|
1274
|
+
}
|
|
1250
1275
|
return undefined;
|
|
1251
1276
|
}
|
|
1252
1277
|
async function readValidationReportError(fs, taskRepoRoot) {
|
|
@@ -1270,41 +1295,63 @@ async function readValidationReportError(fs, taskRepoRoot) {
|
|
|
1270
1295
|
}
|
|
1271
1296
|
return undefined;
|
|
1272
1297
|
}
|
|
1273
|
-
async function readCodeDiffPrerequisiteError(deps, context, index) {
|
|
1298
|
+
async function readCodeDiffPrerequisiteError(deps, context, index, source) {
|
|
1274
1299
|
const reportError = await readValidationReportError(deps.fs, context.taskRepoRoot);
|
|
1275
1300
|
if (reportError) {
|
|
1276
1301
|
return "code-diff requires completed Tester validation. " + reportError;
|
|
1277
1302
|
}
|
|
1278
1303
|
const validationGate = index.gates["validation-adequacy"];
|
|
1279
|
-
if (
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
}
|
|
1288
|
-
const currentValidationHash = await computeInputHash(deps, context.taskRepoRoot, "validation-adequacy");
|
|
1289
|
-
if (!validationGate.inputHash || validationGate.inputHash !== currentValidationHash) {
|
|
1290
|
-
return "code-diff requires a current validation-adequacy approval; code or test evidence changed after the recorded approval.";
|
|
1304
|
+
if (validationGate.required && validationGate.status !== "skipped" && validationGate.status !== "overridden") {
|
|
1305
|
+
if (validationGate.status !== "completed" || validationGate.decision !== "approve") {
|
|
1306
|
+
return "code-diff requires the validation-adequacy Gate to complete successfully for the current Tester evidence.";
|
|
1307
|
+
}
|
|
1308
|
+
const currentValidationHash = await computeInputHash(deps, context.taskRepoRoot, "validation-adequacy");
|
|
1309
|
+
if (!validationGate.inputHash || validationGate.inputHash !== currentValidationHash) {
|
|
1310
|
+
return "code-diff requires a current validation-adequacy approval; code or test evidence changed after the recorded approval.";
|
|
1311
|
+
}
|
|
1291
1312
|
}
|
|
1292
|
-
return
|
|
1313
|
+
return readCodeDiffSourceArtifactError(deps.fs, context.taskRepoRoot, source);
|
|
1293
1314
|
}
|
|
1294
1315
|
async function readArchitectureEvidenceError(fs, taskRepoRoot) {
|
|
1295
1316
|
const relativePath = ".ai/vcm/handoffs/architecture-evidence.md";
|
|
1296
1317
|
const absolutePath = resolveRepoPath(taskRepoRoot, relativePath);
|
|
1297
|
-
|
|
1298
|
-
|
|
1318
|
+
const content = await fs.pathExists(absolutePath) ? await fs.readText(absolutePath) : null;
|
|
1319
|
+
const check = checkMarkdownArtifact("architecture-evidence", relativePath, content);
|
|
1320
|
+
if (check.status !== "ok") {
|
|
1321
|
+
return `${relativePath} is incomplete and cannot start architecture-plan review. ${formatArtifactCheckFailure(check)}`;
|
|
1299
1322
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1323
|
+
return undefined;
|
|
1324
|
+
}
|
|
1325
|
+
async function readArchitecturePlanError(fs, taskRepoRoot) {
|
|
1326
|
+
const relativePath = ".ai/vcm/handoffs/architecture-plan.md";
|
|
1327
|
+
const absolutePath = resolveRepoPath(taskRepoRoot, relativePath);
|
|
1328
|
+
const content = await fs.pathExists(absolutePath) ? await fs.readText(absolutePath) : null;
|
|
1329
|
+
const check = checkMarkdownArtifact("architecture-plan", relativePath, content);
|
|
1330
|
+
return check.status === "ok"
|
|
1331
|
+
? undefined
|
|
1332
|
+
: `${relativePath} is incomplete and cannot start architecture-plan review. ${formatArtifactCheckFailure(check)}`;
|
|
1333
|
+
}
|
|
1334
|
+
async function readCodeDiffSourceArtifactError(fs, taskRepoRoot, source) {
|
|
1335
|
+
if (!source) {
|
|
1336
|
+
return "code-diff requires a production-code source.";
|
|
1337
|
+
}
|
|
1338
|
+
const sourceArtifacts = {
|
|
1339
|
+
coder: ["coder-completion", ".ai/vcm/handoffs/coder-completion.md", "ready_for_review"],
|
|
1340
|
+
"architect-debug": ["architect-debug", ".ai/vcm/handoffs/architect-debug.md", "completed"],
|
|
1341
|
+
"architect-diagnosis": ["architecture-diagnosis", ".ai/vcm/handoffs/architecture-diagnosis.md", "diagnosis implementation completed"]
|
|
1342
|
+
};
|
|
1343
|
+
const [kind, relativePath, terminalValue] = sourceArtifacts[source];
|
|
1344
|
+
const absolutePath = resolveRepoPath(taskRepoRoot, relativePath);
|
|
1345
|
+
const content = await fs.pathExists(absolutePath) ? await fs.readText(absolutePath) : null;
|
|
1346
|
+
const check = checkMarkdownArtifact(kind, relativePath, content);
|
|
1347
|
+
if (check.status !== "ok") {
|
|
1348
|
+
return `code-diff requires a complete ${relativePath}. ${formatArtifactCheckFailure(check)}`;
|
|
1303
1349
|
}
|
|
1304
|
-
const
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1350
|
+
const value = kind === "architecture-diagnosis"
|
|
1351
|
+
? readArtifactSectionValue(content ?? "", "Final Disposition")?.toLowerCase()
|
|
1352
|
+
: matchField(content ?? "", kind === "coder-completion" ? "Decision" : "Status");
|
|
1353
|
+
if (value !== terminalValue) {
|
|
1354
|
+
return `code-diff requires ${relativePath} to report ${terminalValue}; found ${renderFoundValue(value)}.`;
|
|
1308
1355
|
}
|
|
1309
1356
|
return undefined;
|
|
1310
1357
|
}
|
|
@@ -1370,11 +1417,12 @@ Worktree: ${context.taskRepoRoot}
|
|
|
1370
1417
|
Gate: ${gate}
|
|
1371
1418
|
Request: ${requestId}
|
|
1372
1419
|
Report: ${absoluteReportPath}
|
|
1420
|
+
Submit: .ai/tools/vcm-artifact gate-review-report --file <candidate> --path ${reportPath} --mode final
|
|
1373
1421
|
|
|
1374
1422
|
Evidence:
|
|
1375
1423
|
${evidence}${capturedEvidence}${gitLine}${architectureContract}${validationContract}${codeDiffContract}${codeDiffSection}
|
|
1376
1424
|
|
|
1377
|
-
Write only Report. Start exactly:
|
|
1425
|
+
Write only the candidate Report and submit it with the command above. Start exactly:
|
|
1378
1426
|
Gate: ${gate}
|
|
1379
1427
|
Request: ${requestId}
|
|
1380
1428
|
Decision: approve|request_changes
|
|
@@ -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 { getRetrospectiveReportErrors } from "./artifact-service.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`;
|
|
@@ -131,8 +132,11 @@ export function createHarnessFeedbackService(deps) {
|
|
|
131
132
|
return true;
|
|
132
133
|
}
|
|
133
134
|
const reportPath = resolveRepoPath(repoRoot, marker.analysisPath);
|
|
134
|
-
const
|
|
135
|
-
|
|
135
|
+
const reportContent = await deps.fs.pathExists(reportPath)
|
|
136
|
+
? await deps.fs.readText(reportPath)
|
|
137
|
+
: "";
|
|
138
|
+
const reportErrors = reportContent.trim() ? getRetrospectiveReportErrors(reportContent) : ["Report is empty."];
|
|
139
|
+
const reportReady = reportErrors.length === 0;
|
|
136
140
|
if (!reportReady || (marker.memoryRunId && !input.memoryReviewSucceeded)) {
|
|
137
141
|
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
138
142
|
...marker,
|
|
@@ -140,7 +144,7 @@ export function createHarnessFeedbackService(deps) {
|
|
|
140
144
|
failedAt: timestamp,
|
|
141
145
|
updatedAt: timestamp,
|
|
142
146
|
error: !reportReady
|
|
143
|
-
?
|
|
147
|
+
? `Harness Engineer did not write a valid Task Harness Retrospective report: ${reportErrors.join(" ")}`
|
|
144
148
|
: "Task Harness Retrospective memory review failed."
|
|
145
149
|
});
|
|
146
150
|
return true;
|
|
@@ -244,6 +248,8 @@ export function createHarnessFeedbackService(deps) {
|
|
|
244
248
|
"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
249
|
]
|
|
246
250
|
: []),
|
|
251
|
+
"",
|
|
252
|
+
"The report must contain: # Task Harness Retrospective: <task>, ## Findings, ## Feedback Dispositions, ## Recommended Harness Changes, and ## VCM Issue Drafts.",
|
|
247
253
|
...(memoryReview
|
|
248
254
|
? [
|
|
249
255
|
"",
|
|
@@ -305,7 +311,8 @@ export function createHarnessFeedbackService(deps) {
|
|
|
305
311
|
: []),
|
|
306
312
|
"",
|
|
307
313
|
`Write the analysis to Result Path: ${resolveRepoPath(repoRoot, analysisPath)}`,
|
|
308
|
-
|
|
314
|
+
`Submit with: .ai/tools/vcm-artifact retrospective-report --file <candidate> --path ${analysisPath} --mode final`,
|
|
315
|
+
"End your turn after VCM accepts the result."
|
|
309
316
|
].join("\n");
|
|
310
317
|
}
|
|
311
318
|
function renderMemoryProposalDecisionTemplate(candidate) {
|