vibe-coding-master 0.7.47 → 0.7.48

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