vibe-coding-master 0.7.39 → 0.7.41

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.
Files changed (51) hide show
  1. package/README.md +11 -6
  2. package/dist/backend/adapters/claude-adapter.js +8 -6
  3. package/dist/backend/api/artifact-routes.js +72 -1
  4. package/dist/backend/api/runtime-state-routes.js +17 -5
  5. package/dist/backend/api/task-routes.js +6 -0
  6. package/dist/backend/api/workflow-control-routes.js +56 -0
  7. package/dist/backend/cli/install-vcm-harness.js +42 -47
  8. package/dist/backend/role-tool-policy.js +60 -0
  9. package/dist/backend/server.js +21 -5
  10. package/dist/backend/services/artifact-service.js +309 -5
  11. package/dist/backend/services/auto-memory-service.js +2 -2
  12. package/dist/backend/services/gate-review-service.js +81 -33
  13. package/dist/backend/services/harness-feedback-service.js +11 -4
  14. package/dist/backend/services/harness-service.js +35 -45
  15. package/dist/backend/services/lsp-plugin.js +1 -4
  16. package/dist/backend/services/message-service.js +105 -37
  17. package/dist/backend/services/status-service.js +6 -0
  18. package/dist/backend/services/workflow-control-service.js +854 -0
  19. package/dist/backend/templates/handoff.js +158 -3
  20. package/dist/backend/templates/harness/architect-agent.js +3 -2
  21. package/dist/backend/templates/harness/check-scaffold-ledger.js +105 -39
  22. package/dist/backend/templates/harness/claude-root.js +9 -1
  23. package/dist/backend/templates/harness/coder-agent.js +1 -2
  24. package/dist/backend/templates/harness/coder-worker-agent.js +1 -1
  25. package/dist/backend/templates/harness/gate-review.js +3 -4
  26. package/dist/backend/templates/harness/harness-engineer-agent.js +18 -1
  27. package/dist/backend/templates/harness/project-manager-agent.js +3 -2
  28. package/dist/backend/templates/harness/tester-agent.js +1 -0
  29. package/dist/backend/templates/harness/vcm-architecture-interview-skill.js +5 -0
  30. package/dist/backend/templates/harness/vcm-code-navigation-skill.js +3 -4
  31. package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +2 -2
  32. package/dist/backend/templates/harness/vcm-long-running-validation-skill.js +4 -2
  33. package/dist/backend/templates/harness/vcm-propose-memory-skill.js +4 -2
  34. package/dist/backend/templates/harness/vcm-report-harness-issue-skill.js +2 -1
  35. package/dist/backend/templates/harness/vcm-route-message-skill.js +4 -7
  36. package/dist/backend/templates/harness/vcm-task-state-skill.js +2 -13
  37. package/dist/backend/templates/harness/vcm-workflow-review-skill.js +62 -0
  38. package/dist/backend/templates/message-envelope.js +4 -4
  39. package/dist/shared/types/workflow.js +7 -1
  40. package/dist/shared/validation/artifact-check.js +99 -95
  41. package/dist/shared/validation/artifact-contract.js +9 -0
  42. package/dist/shared/validation/artifact-registry.js +211 -0
  43. package/dist-frontend/assets/{index-DkM0mnQD.js → index-Bocc2DWF.js} +34 -34
  44. package/dist-frontend/assets/{index-CXSOe-NN.css → index-C_XHGNBD.css} +1 -1
  45. package/dist-frontend/index.html +2 -2
  46. package/package.json +1 -1
  47. package/scripts/claude-plugins/vcm-lsp-bridge/.claude-plugin/plugin.json +2 -1
  48. package/scripts/harness-tools/run-long-check +38 -8
  49. package/scripts/harness-tools/vcm-artifact +148 -0
  50. package/scripts/harness-tools/vcm-bash-guard +42 -47
  51. 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
- [path.posix.join(paths.handoffDir, "coder-completion.md"), renderCoderCompletionTemplate(input.taskSlug)],
68
- [path.posix.join(paths.handoffDir, "architect-debug.md"), renderArchitectDebugTemplate(input.taskSlug)],
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
- `Write the draft to: ${resolveRepoPath(taskRepoRoot, draft.path)}`
908
+ `Assigned proposal path: ${resolveRepoPath(taskRepoRoot, draft.path)}`
909
909
  ];
910
910
  return [
911
911
  ...prompt,
912
912
  "",
913
- "End the turn after writing the draft."
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
- if (!await fs.pathExists(absolutePath)) {
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 (!validationGate.required) {
1280
- return undefined;
1281
- }
1282
- if (validationGate.status === "skipped" || validationGate.status === "overridden") {
1283
- return undefined;
1284
- }
1285
- if (validationGate.status !== "completed" || validationGate.decision !== "approve") {
1286
- return "code-diff requires the validation-adequacy Gate to complete successfully for the current Tester evidence.";
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 undefined;
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
- if (!await fs.pathExists(absolutePath)) {
1298
- return `${relativePath} is missing. Complete architecture evidence before requesting architecture-plan review.`;
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
- const content = await fs.readText(absolutePath);
1301
- if (content.trim().length === 0) {
1302
- return `${relativePath} is empty. Complete architecture evidence before requesting architecture-plan review.`;
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 status = /^\s*Architecture Evidence Status\s*:\s*(.+?)\s*$/im.exec(content)?.[1]?.trim();
1305
- if (status?.toLowerCase() !== "complete") {
1306
- return `${relativePath} is incomplete and cannot start architecture-plan review. `
1307
- + `Architecture Evidence Status must be exactly "complete"; found ${renderFoundValue(status)}.`;
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 reportReady = await deps.fs.pathExists(reportPath)
135
- && Boolean((await deps.fs.readText(reportPath)).trim());
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
- ? "Harness Engineer did not write the required Task Harness Retrospective report."
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
- "End your turn after writing the result."
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) {