vibe-coding-master 0.0.14 → 0.0.16

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.
@@ -1,20 +1,25 @@
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 MARK_ALL_DONE_STATUSES = new Set([
11
+ const OPEN_MESSAGE_STATUSES = new Set([
12
12
  "pending_approval",
13
13
  "queued",
14
- "staged",
15
14
  "delivering",
16
15
  "submitted",
17
- "delivered",
16
+ "failed",
17
+ "delivery_failed",
18
+ "response_ready_missing_result"
19
+ ]);
20
+ const MUTABLE_ROUTE_MESSAGE_STATUSES = new Set([
21
+ "pending_approval",
22
+ "queued",
18
23
  "failed",
19
24
  "delivery_failed",
20
25
  "response_ready_missing_result"
@@ -29,153 +34,168 @@ export function createMessageService(deps) {
29
34
  return {
30
35
  taskSlug: input.taskSlug,
31
36
  mode: "manual",
32
- paused: false,
33
37
  updatedAt: now()
34
38
  };
35
39
  }
36
- return deps.fs.readJson(statePath);
40
+ const state = await deps.fs.readJson(statePath);
41
+ return {
42
+ taskSlug: state.taskSlug,
43
+ mode: state.mode,
44
+ updatedAt: state.updatedAt
45
+ };
37
46
  }
38
- async function sendMessageLocked(input) {
47
+ async function scanLocked(input) {
39
48
  await deps.taskService.loadTask(input.repoRoot, input.taskSlug);
40
- validateMessagePolicy(input.fromRole, input.toRole, input.type);
41
49
  const timestamp = now();
42
- const message = {
43
- id: id(),
44
- taskSlug: input.taskSlug,
45
- fromRole: input.fromRole,
46
- toRole: input.toRole,
47
- type: input.type,
48
- body: input.body,
49
- artifactRefs: input.artifactRefs ?? [],
50
- parentMessageId: input.parentMessageId,
51
- status: "queued",
52
- createdAt: timestamp
53
- };
54
- message.bodyPath = await writeMessageBody(deps.fs, input.taskRepoRoot ?? input.repoRoot, input.handoffDir, message);
55
- let messages = await readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
56
- if (isRoleActor(input.fromRole)) {
57
- const acknowledged = acknowledgeActiveMessages(messages, input.fromRole, timestamp);
58
- for (const acknowledgedMessage of acknowledged) {
59
- await appendMessageSnapshot(deps.fs, input, acknowledgedMessage);
50
+ const base = toRouteContext(input);
51
+ const state = await getOrchestrationState(input);
52
+ const messagesPath = getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug);
53
+ let messages = await readLatestMessages(deps.fs, messagesPath);
54
+ const updates = [];
55
+ if (input.stoppedRole) {
56
+ const acknowledged = acknowledgeActiveMessages(messages, input.stoppedRole, timestamp);
57
+ for (const message of acknowledged) {
58
+ await appendMessageSnapshot(deps.fs, input, message);
59
+ await clearRouteFileIfStillMatchesMessage(deps.fs, input, message);
60
60
  }
61
+ updates.push(...acknowledged);
61
62
  messages = applyMessageSnapshots(messages, acknowledged);
62
63
  }
63
- const state = await getOrchestrationState(input);
64
- const result = await routeOutboundMessage(input, message, messages, state, timestamp);
65
- await appendMessageSnapshot(deps.fs, input, result.message);
66
- if (isRoleActor(input.fromRole)) {
67
- await deliverNextQueuedMessage(input, input.fromRole);
64
+ const pendingFiles = (await listRouteFiles(deps.fs, base)).filter((routeFile) => routeFile.pending);
65
+ const candidates = selectDispatchCandidates(pendingFiles, input.stoppedRole);
66
+ const results = [];
67
+ const deliveredTargets = new Set();
68
+ for (const routeFile of candidates) {
69
+ if (deliveredTargets.has(routeFile.toRole)) {
70
+ continue;
71
+ }
72
+ const result = await dispatchRouteFile(input, routeFile, messages, state, timestamp);
73
+ results.push(result);
74
+ await appendMessageSnapshot(deps.fs, input, result.message);
75
+ messages = applyMessageSnapshots(messages, [result.message]);
76
+ if (result.delivered) {
77
+ deliveredTargets.add(routeFile.toRole);
78
+ }
68
79
  }
69
- return result;
80
+ return results;
81
+ }
82
+ async function listPendingRouteFiles(input) {
83
+ const context = toRouteContext(input);
84
+ const all = await listRouteFiles(deps.fs, context);
85
+ return all.filter((routeFile) => routeFile.pending);
70
86
  }
71
- async function routeOutboundMessage(input, message, existingMessages, state, timestamp) {
72
- const session = await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, input.toRole);
87
+ async function dispatchRouteFile(input, routeFile, existingMessages, state, timestamp) {
88
+ validateMessagePolicy(routeFile.fromRole, routeFile.toRole, routeFile.type);
89
+ const existingOpen = findOpenMessageForRoute(existingMessages, routeFile.path);
90
+ const message = {
91
+ ...(existingOpen ?? {
92
+ id: id(),
93
+ taskSlug: input.taskSlug,
94
+ createdAt: timestamp
95
+ }),
96
+ fromRole: routeFile.fromRole,
97
+ toRole: routeFile.toRole,
98
+ type: routeFile.type,
99
+ body: routeFile.body,
100
+ artifactRefs: routeFile.artifactRefs,
101
+ bodyPath: routeFile.path,
102
+ routePath: routeFile.path,
103
+ status: "queued",
104
+ queuedBehindMessageId: undefined,
105
+ failureReason: undefined
106
+ };
107
+ const session = await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, routeFile.toRole);
73
108
  if (!session || session.status !== "running") {
74
- const queued = {
75
- ...message,
76
- status: "queued",
77
- failureReason: `${input.toRole} session is not running.`
109
+ return {
110
+ message: {
111
+ ...message,
112
+ status: "queued",
113
+ failureReason: `${routeFile.toRole} session is not running.`
114
+ },
115
+ delivered: false,
116
+ requiresUserApproval: false,
117
+ clearedRouteFile: false
78
118
  };
79
- return { message: queued, delivered: false, requiresUserApproval: false };
80
119
  }
81
120
  if (state.mode === "manual") {
82
- const pending = {
83
- ...message,
84
- status: "pending_approval"
85
- };
86
- return { message: pending, delivered: false, requiresUserApproval: true };
87
- }
88
- if (state.paused) {
89
- const queued = {
90
- ...message,
91
- status: "queued",
92
- failureReason: "Auto orchestration is paused."
121
+ return {
122
+ message: {
123
+ ...message,
124
+ status: "pending_approval"
125
+ },
126
+ delivered: false,
127
+ requiresUserApproval: true,
128
+ clearedRouteFile: false
93
129
  };
94
- return { message: queued, delivered: false, requiresUserApproval: false };
95
130
  }
96
- const activeMessage = findActiveMessageForRole(existingMessages, input.toRole);
131
+ const activeMessage = findActiveMessageForRole(existingMessages, routeFile.toRole);
97
132
  if (activeMessage) {
98
- const queued = {
99
- ...message,
100
- status: "queued",
101
- queuedBehindMessageId: activeMessage.id,
102
- failureReason: `${input.toRole} is still handling VCM message ${activeMessage.id}.`
133
+ return {
134
+ message: {
135
+ ...message,
136
+ status: "queued",
137
+ queuedBehindMessageId: activeMessage.id,
138
+ failureReason: `${routeFile.toRole} is still handling VCM message ${activeMessage.id}.`
139
+ },
140
+ delivered: false,
141
+ requiresUserApproval: false,
142
+ clearedRouteFile: false
103
143
  };
104
- return { message: queued, delivered: false, requiresUserApproval: false };
105
144
  }
106
- const delivered = await deliverMessageToSession(session.id, message, timestamp);
107
- return { message: delivered, delivered: true, requiresUserApproval: false };
108
- }
109
- async function deliverNextQueuedMessage(input, targetRole) {
110
- const state = await getOrchestrationState(input);
111
- if (state.mode !== "auto" || state.paused) {
112
- return undefined;
113
- }
114
- const messages = await readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
115
- if (findActiveMessageForRole(messages, targetRole)) {
116
- return undefined;
117
- }
118
- const nextMessage = messages.find((message) => message.toRole === targetRole && message.status === "queued");
119
- if (!nextMessage) {
120
- return undefined;
121
- }
122
- const session = await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, targetRole);
123
- if (!session || session.status !== "running") {
124
- return undefined;
125
- }
126
- const delivered = await deliverMessageToSession(session.id, nextMessage, now());
127
- await appendMessageSnapshot(deps.fs, input, delivered);
128
- return delivered;
129
- }
130
- async function deliverMessageToSession(sessionId, message, timestamp) {
131
145
  const delivering = {
132
146
  ...message,
133
147
  status: "delivering",
134
- deliveredAt: timestamp,
135
- queuedBehindMessageId: undefined,
136
- failureReason: undefined
148
+ deliveredAt: timestamp
149
+ };
150
+ await submitTerminalInput(deps.runtime, session.id, renderMessageEnvelope(delivering));
151
+ await deps.sessionService.markRoleActivityRunning(input.repoRoot, input.taskSlug, routeFile.toRole);
152
+ return {
153
+ message: delivering,
154
+ delivered: true,
155
+ requiresUserApproval: false,
156
+ clearedRouteFile: false
137
157
  };
138
- await submitTerminalInput(deps.runtime, sessionId, renderMessageEnvelope(delivering));
139
- return delivering;
140
158
  }
141
159
  return {
142
160
  listMessages(input) {
143
161
  return readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
144
162
  },
145
- async sendMessage(input) {
146
- return withTaskLock(taskLocks, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), () => sendMessageLocked(input));
163
+ listPendingRouteFiles(input) {
164
+ return listPendingRouteFiles(input);
165
+ },
166
+ async scanAndDispatchPendingRouteFiles(input) {
167
+ return withTaskLock(taskLocks, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), () => scanLocked(input));
147
168
  },
148
169
  async confirmPromptSubmitted(input) {
149
- const messageId = extractSubmittedMessageId(input.prompt);
150
- if (!messageId) {
151
- return undefined;
152
- }
153
- const messages = await readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
154
- const message = messages.find((candidate) => candidate.id === messageId && candidate.toRole === input.role);
155
- if (!message) {
156
- return undefined;
157
- }
158
- if (message.status === "submitted" || message.status === "acknowledged") {
159
- return message;
160
- }
161
- if (!isSubmittableMessageStatus(message.status)) {
162
- return undefined;
163
- }
164
- const submitted = {
165
- ...message,
166
- status: "submitted",
167
- submittedAt: now(),
168
- failureReason: undefined
169
- };
170
- await appendMessageSnapshot(deps.fs, input, submitted);
171
- return submitted;
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 messageId = extractVcmMessageId(input.prompt);
174
+ if (!messageId) {
175
+ return undefined;
176
+ }
177
+ const message = findDeliveringMessageForPrompt(messages, input.role, messageId);
178
+ if (!message) {
179
+ return undefined;
180
+ }
181
+ const submitted = {
182
+ ...message,
183
+ status: "submitted",
184
+ submittedAt: timestamp,
185
+ queuedBehindMessageId: undefined,
186
+ failureReason: undefined
187
+ };
188
+ await appendMessageSnapshot(deps.fs, input, submitted);
189
+ await clearRouteFileIfStillMatchesMessage(deps.fs, input, submitted);
190
+ return submitted;
191
+ });
172
192
  },
173
193
  async markAllDone(input) {
174
194
  return withTaskLock(taskLocks, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), async () => {
175
195
  const timestamp = now();
176
196
  const messages = await readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
177
197
  const updated = messages
178
- .filter((message) => MARK_ALL_DONE_STATUSES.has(message.status))
198
+ .filter((message) => OPEN_MESSAGE_STATUSES.has(message.status))
179
199
  .map((message) => ({
180
200
  ...message,
181
201
  status: "acknowledged",
@@ -186,6 +206,11 @@ export function createMessageService(deps) {
186
206
  for (const message of updated) {
187
207
  await appendMessageSnapshot(deps.fs, input, message);
188
208
  }
209
+ if (input.clearRouteFiles) {
210
+ for (const routeFile of await listPendingRouteFiles(input)) {
211
+ await deps.fs.writeText(resolveRepoPath(input.taskRepoRoot ?? input.repoRoot, routeFile.path), "");
212
+ }
213
+ }
189
214
  const latest = applyMessageSnapshots(messages, updated);
190
215
  return {
191
216
  taskSlug: input.taskSlug,
@@ -194,40 +219,6 @@ export function createMessageService(deps) {
194
219
  };
195
220
  });
196
221
  },
197
- async stageMessage(input) {
198
- const message = await getMessageOrThrow(deps.fs, input);
199
- const session = await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, message.toRole);
200
- if (!session || session.status !== "running") {
201
- throw new VcmError({
202
- code: "MESSAGE_TARGET_NOT_RUNNING",
203
- message: `${message.toRole} session is not running.`,
204
- statusCode: 409,
205
- hint: `Start the ${message.toRole} session before staging this message.`
206
- });
207
- }
208
- const staged = {
209
- ...message,
210
- status: "staged",
211
- stagedAt: now(),
212
- failureReason: undefined
213
- };
214
- deps.runtime.write(session.id, renderManualStagePrompt(staged));
215
- await appendMessageSnapshot(deps.fs, input, staged);
216
- return staged;
217
- },
218
- approveMessage(input) {
219
- return this.stageMessage(input);
220
- },
221
- async rejectMessage(input) {
222
- const message = await getMessageOrThrow(deps.fs, input);
223
- const rejected = {
224
- ...message,
225
- status: "rejected",
226
- failureReason: undefined
227
- };
228
- await appendMessageSnapshot(deps.fs, input, rejected);
229
- return rejected;
230
- },
231
222
  async getOrchestrationState(input) {
232
223
  return getOrchestrationState(input);
233
224
  },
@@ -236,7 +227,6 @@ export function createMessageService(deps) {
236
227
  const next = {
237
228
  ...current,
238
229
  mode: input.mode ?? current.mode,
239
- paused: input.paused ?? current.paused,
240
230
  updatedAt: now()
241
231
  };
242
232
  await deps.fs.writeJsonAtomic(getOrchestrationStatePath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), next);
@@ -244,6 +234,132 @@ export function createMessageService(deps) {
244
234
  }
245
235
  };
246
236
  }
237
+ function toRouteContext(input) {
238
+ return {
239
+ repoRoot: input.repoRoot,
240
+ taskRepoRoot: input.taskRepoRoot,
241
+ handoffDir: input.handoffDir
242
+ };
243
+ }
244
+ async function listRouteFiles(fs, input) {
245
+ const repoRoot = input.taskRepoRoot ?? input.repoRoot;
246
+ const routeDir = path.posix.join(input.handoffDir, "messages");
247
+ const absoluteMessagesDir = resolveRepoPath(repoRoot, routeDir);
248
+ if (!(await fs.pathExists(absoluteMessagesDir))) {
249
+ return [];
250
+ }
251
+ const entries = await fs.readDir(absoluteMessagesDir);
252
+ const routeFiles = [];
253
+ for (const entry of entries.filter((candidate) => candidate.endsWith(".md")).sort()) {
254
+ const route = parseRouteFileName(entry);
255
+ if (!route) {
256
+ continue;
257
+ }
258
+ const relativePath = path.posix.join(routeDir, entry);
259
+ const content = await fs.readText(resolveRepoPath(repoRoot, relativePath));
260
+ const parsed = parseRouteFileContent(content, route.fromRole, route.toRole);
261
+ routeFiles.push({
262
+ path: relativePath,
263
+ fromRole: route.fromRole,
264
+ toRole: route.toRole,
265
+ type: parsed.type,
266
+ body: parsed.body,
267
+ artifactRefs: parsed.artifactRefs,
268
+ exists: true,
269
+ pending: parsed.body.trim().length > 0
270
+ });
271
+ }
272
+ return routeFiles;
273
+ }
274
+ function parseRouteFileName(fileName) {
275
+ for (const fromRole of ROLE_NAMES) {
276
+ for (const toRole of ROLE_NAMES) {
277
+ if (fromRole === toRole) {
278
+ continue;
279
+ }
280
+ if (fileName === `${fromRole}-${toRole}.md`) {
281
+ return { fromRole, toRole };
282
+ }
283
+ }
284
+ }
285
+ return undefined;
286
+ }
287
+ function parseRouteFileContent(content, fromRole, toRole) {
288
+ const { frontmatter, body } = splitFrontmatter(content);
289
+ const type = parseMessageType(frontmatter.type) ?? getDefaultMessageType(fromRole, toRole);
290
+ const artifactRefs = parseArtifactRefs(frontmatter);
291
+ return {
292
+ type,
293
+ body: body.trim(),
294
+ artifactRefs
295
+ };
296
+ }
297
+ function splitFrontmatter(content) {
298
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
299
+ if (!match) {
300
+ return { frontmatter: {}, body: content };
301
+ }
302
+ const frontmatter = {};
303
+ for (const line of match[1].split(/\r?\n/)) {
304
+ const delimiter = line.indexOf(":");
305
+ if (delimiter <= 0) {
306
+ continue;
307
+ }
308
+ const key = line.slice(0, delimiter).trim();
309
+ const value = line.slice(delimiter + 1).trim();
310
+ if (key) {
311
+ frontmatter[key] = value;
312
+ }
313
+ }
314
+ return {
315
+ frontmatter,
316
+ body: content.slice(match[0].length)
317
+ };
318
+ }
319
+ function parseMessageType(value) {
320
+ const validTypes = [
321
+ "user-request",
322
+ "task",
323
+ "question",
324
+ "blocked",
325
+ "result",
326
+ "finding",
327
+ "review-request",
328
+ "revise",
329
+ "cancel"
330
+ ];
331
+ return value && validTypes.includes(value)
332
+ ? value
333
+ : undefined;
334
+ }
335
+ function parseArtifactRefs(frontmatter) {
336
+ const refs = frontmatter.artifact_refs ?? frontmatter.artifactRefs ?? frontmatter.related_artifact;
337
+ if (!refs) {
338
+ return [];
339
+ }
340
+ return refs
341
+ .split(",")
342
+ .map((entry) => entry.trim())
343
+ .filter(Boolean);
344
+ }
345
+ function getDefaultMessageType(fromRole, toRole) {
346
+ if (fromRole === PM_ROLE && toRole !== PM_ROLE) {
347
+ return "task";
348
+ }
349
+ if (fromRole !== PM_ROLE && toRole === PM_ROLE) {
350
+ return "result";
351
+ }
352
+ return "question";
353
+ }
354
+ function selectDispatchCandidates(routeFiles, stoppedRole) {
355
+ const candidates = stoppedRole
356
+ ? routeFiles.filter((routeFile) => routeFile.fromRole === stoppedRole || routeFile.toRole === stoppedRole)
357
+ : routeFiles;
358
+ return [...candidates].sort((left, right) => {
359
+ const updated = (left.updatedAt ?? "").localeCompare(right.updatedAt ?? "");
360
+ return updated !== 0 ? updated : left.path.localeCompare(right.path);
361
+ });
362
+ }
247
363
  function validateMessagePolicy(fromRole, toRole, type) {
248
364
  if (!ROLE_NAMES.includes(toRole)) {
249
365
  throw new VcmError({
@@ -252,17 +368,7 @@ function validateMessagePolicy(fromRole, toRole, type) {
252
368
  statusCode: 400
253
369
  });
254
370
  }
255
- if (fromRole === "user") {
256
- if (toRole === PM_ROLE && type === "user-request") {
257
- return;
258
- }
259
- throw new VcmError({
260
- code: "MESSAGE_POLICY_DENIED",
261
- message: "User messages can only target project-manager as user-request.",
262
- statusCode: 403
263
- });
264
- }
265
- if (!ROLE_NAMES.includes(fromRole)) {
371
+ if (!isRoleName(String(fromRole))) {
266
372
  throw new VcmError({
267
373
  code: "MESSAGE_SENDER_UNKNOWN",
268
374
  message: `Unknown sender role: ${fromRole}`,
@@ -279,7 +385,7 @@ function validateMessagePolicy(fromRole, toRole, type) {
279
385
  code: "MESSAGE_POLICY_DENIED",
280
386
  message: `${fromRole} cannot send ${type} messages to ${toRole}.`,
281
387
  statusCode: 403,
282
- hint: "Use project-manager as the orchestration hub."
388
+ hint: "Use project-manager as the orchestration hub unless this task explicitly allows a peer route."
283
389
  });
284
390
  }
285
391
  async function withTaskLock(locks, key, run) {
@@ -295,31 +401,59 @@ async function withTaskLock(locks, key, run) {
295
401
  }
296
402
  }
297
403
  }
298
- function isRoleActor(actor) {
299
- return ROLE_NAMES.includes(actor);
404
+ function findOpenMessageForRoute(messages, routePath) {
405
+ return [...messages]
406
+ .reverse()
407
+ .find((message) => (message.routePath ?? message.bodyPath) === routePath && MUTABLE_ROUTE_MESSAGE_STATUSES.has(message.status));
300
408
  }
301
409
  function findActiveMessageForRole(messages, role) {
302
- return messages.find((message) => message.toRole === role && isActiveMessageStatus(message.status));
410
+ return messages.find((message) => message.toRole === role &&
411
+ (message.status === "delivering" || message.status === "submitted"));
303
412
  }
304
413
  function acknowledgeActiveMessages(messages, role, timestamp) {
305
414
  return messages
306
- .filter((message) => message.toRole === role && isActiveMessageStatus(message.status))
415
+ .filter((message) => message.toRole === role && (message.status === "delivering" || message.status === "submitted"))
307
416
  .map((message) => ({
308
417
  ...message,
309
418
  status: "acknowledged",
419
+ submittedAt: message.submittedAt ?? message.deliveredAt ?? timestamp,
310
420
  acknowledgedAt: timestamp,
311
421
  failureReason: undefined
312
422
  }));
313
423
  }
314
- function isActiveMessageStatus(status) {
315
- return status === "delivering" || status === "submitted" || status === "delivered" || status === "staged";
424
+ function findDeliveringMessageForPrompt(messages, role, messageId) {
425
+ const delivering = messages.filter((message) => message.toRole === role &&
426
+ message.status === "delivering" &&
427
+ message.id === messageId);
428
+ return delivering.sort((left, right) => getMessageDeliveryTime(left).localeCompare(getMessageDeliveryTime(right))).at(-1);
316
429
  }
317
- function isSubmittableMessageStatus(status) {
318
- return status === "delivering" || status === "delivered" || status === "staged" || status === "pending_approval";
430
+ function extractVcmMessageId(prompt) {
431
+ return prompt?.match(/^\s*id:\s*(\S+)\s*$/m)?.[1];
319
432
  }
320
- function extractSubmittedMessageId(prompt) {
321
- const match = prompt.match(/\bmsg_[A-Za-z0-9_-]+\b/);
322
- return match?.[0];
433
+ function getMessageDeliveryTime(message) {
434
+ return message.deliveredAt ?? message.createdAt;
435
+ }
436
+ async function clearRouteFileIfStillMatchesMessage(fs, input, message) {
437
+ if (!message.routePath || !isRoleName(String(message.fromRole))) {
438
+ return;
439
+ }
440
+ const absolutePath = resolveRepoPath(input.taskRepoRoot ?? input.repoRoot, message.routePath);
441
+ if (!(await fs.pathExists(absolutePath))) {
442
+ return;
443
+ }
444
+ const routeContent = await fs.readText(absolutePath);
445
+ const parsed = parseRouteFileContent(routeContent, message.fromRole, message.toRole);
446
+ if (parsed.body.trim() === message.body.trim() &&
447
+ parsed.type === message.type &&
448
+ arraysEqual(parsed.artifactRefs, message.artifactRefs)) {
449
+ await fs.writeText(absolutePath, "");
450
+ }
451
+ }
452
+ function arraysEqual(left, right) {
453
+ if (left.length !== right.length) {
454
+ return false;
455
+ }
456
+ return left.every((value, index) => value === right[index]);
323
457
  }
324
458
  function applyMessageSnapshots(messages, snapshots) {
325
459
  if (snapshots.length === 0) {
@@ -331,43 +465,6 @@ function applyMessageSnapshots(messages, snapshots) {
331
465
  }
332
466
  return [...latest.values()].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
333
467
  }
334
- async function writeMessageBody(fs, repoRoot, handoffDir, message) {
335
- const bodyPath = path.posix.join(handoffDir, "messages", `${message.id}.md`);
336
- await fs.writeText(resolveRepoPath(repoRoot, bodyPath), renderMessageBodyFile(message));
337
- return bodyPath;
338
- }
339
- function renderMessageBodyFile(message) {
340
- const artifactRefs = message.artifactRefs.length > 0
341
- ? message.artifactRefs.map((artifact) => `- ${artifact}`).join("\n")
342
- : "- none";
343
- return `# VCM Message ${message.id}
344
-
345
- - Task: ${message.taskSlug}
346
- - From: ${message.fromRole}
347
- - To: ${message.toRole}
348
- - Type: ${message.type}
349
-
350
- ## Body
351
-
352
- ${message.body}
353
-
354
- ## Artifact Refs
355
-
356
- ${artifactRefs}
357
- `;
358
- }
359
- async function getMessageOrThrow(fs, input) {
360
- const messages = await readLatestMessages(fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
361
- const message = messages.find((candidate) => candidate.id === input.messageId);
362
- if (!message) {
363
- throw new VcmError({
364
- code: "MESSAGE_MISSING",
365
- message: `Message does not exist: ${input.messageId}`,
366
- statusCode: 404
367
- });
368
- }
369
- return message;
370
- }
371
468
  async function readLatestMessages(fs, messagesPath) {
372
469
  if (!(await fs.pathExists(messagesPath))) {
373
470
  return [];
@@ -96,12 +96,12 @@ function toRoleTurnState(role, sessions) {
96
96
  }
97
97
  function getLatestDeliveredMessage(messages) {
98
98
  return messages
99
- .filter((message) => message.status === "delivering" || message.status === "submitted" || message.status === "delivered" || message.status === "staged")
99
+ .filter((message) => message.status === "delivering" || message.status === "submitted")
100
100
  .sort((left, right) => getMessageDeliveredAt(left).localeCompare(getMessageDeliveredAt(right)))
101
101
  .at(-1);
102
102
  }
103
103
  function getMessageDeliveredAt(message) {
104
- return message?.submittedAt ?? message?.deliveredAt ?? message?.stagedAt ?? message?.createdAt ?? "";
104
+ return message?.submittedAt ?? message?.deliveredAt ?? message?.createdAt ?? "";
105
105
  }
106
106
  function getCompletedAtForDelivery(roleState, deliveredAt) {
107
107
  if (roleState.status !== "idle" || !roleState.lastAnswerEndedAt) {