vibe-coding-master 0.0.13 → 0.0.15

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 (30) hide show
  1. package/README.md +36 -36
  2. package/dist/backend/api/claude-hook-routes.js +5 -0
  3. package/dist/backend/api/message-routes.js +8 -36
  4. package/dist/backend/server.js +12 -26
  5. package/dist/backend/services/artifact-service.js +27 -2
  6. package/dist/backend/services/claude-hook-service.js +67 -0
  7. package/dist/backend/services/harness-service.js +112 -0
  8. package/dist/backend/services/message-service.js +283 -175
  9. package/dist/backend/services/round-service.js +17 -127
  10. package/dist/backend/services/session-service.js +55 -2
  11. package/dist/backend/services/status-service.js +0 -76
  12. package/dist/backend/templates/handoff.js +3 -0
  13. package/dist/backend/templates/harness/architect-agent.js +5 -0
  14. package/dist/backend/templates/harness/claude-root.js +6 -2
  15. package/dist/backend/templates/harness/coder-agent.js +5 -0
  16. package/dist/backend/templates/harness/project-manager-agent.js +7 -3
  17. package/dist/backend/templates/harness/reviewer-agent.js +5 -0
  18. package/dist/backend/templates/message-envelope.js +7 -1
  19. package/dist/shared/types/claude-hook.js +1 -0
  20. package/dist-frontend/assets/{index-CyJrJge9.js → index-QhzvzTMZ.js} +43 -42
  21. package/dist-frontend/assets/index-jEkUTnIY.css +32 -0
  22. package/dist-frontend/index.html +2 -2
  23. package/docs/cc-best-practices.md +13 -4
  24. package/docs/product-design.md +74 -47
  25. package/docs/v1-architecture-design.md +53 -53
  26. package/docs/v1-implementation-plan.md +94 -61
  27. package/package.json +2 -3
  28. package/scripts/verify-package.mjs +0 -3
  29. package/dist/cli/vcmctl.js +0 -141
  30. package/dist-frontend/assets/index-N5DA0uE9.css +0 -32
@@ -1,13 +1,28 @@
1
1
  import path from "node:path";
2
2
  import { randomUUID } from "node:crypto";
3
- import { ROLE_NAMES } from "../../shared/constants.js";
3
+ import { ROLE_NAMES, isRoleName } from "../../shared/constants.js";
4
4
  import { VcmError } from "../errors.js";
5
5
  import { resolveRepoPath } from "../adapters/filesystem.js";
6
6
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
7
- import { renderManualStagePrompt, renderMessageEnvelope } from "../templates/message-envelope.js";
7
+ import { renderMessageEnvelope } from "../templates/message-envelope.js";
8
8
  const PM_ROLE = "project-manager";
9
9
  const PM_TO_ROLE_TYPES = new Set(["task", "question", "review-request", "revise", "cancel"]);
10
10
  const ROLE_TO_PM_TYPES = new Set(["result", "question", "blocked", "finding"]);
11
+ const OPEN_MESSAGE_STATUSES = new Set([
12
+ "pending_approval",
13
+ "queued",
14
+ "submitted",
15
+ "failed",
16
+ "delivery_failed",
17
+ "response_ready_missing_result"
18
+ ]);
19
+ const MUTABLE_ROUTE_MESSAGE_STATUSES = new Set([
20
+ "pending_approval",
21
+ "queued",
22
+ "failed",
23
+ "delivery_failed",
24
+ "response_ready_missing_result"
25
+ ]);
11
26
  export function createMessageService(deps) {
12
27
  const now = deps.now ?? (() => new Date().toISOString());
13
28
  const id = deps.id ?? (() => `msg_${randomUUID()}`);
@@ -18,155 +33,167 @@ export function createMessageService(deps) {
18
33
  return {
19
34
  taskSlug: input.taskSlug,
20
35
  mode: "manual",
21
- paused: false,
22
36
  updatedAt: now()
23
37
  };
24
38
  }
25
- return deps.fs.readJson(statePath);
39
+ const state = await deps.fs.readJson(statePath);
40
+ return {
41
+ taskSlug: state.taskSlug,
42
+ mode: state.mode,
43
+ updatedAt: state.updatedAt
44
+ };
26
45
  }
27
- async function sendMessageLocked(input) {
46
+ async function scanLocked(input) {
28
47
  await deps.taskService.loadTask(input.repoRoot, input.taskSlug);
29
- validateMessagePolicy(input.fromRole, input.toRole, input.type);
30
48
  const timestamp = now();
31
- const message = {
32
- id: id(),
33
- taskSlug: input.taskSlug,
34
- fromRole: input.fromRole,
35
- toRole: input.toRole,
36
- type: input.type,
37
- body: input.body,
38
- artifactRefs: input.artifactRefs ?? [],
39
- parentMessageId: input.parentMessageId,
40
- status: "queued",
41
- createdAt: timestamp
42
- };
43
- message.bodyPath = await writeMessageBody(deps.fs, input.taskRepoRoot ?? input.repoRoot, input.handoffDir, message);
44
- let messages = await readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
45
- if (isRoleActor(input.fromRole)) {
46
- const acknowledged = acknowledgeActiveMessages(messages, input.fromRole, timestamp);
47
- for (const acknowledgedMessage of acknowledged) {
48
- await appendMessageSnapshot(deps.fs, input, acknowledgedMessage);
49
+ const base = toRouteContext(input);
50
+ const state = await getOrchestrationState(input);
51
+ const messagesPath = getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug);
52
+ let messages = await readLatestMessages(deps.fs, messagesPath);
53
+ const updates = [];
54
+ if (input.stoppedRole) {
55
+ const acknowledged = acknowledgeActiveMessages(messages, input.stoppedRole, timestamp);
56
+ for (const message of acknowledged) {
57
+ await appendMessageSnapshot(deps.fs, input, message);
49
58
  }
59
+ updates.push(...acknowledged);
50
60
  messages = applyMessageSnapshots(messages, acknowledged);
51
61
  }
52
- const state = await getOrchestrationState(input);
53
- const result = await routeOutboundMessage(input, message, messages, state, timestamp);
54
- await appendMessageSnapshot(deps.fs, input, result.message);
55
- if (isRoleActor(input.fromRole)) {
56
- await deliverNextQueuedMessage(input, input.fromRole);
62
+ const pendingFiles = (await listRouteFiles(deps.fs, base)).filter((routeFile) => routeFile.pending);
63
+ const candidates = selectDispatchCandidates(pendingFiles, input.stoppedRole);
64
+ const results = [];
65
+ const deliveredTargets = new Set();
66
+ for (const routeFile of candidates) {
67
+ if (deliveredTargets.has(routeFile.toRole)) {
68
+ continue;
69
+ }
70
+ const result = await dispatchRouteFile(input, routeFile, messages, state, timestamp);
71
+ results.push(result);
72
+ await appendMessageSnapshot(deps.fs, input, result.message);
73
+ messages = applyMessageSnapshots(messages, [result.message]);
74
+ if (result.delivered) {
75
+ deliveredTargets.add(routeFile.toRole);
76
+ }
57
77
  }
58
- return result;
78
+ return results;
59
79
  }
60
- async function routeOutboundMessage(input, message, existingMessages, state, timestamp) {
61
- const session = await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, input.toRole);
80
+ async function listPendingRouteFiles(input) {
81
+ const context = toRouteContext(input);
82
+ const all = await listRouteFiles(deps.fs, context);
83
+ return all.filter((routeFile) => routeFile.pending);
84
+ }
85
+ async function dispatchRouteFile(input, routeFile, existingMessages, state, timestamp) {
86
+ validateMessagePolicy(routeFile.fromRole, routeFile.toRole, routeFile.type);
87
+ const existingOpen = findOpenMessageForRoute(existingMessages, routeFile.path);
88
+ const message = {
89
+ ...(existingOpen ?? {
90
+ id: id(),
91
+ taskSlug: input.taskSlug,
92
+ createdAt: timestamp
93
+ }),
94
+ fromRole: routeFile.fromRole,
95
+ toRole: routeFile.toRole,
96
+ type: routeFile.type,
97
+ body: routeFile.body,
98
+ artifactRefs: routeFile.artifactRefs,
99
+ bodyPath: routeFile.path,
100
+ routePath: routeFile.path,
101
+ status: "queued",
102
+ queuedBehindMessageId: undefined,
103
+ failureReason: undefined
104
+ };
105
+ const session = await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, routeFile.toRole);
62
106
  if (!session || session.status !== "running") {
63
- const queued = {
64
- ...message,
65
- status: "queued",
66
- failureReason: `${input.toRole} session is not running.`
107
+ return {
108
+ message: {
109
+ ...message,
110
+ status: "queued",
111
+ failureReason: `${routeFile.toRole} session is not running.`
112
+ },
113
+ delivered: false,
114
+ requiresUserApproval: false,
115
+ clearedRouteFile: false
67
116
  };
68
- return { message: queued, delivered: false, requiresUserApproval: false };
69
117
  }
70
118
  if (state.mode === "manual") {
71
- const pending = {
72
- ...message,
73
- status: "pending_approval"
74
- };
75
- return { message: pending, delivered: false, requiresUserApproval: true };
76
- }
77
- if (state.paused) {
78
- const queued = {
79
- ...message,
80
- status: "queued",
81
- failureReason: "Auto orchestration is paused."
119
+ return {
120
+ message: {
121
+ ...message,
122
+ status: "pending_approval"
123
+ },
124
+ delivered: false,
125
+ requiresUserApproval: true,
126
+ clearedRouteFile: false
82
127
  };
83
- return { message: queued, delivered: false, requiresUserApproval: false };
84
128
  }
85
- const activeMessage = findActiveMessageForRole(existingMessages, input.toRole);
129
+ const activeMessage = findActiveMessageForRole(existingMessages, routeFile.toRole);
86
130
  if (activeMessage) {
87
- const queued = {
88
- ...message,
89
- status: "queued",
90
- queuedBehindMessageId: activeMessage.id,
91
- failureReason: `${input.toRole} is still handling VCM message ${activeMessage.id}.`
131
+ return {
132
+ message: {
133
+ ...message,
134
+ status: "queued",
135
+ queuedBehindMessageId: activeMessage.id,
136
+ failureReason: `${routeFile.toRole} is still handling VCM message ${activeMessage.id}.`
137
+ },
138
+ delivered: false,
139
+ requiresUserApproval: false,
140
+ clearedRouteFile: false
92
141
  };
93
- return { message: queued, delivered: false, requiresUserApproval: false };
94
142
  }
95
- const delivered = await deliverMessageToSession(session.id, message, timestamp);
96
- return { message: delivered, delivered: true, requiresUserApproval: false };
97
- }
98
- async function deliverNextQueuedMessage(input, targetRole) {
99
- const state = await getOrchestrationState(input);
100
- if (state.mode !== "auto" || state.paused) {
101
- return undefined;
102
- }
103
- const messages = await readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
104
- if (findActiveMessageForRole(messages, targetRole)) {
105
- return undefined;
106
- }
107
- const nextMessage = messages.find((message) => message.toRole === targetRole && message.status === "queued");
108
- if (!nextMessage) {
109
- return undefined;
110
- }
111
- const session = await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, targetRole);
112
- if (!session || session.status !== "running") {
113
- return undefined;
114
- }
115
- const delivered = await deliverMessageToSession(session.id, nextMessage, now());
116
- await appendMessageSnapshot(deps.fs, input, delivered);
117
- return delivered;
118
- }
119
- async function deliverMessageToSession(sessionId, message, timestamp) {
120
- const delivered = {
143
+ const submitted = {
121
144
  ...message,
122
- status: "delivered",
145
+ status: "submitted",
123
146
  deliveredAt: timestamp,
124
- queuedBehindMessageId: undefined,
125
- failureReason: undefined
147
+ submittedAt: timestamp
148
+ };
149
+ await submitTerminalInput(deps.runtime, session.id, renderMessageEnvelope(submitted));
150
+ await deps.sessionService.markRoleActivityRunning(input.repoRoot, input.taskSlug, routeFile.toRole);
151
+ await deps.fs.writeText(resolveRepoPath(input.taskRepoRoot ?? input.repoRoot, routeFile.path), "");
152
+ return {
153
+ message: submitted,
154
+ delivered: true,
155
+ requiresUserApproval: false,
156
+ clearedRouteFile: true
126
157
  };
127
- await submitTerminalInput(deps.runtime, sessionId, renderMessageEnvelope(delivered));
128
- return delivered;
129
158
  }
130
159
  return {
131
160
  listMessages(input) {
132
161
  return readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
133
162
  },
134
- async sendMessage(input) {
135
- return withTaskLock(taskLocks, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), () => sendMessageLocked(input));
163
+ listPendingRouteFiles(input) {
164
+ return listPendingRouteFiles(input);
136
165
  },
137
- async stageMessage(input) {
138
- const message = await getMessageOrThrow(deps.fs, input);
139
- const session = await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, message.toRole);
140
- if (!session || session.status !== "running") {
141
- throw new VcmError({
142
- code: "MESSAGE_TARGET_NOT_RUNNING",
143
- message: `${message.toRole} session is not running.`,
144
- statusCode: 409,
145
- hint: `Start the ${message.toRole} session before staging this message.`
146
- });
147
- }
148
- const staged = {
149
- ...message,
150
- status: "staged",
151
- stagedAt: now(),
152
- failureReason: undefined
153
- };
154
- deps.runtime.write(session.id, renderManualStagePrompt(staged));
155
- await appendMessageSnapshot(deps.fs, input, staged);
156
- return staged;
166
+ async scanAndDispatchPendingRouteFiles(input) {
167
+ return withTaskLock(taskLocks, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), () => scanLocked(input));
157
168
  },
158
- approveMessage(input) {
159
- return this.stageMessage(input);
160
- },
161
- async rejectMessage(input) {
162
- const message = await getMessageOrThrow(deps.fs, input);
163
- const rejected = {
164
- ...message,
165
- status: "rejected",
166
- failureReason: undefined
167
- };
168
- await appendMessageSnapshot(deps.fs, input, rejected);
169
- return rejected;
169
+ async markAllDone(input) {
170
+ return withTaskLock(taskLocks, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), async () => {
171
+ const timestamp = now();
172
+ const messages = await readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
173
+ const updated = messages
174
+ .filter((message) => OPEN_MESSAGE_STATUSES.has(message.status))
175
+ .map((message) => ({
176
+ ...message,
177
+ status: "acknowledged",
178
+ acknowledgedAt: timestamp,
179
+ queuedBehindMessageId: undefined,
180
+ failureReason: undefined
181
+ }));
182
+ for (const message of updated) {
183
+ await appendMessageSnapshot(deps.fs, input, message);
184
+ }
185
+ if (input.clearRouteFiles) {
186
+ for (const routeFile of await listPendingRouteFiles(input)) {
187
+ await deps.fs.writeText(resolveRepoPath(input.taskRepoRoot ?? input.repoRoot, routeFile.path), "");
188
+ }
189
+ }
190
+ const latest = applyMessageSnapshots(messages, updated);
191
+ return {
192
+ taskSlug: input.taskSlug,
193
+ updatedCount: updated.length,
194
+ messages: latest
195
+ };
196
+ });
170
197
  },
171
198
  async getOrchestrationState(input) {
172
199
  return getOrchestrationState(input);
@@ -176,7 +203,6 @@ export function createMessageService(deps) {
176
203
  const next = {
177
204
  ...current,
178
205
  mode: input.mode ?? current.mode,
179
- paused: input.paused ?? current.paused,
180
206
  updatedAt: now()
181
207
  };
182
208
  await deps.fs.writeJsonAtomic(getOrchestrationStatePath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), next);
@@ -184,6 +210,132 @@ export function createMessageService(deps) {
184
210
  }
185
211
  };
186
212
  }
213
+ function toRouteContext(input) {
214
+ return {
215
+ repoRoot: input.repoRoot,
216
+ taskRepoRoot: input.taskRepoRoot,
217
+ handoffDir: input.handoffDir
218
+ };
219
+ }
220
+ async function listRouteFiles(fs, input) {
221
+ const repoRoot = input.taskRepoRoot ?? input.repoRoot;
222
+ const routeDir = path.posix.join(input.handoffDir, "messages");
223
+ const absoluteMessagesDir = resolveRepoPath(repoRoot, routeDir);
224
+ if (!(await fs.pathExists(absoluteMessagesDir))) {
225
+ return [];
226
+ }
227
+ const entries = await fs.readDir(absoluteMessagesDir);
228
+ const routeFiles = [];
229
+ for (const entry of entries.filter((candidate) => candidate.endsWith(".md")).sort()) {
230
+ const route = parseRouteFileName(entry);
231
+ if (!route) {
232
+ continue;
233
+ }
234
+ const relativePath = path.posix.join(routeDir, entry);
235
+ const content = await fs.readText(resolveRepoPath(repoRoot, relativePath));
236
+ const parsed = parseRouteFileContent(content, route.fromRole, route.toRole);
237
+ routeFiles.push({
238
+ path: relativePath,
239
+ fromRole: route.fromRole,
240
+ toRole: route.toRole,
241
+ type: parsed.type,
242
+ body: parsed.body,
243
+ artifactRefs: parsed.artifactRefs,
244
+ exists: true,
245
+ pending: parsed.body.trim().length > 0
246
+ });
247
+ }
248
+ return routeFiles;
249
+ }
250
+ function parseRouteFileName(fileName) {
251
+ for (const fromRole of ROLE_NAMES) {
252
+ for (const toRole of ROLE_NAMES) {
253
+ if (fromRole === toRole) {
254
+ continue;
255
+ }
256
+ if (fileName === `${fromRole}-${toRole}.md`) {
257
+ return { fromRole, toRole };
258
+ }
259
+ }
260
+ }
261
+ return undefined;
262
+ }
263
+ function parseRouteFileContent(content, fromRole, toRole) {
264
+ const { frontmatter, body } = splitFrontmatter(content);
265
+ const type = parseMessageType(frontmatter.type) ?? getDefaultMessageType(fromRole, toRole);
266
+ const artifactRefs = parseArtifactRefs(frontmatter);
267
+ return {
268
+ type,
269
+ body: body.trim(),
270
+ artifactRefs
271
+ };
272
+ }
273
+ function splitFrontmatter(content) {
274
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
275
+ if (!match) {
276
+ return { frontmatter: {}, body: content };
277
+ }
278
+ const frontmatter = {};
279
+ for (const line of match[1].split(/\r?\n/)) {
280
+ const delimiter = line.indexOf(":");
281
+ if (delimiter <= 0) {
282
+ continue;
283
+ }
284
+ const key = line.slice(0, delimiter).trim();
285
+ const value = line.slice(delimiter + 1).trim();
286
+ if (key) {
287
+ frontmatter[key] = value;
288
+ }
289
+ }
290
+ return {
291
+ frontmatter,
292
+ body: content.slice(match[0].length)
293
+ };
294
+ }
295
+ function parseMessageType(value) {
296
+ const validTypes = [
297
+ "user-request",
298
+ "task",
299
+ "question",
300
+ "blocked",
301
+ "result",
302
+ "finding",
303
+ "review-request",
304
+ "revise",
305
+ "cancel"
306
+ ];
307
+ return value && validTypes.includes(value)
308
+ ? value
309
+ : undefined;
310
+ }
311
+ function parseArtifactRefs(frontmatter) {
312
+ const refs = frontmatter.artifact_refs ?? frontmatter.artifactRefs ?? frontmatter.related_artifact;
313
+ if (!refs) {
314
+ return [];
315
+ }
316
+ return refs
317
+ .split(",")
318
+ .map((entry) => entry.trim())
319
+ .filter(Boolean);
320
+ }
321
+ function getDefaultMessageType(fromRole, toRole) {
322
+ if (fromRole === PM_ROLE && toRole !== PM_ROLE) {
323
+ return "task";
324
+ }
325
+ if (fromRole !== PM_ROLE && toRole === PM_ROLE) {
326
+ return "result";
327
+ }
328
+ return "question";
329
+ }
330
+ function selectDispatchCandidates(routeFiles, stoppedRole) {
331
+ const candidates = stoppedRole
332
+ ? routeFiles.filter((routeFile) => routeFile.fromRole === stoppedRole || routeFile.toRole === stoppedRole)
333
+ : routeFiles;
334
+ return [...candidates].sort((left, right) => {
335
+ const updated = (left.updatedAt ?? "").localeCompare(right.updatedAt ?? "");
336
+ return updated !== 0 ? updated : left.path.localeCompare(right.path);
337
+ });
338
+ }
187
339
  function validateMessagePolicy(fromRole, toRole, type) {
188
340
  if (!ROLE_NAMES.includes(toRole)) {
189
341
  throw new VcmError({
@@ -192,17 +344,7 @@ function validateMessagePolicy(fromRole, toRole, type) {
192
344
  statusCode: 400
193
345
  });
194
346
  }
195
- if (fromRole === "user") {
196
- if (toRole === PM_ROLE && type === "user-request") {
197
- return;
198
- }
199
- throw new VcmError({
200
- code: "MESSAGE_POLICY_DENIED",
201
- message: "User messages can only target project-manager as user-request.",
202
- statusCode: 403
203
- });
204
- }
205
- if (!ROLE_NAMES.includes(fromRole)) {
347
+ if (!isRoleName(String(fromRole))) {
206
348
  throw new VcmError({
207
349
  code: "MESSAGE_SENDER_UNKNOWN",
208
350
  message: `Unknown sender role: ${fromRole}`,
@@ -219,7 +361,7 @@ function validateMessagePolicy(fromRole, toRole, type) {
219
361
  code: "MESSAGE_POLICY_DENIED",
220
362
  message: `${fromRole} cannot send ${type} messages to ${toRole}.`,
221
363
  statusCode: 403,
222
- hint: "Use project-manager as the orchestration hub."
364
+ hint: "Use project-manager as the orchestration hub unless this task explicitly allows a peer route."
223
365
  });
224
366
  }
225
367
  async function withTaskLock(locks, key, run) {
@@ -235,15 +377,18 @@ async function withTaskLock(locks, key, run) {
235
377
  }
236
378
  }
237
379
  }
238
- function isRoleActor(actor) {
239
- return ROLE_NAMES.includes(actor);
380
+ function findOpenMessageForRoute(messages, routePath) {
381
+ return [...messages]
382
+ .reverse()
383
+ .find((message) => (message.routePath ?? message.bodyPath) === routePath && MUTABLE_ROUTE_MESSAGE_STATUSES.has(message.status));
240
384
  }
241
385
  function findActiveMessageForRole(messages, role) {
242
- return messages.find((message) => message.toRole === role && message.status === "delivered");
386
+ return messages.find((message) => message.toRole === role &&
387
+ message.status === "submitted");
243
388
  }
244
389
  function acknowledgeActiveMessages(messages, role, timestamp) {
245
390
  return messages
246
- .filter((message) => message.toRole === role && message.status === "delivered")
391
+ .filter((message) => message.toRole === role && message.status === "submitted")
247
392
  .map((message) => ({
248
393
  ...message,
249
394
  status: "acknowledged",
@@ -261,43 +406,6 @@ function applyMessageSnapshots(messages, snapshots) {
261
406
  }
262
407
  return [...latest.values()].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
263
408
  }
264
- async function writeMessageBody(fs, repoRoot, handoffDir, message) {
265
- const bodyPath = path.posix.join(handoffDir, "messages", `${message.id}.md`);
266
- await fs.writeText(resolveRepoPath(repoRoot, bodyPath), renderMessageBodyFile(message));
267
- return bodyPath;
268
- }
269
- function renderMessageBodyFile(message) {
270
- const artifactRefs = message.artifactRefs.length > 0
271
- ? message.artifactRefs.map((artifact) => `- ${artifact}`).join("\n")
272
- : "- none";
273
- return `# VCM Message ${message.id}
274
-
275
- - Task: ${message.taskSlug}
276
- - From: ${message.fromRole}
277
- - To: ${message.toRole}
278
- - Type: ${message.type}
279
-
280
- ## Body
281
-
282
- ${message.body}
283
-
284
- ## Artifact Refs
285
-
286
- ${artifactRefs}
287
- `;
288
- }
289
- async function getMessageOrThrow(fs, input) {
290
- const messages = await readLatestMessages(fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
291
- const message = messages.find((candidate) => candidate.id === input.messageId);
292
- if (!message) {
293
- throw new VcmError({
294
- code: "MESSAGE_MISSING",
295
- message: `Message does not exist: ${input.messageId}`,
296
- statusCode: 404
297
- });
298
- }
299
- return message;
300
- }
301
409
  async function readLatestMessages(fs, messagesPath) {
302
410
  if (!(await fs.pathExists(messagesPath))) {
303
411
  return [];