vibe-coding-master 0.8.1 → 0.8.2

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,3 +1,4 @@
1
+ import { getRoleDefinition, isVcmRoleName, VCM_ROLE_NAMES } from "../../shared/constants.js";
1
2
  import { VcmError } from "../errors.js";
2
3
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
3
4
  import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
@@ -14,7 +15,7 @@ const DEFAULT_POLL_TIMEOUT_MS = 35_000;
14
15
  const LARK_REGISTRATION_CONFIRM_TIMEOUT_MS = 15_000;
15
16
  const GATEWAY_ROUND_FINAL_WAIT_MS = 12_000;
16
17
  const GATEWAY_ROUND_FINAL_POLL_MS = 250;
17
- const GATEWAY_TRANSLATION_FAILURE_TEXT = "PM 回复已收到,但翻译失败。\n发送 /retry 重新翻译。";
18
+ const GATEWAY_TRANSLATION_FAILURE_TEXT = "角色回复已收到,但翻译失败。\n发送 /retry 重新翻译。";
18
19
  const COMMANDS_ALLOWED_WHEN_DISABLED = new Set([
19
20
  "help",
20
21
  "start",
@@ -33,7 +34,7 @@ export function createGatewayService(deps) {
33
34
  let qrLogin = null;
34
35
  let larkRegistrationState = null;
35
36
  let lastFailedTranslation = null;
36
- let lastPmInputMessageId = null;
37
+ let lastGatewayInputMessageId = null;
37
38
  // Runtime channel-connection arming switch. Not persisted: every process starts
38
39
  // disarmed. `ensurePolling` is the single chokepoint that reads it, so no
39
40
  // self-heal path can connect the channel while this is false.
@@ -257,7 +258,7 @@ export function createGatewayService(deps) {
257
258
  }
258
259
  async function handlePlainInbound(update, text) {
259
260
  try {
260
- const target = await resolvePlainTextPmTarget(text);
261
+ const target = await resolvePlainTextRoleTarget(text);
261
262
  if (typeof target === "string") {
262
263
  await reply(await deps.settings.loadSettings(), update.fromUserId, target);
263
264
  await recordInbound(update, "ok", "plain");
@@ -267,16 +268,16 @@ export function createGatewayService(deps) {
267
268
  await reply(await deps.settings.loadSettings(), update.fromUserId, "已收到,正在翻译...");
268
269
  }
269
270
  else {
270
- await reply(await deps.settings.loadSettings(), update.fromUserId, "已收到,正在发送给 PM...");
271
+ await reply(await deps.settings.loadSettings(), update.fromUserId, `已收到,正在发送给 ${roleLabel(target.role)}...`);
271
272
  }
272
273
  const englishText = target.settings.translationEnabled
273
- ? await translatePlainTextForPm(target, text, update)
274
+ ? await translatePlainTextForRole(target, text, update)
274
275
  : text;
275
276
  await submitTerminalInput(deps.runtime, target.session.id, englishText);
276
- lastPmInputMessageId = update.messageId;
277
+ lastGatewayInputMessageId = update.messageId;
277
278
  await reply(await deps.settings.loadSettings(), update.fromUserId, target.settings.translationEnabled
278
- ? formatGatewayInputTranslationSuccess(englishText)
279
- : "已发送给 PM。");
279
+ ? formatGatewayInputTranslationSuccess(target.role, englishText)
280
+ : `已发送给 ${roleLabel(target.role)}。`);
280
281
  await recordInbound(update, "ok", "plain");
281
282
  }
282
283
  catch (error) {
@@ -288,13 +289,13 @@ export function createGatewayService(deps) {
288
289
  await recordInbound(update, "error", "plain", message);
289
290
  }
290
291
  }
291
- async function translatePlainTextForPm(target, text, update) {
292
+ async function translatePlainTextForRole(target, text, update) {
292
293
  try {
293
294
  return (await deps.translationService.translateUserInput({
294
295
  repoRoot: target.project.repoRoot,
295
296
  taskRepoRoot: getTaskRuntimeRepoRoot(target.task),
296
297
  taskSlug: target.task.taskSlug,
297
- role: "project-manager",
298
+ role: target.role,
298
299
  text,
299
300
  useContext: false,
300
301
  send: false
@@ -302,7 +303,7 @@ export function createGatewayService(deps) {
302
303
  }
303
304
  catch (error) {
304
305
  const message = errorMessage(error);
305
- await reply(await deps.settings.loadSettings(), update.fromUserId, formatGatewayInputTranslationFailure(message));
306
+ await reply(await deps.settings.loadSettings(), update.fromUserId, formatGatewayInputTranslationFailure(target.role, message));
306
307
  await recordInbound(update, "error", "plain", message);
307
308
  throw new HandledGatewayInboundError();
308
309
  }
@@ -357,7 +358,7 @@ export function createGatewayService(deps) {
357
358
  return [
358
359
  "Gateway is connected but off.",
359
360
  "Available commands while off: /help, /start, /status, /projects, /tasks.",
360
- "Turn Gateway on in desktop VCM to send PM messages or run task-changing commands."
361
+ "Turn Gateway on in desktop VCM to send role messages or run task-changing commands."
361
362
  ].join("\n");
362
363
  }
363
364
  switch (command.kind) {
@@ -387,8 +388,10 @@ export function createGatewayService(deps) {
387
388
  return closeTaskConfirm(command.taskSlug);
388
389
  case "translate":
389
390
  return setGatewayTranslation(command.enabled);
391
+ case "role":
392
+ return command.role ? setGatewayTargetRole(command.role) : gatewayRoleText(settings);
390
393
  case "plain":
391
- return sendPlainTextToPm(command.text);
394
+ return sendPlainTextToRole(command.text);
392
395
  case "unknown":
393
396
  return `Unknown command: ${command.name}. Send /help for available commands.`;
394
397
  }
@@ -410,6 +413,8 @@ export function createGatewayService(deps) {
410
413
  await deps.settings.saveSettings({
411
414
  ...settings,
412
415
  currentProjectId: project.repoRoot,
416
+ currentTaskSlug: null,
417
+ targetRole: "project-manager",
413
418
  updatedAt: now()
414
419
  });
415
420
  }
@@ -433,6 +438,7 @@ export function createGatewayService(deps) {
433
438
  ...settings,
434
439
  currentProjectId: project.repoRoot,
435
440
  currentTaskSlug: nextTaskSlug,
441
+ targetRole: "project-manager",
436
442
  updatedAt: now()
437
443
  });
438
444
  }
@@ -461,6 +467,7 @@ export function createGatewayService(deps) {
461
467
  ...settings,
462
468
  currentProjectId: project.repoRoot,
463
469
  currentTaskSlug: null,
470
+ targetRole: "project-manager",
464
471
  updatedAt: now()
465
472
  });
466
473
  return `Selected project:\n${project.repoRoot}\nbranch: ${project.branch}\ncommit: ${project.shortHeadCommit ?? project.headCommit ?? "unknown"}`;
@@ -501,6 +508,7 @@ export function createGatewayService(deps) {
501
508
  await deps.settings.saveSettings({
502
509
  ...settings,
503
510
  currentTaskSlug: task.taskSlug,
511
+ targetRole: "project-manager",
504
512
  updatedAt: now()
505
513
  });
506
514
  return `Selected task: ${task.taskSlug}\nstatus: ${task.status}\nbranch: ${task.branch}`;
@@ -520,6 +528,7 @@ export function createGatewayService(deps) {
520
528
  ...settings,
521
529
  currentProjectId: project.repoRoot,
522
530
  currentTaskSlug: task.taskSlug,
531
+ targetRole: "project-manager",
523
532
  translationEnabled: preferences.translationEnabled,
524
533
  updatedAt: now()
525
534
  });
@@ -608,12 +617,13 @@ export function createGatewayService(deps) {
608
617
  }
609
618
  const result = await deps.taskCloseService.closeTask(project.repoRoot, taskSlug);
610
619
  clearFailedTranslation(project.repoRoot, taskSlug);
611
- const latestPmReplies = { ...settings.latestPmReplies };
612
- delete latestPmReplies[latestPmReplyKey(project.repoRoot, taskSlug)];
620
+ const latestRoleReplies = Object.fromEntries(Object.entries(settings.latestRoleReplies)
621
+ .filter(([, reply]) => reply.repoRoot !== project.repoRoot || reply.taskSlug !== taskSlug));
613
622
  await deps.settings.saveSettings({
614
623
  ...settings,
615
624
  currentTaskSlug: null,
616
- latestPmReplies,
625
+ targetRole: "project-manager",
626
+ latestRoleReplies,
617
627
  pendingConfirmations: {
618
628
  ...settings.pendingConfirmations,
619
629
  closeTask: null
@@ -673,19 +683,20 @@ export function createGatewayService(deps) {
673
683
  await ensurePolling();
674
684
  const lines = [
675
685
  "Gateway started.",
676
- "Full mobile commands and PM messages are now enabled.",
686
+ "Full mobile commands and role messages are now enabled.",
677
687
  `Project: ${enabled.currentProjectId ?? "none"}`,
678
- `Task: ${enabled.currentTaskSlug ?? "none"}`
688
+ `Task: ${enabled.currentTaskSlug ?? "none"}`,
689
+ `Role: ${roleLabel(enabled.targetRole)}`
679
690
  ];
680
- const latestReply = getLatestPmReply(enabled);
691
+ const latestReply = getLatestRoleReply(enabled);
681
692
  if (latestReply) {
682
- const rendered = await renderLatestPmReply(enabled, latestReply);
683
- lines.push("", "Latest PM reply:", rendered.text);
693
+ const rendered = await renderLatestRoleReply(enabled, latestReply);
694
+ lines.push("", `Latest ${roleLabel(enabled.targetRole)} reply:`, rendered.text);
684
695
  }
685
696
  return lines.join("\n");
686
697
  }
687
- async function sendPlainTextToPm(text) {
688
- const target = await resolvePlainTextPmTarget(text);
698
+ async function sendPlainTextToRole(text) {
699
+ const target = await resolvePlainTextRoleTarget(text);
689
700
  if (typeof target === "string") {
690
701
  return target;
691
702
  }
@@ -694,16 +705,16 @@ export function createGatewayService(deps) {
694
705
  repoRoot: target.project.repoRoot,
695
706
  taskRepoRoot: getTaskRuntimeRepoRoot(target.task),
696
707
  taskSlug: target.task.taskSlug,
697
- role: "project-manager",
708
+ role: target.role,
698
709
  text,
699
710
  useContext: false,
700
711
  send: false
701
712
  })).englishPreview
702
713
  : text;
703
714
  await submitTerminalInput(deps.runtime, target.session.id, englishText);
704
- return "Sent to PM.";
715
+ return `Sent to ${roleLabel(target.role)}.`;
705
716
  }
706
- async function resolvePlainTextPmTarget(text) {
717
+ async function resolvePlainTextRoleTarget(text) {
707
718
  if (!text.trim()) {
708
719
  return "Empty message ignored.";
709
720
  }
@@ -717,18 +728,51 @@ export function createGatewayService(deps) {
717
728
  });
718
729
  }
719
730
  const task = await deps.taskService.loadTask(project.repoRoot, settings.currentTaskSlug);
720
- const session = await deps.sessionService.getRoleSession(project.repoRoot, task.taskSlug, "project-manager");
731
+ const role = settings.targetRole;
732
+ const session = await deps.sessionService.getRoleSession(project.repoRoot, task.taskSlug, role);
721
733
  if (!session || session.status !== "running") {
722
734
  throw new VcmError({
723
- code: "PM_SESSION_NOT_RUNNING",
724
- message: "The current task's PM session is not running.",
735
+ code: "GATEWAY_TARGET_SESSION_NOT_RUNNING",
736
+ message: `The current task's ${roleLabel(role)} session is not running.`,
725
737
  statusCode: 409
726
738
  });
727
739
  }
728
740
  if (session.activityStatus === "running") {
729
- return "PM is still working on the current turn. Please wait and send again later.";
741
+ return `${roleLabel(role)} is still working on the current turn. Please wait and send again later.`;
730
742
  }
731
- return { project, task, session, settings };
743
+ return { project, task, session, role, settings };
744
+ }
745
+ async function setGatewayTargetRole(role) {
746
+ const project = await ensureProject();
747
+ const settings = await syncDesktopContext(await deps.settings.loadSettings());
748
+ if (!settings.currentTaskSlug) {
749
+ throw new VcmError({
750
+ code: "TASK_NOT_SELECTED",
751
+ message: "No task is selected. Use /tasks and /use-task first.",
752
+ statusCode: 409
753
+ });
754
+ }
755
+ await deps.taskService.loadTask(project.repoRoot, settings.currentTaskSlug);
756
+ const updated = await deps.settings.updateSettings({ targetRole: role });
757
+ return `Gateway role: ${roleLabel(updated.targetRole)}.`;
758
+ }
759
+ async function gatewayRoleText(settings) {
760
+ const synced = await syncDesktopContext(settings);
761
+ const lines = [`Current Gateway role: ${roleLabel(synced.targetRole)}.`, "Available roles:"];
762
+ for (const role of VCM_ROLE_NAMES) {
763
+ let status = "not started";
764
+ if (synced.currentProjectId && synced.currentTaskSlug) {
765
+ try {
766
+ const session = await deps.sessionService.getRoleSession(synced.currentProjectId, synced.currentTaskSlug, role);
767
+ status = session?.status ?? status;
768
+ }
769
+ catch {
770
+ status = "missing";
771
+ }
772
+ }
773
+ lines.push(`- ${gatewayRoleCommandName(role)} [${status}]`);
774
+ }
775
+ return lines.join("\n");
732
776
  }
733
777
  async function reply(settings, userId, text) {
734
778
  await sendGatewayText(settings, userId, text);
@@ -766,9 +810,20 @@ export function createGatewayService(deps) {
766
810
  }
767
811
  async function exposeStatus(settings) {
768
812
  const preferences = await deps.appSettings.getPreferences();
813
+ let targetRoleSessionStatus = null;
814
+ if (settings.currentProjectId && settings.currentTaskSlug) {
815
+ try {
816
+ const session = await deps.sessionService.getRoleSession(settings.currentProjectId, settings.currentTaskSlug, settings.targetRole);
817
+ targetRoleSessionStatus = session?.status ?? "not_started";
818
+ }
819
+ catch {
820
+ targetRoleSessionStatus = "missing";
821
+ }
822
+ }
769
823
  return {
770
824
  ...deps.settings.expose(settings, isRunning(), connectionEnabled),
771
- lastPmInputMessageId,
825
+ targetRoleSessionStatus,
826
+ lastGatewayInputMessageId,
772
827
  pauseAlertSoundEnabled: preferences.flowPauseAlerts
773
828
  };
774
829
  }
@@ -784,6 +839,7 @@ export function createGatewayService(deps) {
784
839
  `Translation: ${synced.translationEnabled ? "on" : "off"}`,
785
840
  `Project: ${project?.repoRoot ?? synced.currentProjectId ?? "none"}`,
786
841
  `Task: ${synced.currentTaskSlug ?? "none"}`,
842
+ `Role: ${roleLabel(synced.targetRole)}`,
787
843
  `Last poll: ${synced.lastPollStatus.state}${synced.lastPollStatus.error ? ` (${synced.lastPollStatus.error})` : ""}`
788
844
  ].join("\n");
789
845
  }
@@ -803,7 +859,10 @@ export function createGatewayService(deps) {
803
859
  return exposeStatus(settings);
804
860
  },
805
861
  async updateSettings(input) {
806
- const currentSettings = await deps.settings.loadSettings();
862
+ let currentSettings = await deps.settings.loadSettings();
863
+ if (input.enabled === true || currentSettings.enabled) {
864
+ currentSettings = await syncDesktopContext(currentSettings);
865
+ }
807
866
  const enablingGateway = input.enabled === true && !currentSettings.enabled;
808
867
  const gatewayStartInput = input.enabled === true
809
868
  ? { ...input, translationEnabled: true }
@@ -818,9 +877,6 @@ export function createGatewayService(deps) {
818
877
  await enableGatewayTranslationRuntime({ disablePauseAlertSound: enablingGateway });
819
878
  }
820
879
  let settings = await deps.settings.updateSettings(updateInput);
821
- if (settings.enabled) {
822
- settings = await syncDesktopContext(settings);
823
- }
824
880
  if (toAccount(settings)) {
825
881
  await ensurePolling();
826
882
  }
@@ -1095,8 +1151,15 @@ export function createGatewayService(deps) {
1095
1151
  gatewayStatus: status
1096
1152
  };
1097
1153
  },
1098
- async handlePmStop(input) {
1099
- if (input.session.role !== "project-manager") {
1154
+ async handleRoleStop(input) {
1155
+ if (!isVcmRoleName(input.session.role)) {
1156
+ return;
1157
+ }
1158
+ const role = input.session.role;
1159
+ const settings = await syncDesktopContext(await deps.settings.loadSettings());
1160
+ if (settings.currentProjectId !== input.repoRoot
1161
+ || settings.currentTaskSlug !== input.taskSlug
1162
+ || settings.targetRole !== role) {
1100
1163
  return;
1101
1164
  }
1102
1165
  const transcriptPath = resolveExistingClaudeTranscriptPath(input.session);
@@ -1104,46 +1167,52 @@ export function createGatewayService(deps) {
1104
1167
  return;
1105
1168
  }
1106
1169
  const events = await readTranscriptTextEvents(transcriptPath);
1107
- const round = await waitForGatewayRoundFinal(input.repoRoot, input.taskSlug);
1170
+ const round = await waitForGatewayRoundFinal(input.repoRoot, input.taskSlug, role);
1108
1171
  if (!round) {
1109
1172
  return;
1110
1173
  }
1111
- const cursorKey = `${input.taskSlug}:project-manager:${input.session.claudeSessionId}`;
1112
- const settings = await deps.settings.loadSettings();
1113
- const cursor = settings.pushCursors[cursorKey];
1174
+ const latestSettings = await syncDesktopContext(await deps.settings.loadSettings());
1175
+ if (latestSettings.currentProjectId !== input.repoRoot
1176
+ || latestSettings.currentTaskSlug !== input.taskSlug
1177
+ || latestSettings.targetRole !== role) {
1178
+ return;
1179
+ }
1180
+ const cursorKey = `${input.taskSlug}:${role}:${input.session.claudeSessionId}`;
1181
+ const cursor = latestSettings.pushCursors[cursorKey];
1114
1182
  const nextEvents = selectEventsAfterCursor(events, cursor?.lastTranscriptEventId)
1115
1183
  .filter(isFinalTurnTextEvent);
1116
1184
  if (nextEvents.length === 0) {
1117
1185
  return;
1118
1186
  }
1119
- const latestReply = toGatewayPmReply(nextEvents[nextEvents.length - 1]);
1187
+ const latestReply = toGatewayRoleReply(nextEvents[nextEvents.length - 1]);
1120
1188
  const text = latestReply.text.trim();
1121
1189
  if (!text) {
1122
1190
  return;
1123
1191
  }
1124
- await saveLatestPmReply(input, latestReply);
1125
- const account = toAccount(settings);
1126
- const boundUserId = settings.binding.boundUserId;
1192
+ await saveLatestRoleReply(input, latestReply);
1193
+ const account = toAccount(latestSettings);
1194
+ const boundUserId = latestSettings.binding.boundUserId;
1127
1195
  // A disarmed gateway never touches the channel: skip the outbound push.
1128
- // The round-final PM reply was already cached above and replays on the next /start.
1129
- if (!connectionEnabled || !settings.enabled || !account || !boundUserId) {
1196
+ // The selected role's round-final reply was already cached above and replays on the next /start.
1197
+ if (!connectionEnabled || !latestSettings.enabled || !account || !boundUserId) {
1130
1198
  return;
1131
1199
  }
1132
1200
  const roundNotice = formatGatewayRoundNotice(round);
1133
- const originalMessage = formatGatewayPmOriginalReply(text, roundNotice);
1134
- await sendGatewayText(settings, boundUserId, originalMessage);
1201
+ const originalMessage = formatGatewayRoleOriginalReply(role, text, roundNotice);
1202
+ await sendGatewayText(latestSettings, boundUserId, originalMessage);
1135
1203
  let output;
1136
- if (settings.translationEnabled) {
1137
- output = await renderGatewayPmOutput({
1138
- settings,
1204
+ if (latestSettings.translationEnabled) {
1205
+ output = await renderGatewayRoleOutput({
1206
+ settings: latestSettings,
1139
1207
  repoRoot: input.repoRoot,
1140
1208
  taskSlug: input.taskSlug,
1209
+ role,
1141
1210
  sourceText: text,
1142
1211
  sourceEntryIds: latestReply.transcriptEventId ? [latestReply.transcriptEventId] : undefined
1143
1212
  });
1144
- await sendGatewayText(settings, boundUserId, output.translationFailed
1145
- ? formatGatewayPmTranslationFailure(output.translationError)
1146
- : formatGatewayPmTranslatedReply(output.text));
1213
+ await sendGatewayText(latestSettings, boundUserId, output.translationFailed
1214
+ ? formatGatewayRoleTranslationFailure(role, output.translationError)
1215
+ : formatGatewayRoleTranslatedReply(role, output.text));
1147
1216
  }
1148
1217
  else {
1149
1218
  clearFailedTranslation(input.repoRoot, input.taskSlug);
@@ -1162,16 +1231,16 @@ export function createGatewayService(deps) {
1162
1231
  checkedAt: now(),
1163
1232
  direction: "outbound",
1164
1233
  result: output?.translationFailed ? "error" : "ok",
1165
- command: "pm-stop",
1234
+ command: `${role}-stop`,
1166
1235
  preview: (output?.text ?? originalMessage).slice(0, 160),
1167
1236
  error: output?.translationError
1168
1237
  },
1169
1238
  updatedAt: now()
1170
1239
  });
1171
1240
  await deps.audit.record({
1172
- type: "gateway.pm_push",
1241
+ type: "gateway.role_push",
1173
1242
  result: output?.translationFailed ? "error" : "ok",
1174
- command: "pm-stop",
1243
+ command: `${role}-stop`,
1175
1244
  preview: output ? `${originalMessage}\n\n${output.text}` : originalMessage,
1176
1245
  error: output?.translationError
1177
1246
  });
@@ -1216,17 +1285,17 @@ export function createGatewayService(deps) {
1216
1285
  };
1217
1286
  }
1218
1287
  };
1219
- function getLatestPmReply(settings) {
1288
+ function getLatestRoleReply(settings) {
1220
1289
  if (!settings.currentProjectId || !settings.currentTaskSlug) {
1221
1290
  return undefined;
1222
1291
  }
1223
- return settings.latestPmReplies[latestPmReplyKey(settings.currentProjectId, settings.currentTaskSlug)];
1292
+ return settings.latestRoleReplies[latestRoleReplyKey(settings.currentProjectId, settings.currentTaskSlug, settings.targetRole)];
1224
1293
  }
1225
- async function waitForGatewayRoundFinal(repoRoot, taskSlug) {
1294
+ async function waitForGatewayRoundFinal(repoRoot, taskSlug, role) {
1226
1295
  const startedAt = Date.now();
1227
1296
  while (Date.now() - startedAt <= GATEWAY_ROUND_FINAL_WAIT_MS) {
1228
1297
  const round = await getGatewayRoundState(repoRoot, taskSlug);
1229
- if (round && isGatewayRoundFinal(round)) {
1298
+ if (round && isGatewayRoundFinal(round) && round.activeRole === role) {
1230
1299
  return round;
1231
1300
  }
1232
1301
  if (round && isGatewayNonFinalTerminalRound(round)) {
@@ -1295,47 +1364,48 @@ export function createGatewayService(deps) {
1295
1364
  const trimmed = value.trim();
1296
1365
  return trimmed.length > 600 ? `${trimmed.slice(0, 600)}...` : trimmed;
1297
1366
  }
1298
- function formatGatewayPmOriginalReply(text, roundNotice) {
1367
+ function formatGatewayRoleOriginalReply(role, text, roundNotice) {
1299
1368
  return [
1300
- "PM final reply 原文:",
1369
+ `${roleLabel(role)} Round Final Reply 原文:`,
1301
1370
  "",
1302
1371
  text.trim(),
1303
1372
  roundNotice ? "" : undefined,
1304
1373
  roundNotice ? `Round: ${roundNotice}` : undefined
1305
1374
  ].filter((line) => line !== undefined).join("\n");
1306
1375
  }
1307
- function formatGatewayPmTranslatedReply(text) {
1376
+ function formatGatewayRoleTranslatedReply(role, text) {
1308
1377
  return [
1309
- "PM final reply 翻译:",
1378
+ `${roleLabel(role)} Round Final Reply 翻译:`,
1310
1379
  "",
1311
1380
  text.trim()
1312
1381
  ].join("\n");
1313
1382
  }
1314
- function formatGatewayPmTranslationFailure(error) {
1383
+ function formatGatewayRoleTranslationFailure(role, error) {
1315
1384
  return [
1316
- GATEWAY_TRANSLATION_FAILURE_TEXT,
1385
+ `${roleLabel(role)} ${GATEWAY_TRANSLATION_FAILURE_TEXT}`,
1317
1386
  error ? `原因:${error}` : undefined
1318
1387
  ].filter((line) => line !== undefined).join("\n");
1319
1388
  }
1320
- function formatGatewayInputTranslationSuccess(englishText) {
1389
+ function formatGatewayInputTranslationSuccess(role, englishText) {
1321
1390
  return [
1322
- "翻译完成,已发送给 PM:",
1391
+ `翻译完成,已发送给 ${roleLabel(role)}:`,
1323
1392
  "",
1324
1393
  englishText.trim()
1325
1394
  ].join("\n");
1326
1395
  }
1327
- function formatGatewayInputTranslationFailure(error) {
1396
+ function formatGatewayInputTranslationFailure(role, error) {
1328
1397
  return [
1329
- "翻译失败,消息未发送给 PM。",
1398
+ `翻译失败,消息未发送给 ${roleLabel(role)}。`,
1330
1399
  `原因:${error}`,
1331
1400
  "请重新输入后再试。"
1332
1401
  ].join("\n");
1333
1402
  }
1334
- async function renderLatestPmReply(settings, reply) {
1335
- const rendered = await renderGatewayPmOutput({
1403
+ async function renderLatestRoleReply(settings, reply) {
1404
+ const rendered = await renderGatewayRoleOutput({
1336
1405
  settings,
1337
1406
  repoRoot: reply.repoRoot,
1338
1407
  taskSlug: reply.taskSlug,
1408
+ role: reply.role,
1339
1409
  sourceText: reply.text,
1340
1410
  sourceEntryIds: reply.transcriptEventId ? [reply.transcriptEventId] : undefined
1341
1411
  });
@@ -1344,7 +1414,7 @@ export function createGatewayService(deps) {
1344
1414
  text: reply.truncated && !rendered.translationFailed ? `${rendered.text}\n\n[truncated]` : rendered.text
1345
1415
  };
1346
1416
  }
1347
- async function renderGatewayPmOutput(input) {
1417
+ async function renderGatewayRoleOutput(input) {
1348
1418
  if (!input.settings.translationEnabled) {
1349
1419
  clearFailedTranslation(input.repoRoot, input.taskSlug);
1350
1420
  return {
@@ -1356,7 +1426,7 @@ export function createGatewayService(deps) {
1356
1426
  const text = await deps.translationService.translateGatewayOutput({
1357
1427
  repoRoot: input.repoRoot,
1358
1428
  taskSlug: input.taskSlug,
1359
- role: "project-manager",
1429
+ role: input.role,
1360
1430
  text: input.sourceText,
1361
1431
  sourceEntryIds: input.sourceEntryIds
1362
1432
  });
@@ -1371,7 +1441,7 @@ export function createGatewayService(deps) {
1371
1441
  lastFailedTranslation = {
1372
1442
  repoRoot: input.repoRoot,
1373
1443
  taskSlug: input.taskSlug,
1374
- role: "project-manager",
1444
+ role: input.role,
1375
1445
  sourceText: input.sourceText,
1376
1446
  failedAt: now(),
1377
1447
  error: message,
@@ -1415,7 +1485,7 @@ export function createGatewayService(deps) {
1415
1485
  lastFailedTranslation = null;
1416
1486
  }
1417
1487
  }
1418
- function toGatewayPmReply(event) {
1488
+ function toGatewayRoleReply(event) {
1419
1489
  return {
1420
1490
  text: event.text,
1421
1491
  truncated: false,
@@ -1423,16 +1493,20 @@ export function createGatewayService(deps) {
1423
1493
  transcriptTimestamp: event.timestamp
1424
1494
  };
1425
1495
  }
1426
- async function saveLatestPmReply(input, reply) {
1496
+ async function saveLatestRoleReply(input, reply) {
1497
+ if (!isVcmRoleName(input.session.role)) {
1498
+ return;
1499
+ }
1427
1500
  const settings = await deps.settings.loadSettings();
1428
- const key = latestPmReplyKey(input.repoRoot, input.taskSlug);
1501
+ const key = latestRoleReplyKey(input.repoRoot, input.taskSlug, input.session.role);
1429
1502
  await deps.settings.saveSettings({
1430
1503
  ...settings,
1431
- latestPmReplies: {
1432
- ...settings.latestPmReplies,
1504
+ latestRoleReplies: {
1505
+ ...settings.latestRoleReplies,
1433
1506
  [key]: {
1434
1507
  repoRoot: input.repoRoot,
1435
1508
  taskSlug: input.taskSlug,
1509
+ role: input.session.role,
1436
1510
  sessionId: input.session.id,
1437
1511
  claudeSessionId: input.session.claudeSessionId,
1438
1512
  transcriptEventId: reply.transcriptEventId,
@@ -1459,8 +1533,8 @@ function selectEventsAfterCursor(events, cursorId) {
1459
1533
  }
1460
1534
  return events.slice(index + 1);
1461
1535
  }
1462
- function latestPmReplyKey(repoRoot, taskSlug) {
1463
- return JSON.stringify([repoRoot, taskSlug]);
1536
+ function latestRoleReplyKey(repoRoot, taskSlug, role) {
1537
+ return JSON.stringify([repoRoot, taskSlug, role]);
1464
1538
  }
1465
1539
  function projectsText(projects) {
1466
1540
  if (projects.length === 0) {
@@ -1478,7 +1552,7 @@ function helpText(enabled) {
1478
1552
  "/status",
1479
1553
  "/projects",
1480
1554
  "/tasks",
1481
- "Send /start to enable PM messages and task-changing commands."
1555
+ "Send /start to enable role messages and task-changing commands."
1482
1556
  ].join("\n");
1483
1557
  }
1484
1558
  return [
@@ -1487,6 +1561,8 @@ function helpText(enabled) {
1487
1561
  "/start",
1488
1562
  "/retry",
1489
1563
  "/status",
1564
+ "/role",
1565
+ "/role <pm|architect|coder|tester|reviewer>",
1490
1566
  "/projects",
1491
1567
  "/use-project <index-or-path>",
1492
1568
  "/pull-current",
@@ -1499,6 +1575,12 @@ function helpText(enabled) {
1499
1575
  "/translate off"
1500
1576
  ].join("\n");
1501
1577
  }
1578
+ function roleLabel(role) {
1579
+ return role === "project-manager" ? "PM" : getRoleDefinition(role).label;
1580
+ }
1581
+ function gatewayRoleCommandName(role) {
1582
+ return role === "project-manager" ? "pm" : role;
1583
+ }
1502
1584
  function formatAheadBehind(project) {
1503
1585
  if (!project.upstreamBranch) {
1504
1586
  return "no upstream";