vibe-coding-master 0.0.5 → 0.0.7
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.
- package/README.md +168 -63
- package/dist/backend/adapters/translation-provider.js +145 -0
- package/dist/backend/api/artifact-routes.js +3 -0
- package/dist/backend/api/harness-routes.js +22 -0
- package/dist/backend/api/project-routes.js +3 -8
- package/dist/backend/api/translation-routes.js +70 -0
- package/dist/backend/runtime/node-pty-runtime.js +20 -18
- package/dist/backend/server.js +31 -1
- package/dist/backend/services/app-settings-service.js +128 -0
- package/dist/backend/services/artifact-service.js +7 -4
- package/dist/backend/services/claude-transcript-service.js +509 -0
- package/dist/backend/services/harness-service.js +178 -0
- package/dist/backend/services/project-service.js +4 -0
- package/dist/backend/services/session-service.js +7 -5
- package/dist/backend/services/status-service.js +76 -0
- package/dist/backend/services/translation-prompts.js +173 -0
- package/dist/backend/services/translation-queue.js +39 -0
- package/dist/backend/services/translation-service.js +546 -0
- package/dist/backend/templates/handoff.js +32 -0
- package/dist/backend/templates/harness/architect-agent.js +12 -0
- package/dist/backend/templates/harness/claude-root.js +14 -0
- package/dist/backend/templates/harness/coder-agent.js +11 -0
- package/dist/backend/templates/harness/project-manager-agent.js +14 -0
- package/dist/backend/templates/harness/reviewer-agent.js +13 -0
- package/dist/backend/ws/translation-ws.js +35 -0
- package/dist/shared/types/harness.js +1 -0
- package/dist/shared/types/translation.js +5 -0
- package/dist/shared/validation/artifact-check.js +15 -1
- package/dist/shared/validation/language-detect.js +46 -0
- package/dist-frontend/assets/index-BNASqKEK.css +32 -0
- package/dist-frontend/assets/index-Bp49_End.js +58 -0
- package/dist-frontend/index.html +2 -2
- package/docs/cc-best-practices.md +93 -36
- package/docs/product-design.md +313 -1408
- package/docs/v1-architecture-design.md +500 -1153
- package/docs/v1-implementation-plan.md +783 -1604
- package/package.json +3 -1
- package/scripts/verify-package.mjs +121 -0
- package/dist/backend/templates/role-messaging-context.js +0 -44
- package/dist-frontend/assets/index-Bah6k-Ix.css +0 -32
- package/dist-frontend/assets/index-EMaQuIB6.js +0 -58
- package/docs/v1-message-bus-orchestration-design.md +0 -534
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import { TRANSLATION_PROMPT_KEYS } from "../../shared/types/translation.js";
|
|
2
|
+
import { TranslationProviderError } from "../adapters/translation-provider.js";
|
|
3
|
+
import { VcmError } from "../errors.js";
|
|
4
|
+
import { buildTranslationPrompt, getTranslationPromptPreviews, parseTranslationWarning } from "./translation-prompts.js";
|
|
5
|
+
import { createTranslationQueueRegistry } from "./translation-queue.js";
|
|
6
|
+
const DEFAULT_SETTINGS = {
|
|
7
|
+
version: 1,
|
|
8
|
+
enabled: false,
|
|
9
|
+
providerType: "openai-compatible",
|
|
10
|
+
baseUrl: "https://api.openai.com/v1",
|
|
11
|
+
model: "gpt-4o-mini",
|
|
12
|
+
sourceLanguage: "auto",
|
|
13
|
+
targetLanguage: "zh-CN",
|
|
14
|
+
workingLanguage: "en",
|
|
15
|
+
inputMode: "review-before-send",
|
|
16
|
+
translateOutput: true,
|
|
17
|
+
translateUserInput: true,
|
|
18
|
+
contextEnabled: true,
|
|
19
|
+
preserveTechnicalTokens: true,
|
|
20
|
+
skipCjkText: true,
|
|
21
|
+
redactSecrets: true,
|
|
22
|
+
requestTimeoutMs: 15000,
|
|
23
|
+
temperature: 0.1
|
|
24
|
+
};
|
|
25
|
+
const TRANSCRIPT_REPLAY_GRACE_MS = 5000;
|
|
26
|
+
export function createTranslationService(deps) {
|
|
27
|
+
const now = deps.now ?? (() => new Date().toISOString());
|
|
28
|
+
const id = deps.id ?? (() => `tr_${Date.now()}_${Math.random().toString(16).slice(2)}`);
|
|
29
|
+
const queues = createTranslationQueueRegistry();
|
|
30
|
+
const sessionStates = new Map();
|
|
31
|
+
let cachedConfig = null;
|
|
32
|
+
async function loadConfig() {
|
|
33
|
+
if (cachedConfig) {
|
|
34
|
+
return cachedConfig;
|
|
35
|
+
}
|
|
36
|
+
const storedConfig = await deps.appSettings.getTranslationConfig();
|
|
37
|
+
if (!storedConfig) {
|
|
38
|
+
cachedConfig = { settings: DEFAULT_SETTINGS, secrets: {} };
|
|
39
|
+
return cachedConfig;
|
|
40
|
+
}
|
|
41
|
+
const rawSettings = storedConfig.settings ?? {};
|
|
42
|
+
const apiKey = storedConfig.secrets?.apiKey ?? rawSettings.apiKey;
|
|
43
|
+
cachedConfig = {
|
|
44
|
+
settings: normalizeSettings(rawSettings),
|
|
45
|
+
secrets: {
|
|
46
|
+
...(storedConfig.secrets ?? {}),
|
|
47
|
+
...(apiKey !== undefined ? { apiKey } : {})
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
return cachedConfig;
|
|
51
|
+
}
|
|
52
|
+
async function saveConfig(config) {
|
|
53
|
+
cachedConfig = config;
|
|
54
|
+
await deps.appSettings.updateTranslationConfig(config);
|
|
55
|
+
}
|
|
56
|
+
function getState(sessionId) {
|
|
57
|
+
let state = sessionStates.get(sessionId);
|
|
58
|
+
if (!state) {
|
|
59
|
+
state = {
|
|
60
|
+
listeners: new Set(),
|
|
61
|
+
seenTranscriptIds: new Set(),
|
|
62
|
+
entries: []
|
|
63
|
+
};
|
|
64
|
+
sessionStates.set(sessionId, state);
|
|
65
|
+
}
|
|
66
|
+
return state;
|
|
67
|
+
}
|
|
68
|
+
function emit(sessionId, message) {
|
|
69
|
+
const state = getState(sessionId);
|
|
70
|
+
for (const listener of state.listeners) {
|
|
71
|
+
listener(message);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function handleTranscriptEvent(sessionId, event) {
|
|
75
|
+
const state = getState(sessionId);
|
|
76
|
+
if (state.seenTranscriptIds.has(event.id)) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
state.seenTranscriptIds.add(event.id);
|
|
80
|
+
if (event.kind === "text") {
|
|
81
|
+
state.lastAssistantText = event.text;
|
|
82
|
+
}
|
|
83
|
+
const { settings } = await loadConfig();
|
|
84
|
+
if (!settings.enabled || !settings.translateOutput) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (event.kind === "text") {
|
|
88
|
+
await processClaudeOutputText(sessionId, event.text, event.id);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (event.kind === "question" || event.kind === "todo" || event.kind === "agent") {
|
|
92
|
+
await processClaudeOutputText(sessionId, formatStructuredTranscriptEvent(event), event.id);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (event.kind === "tool_use" || event.kind === "tool_result") {
|
|
96
|
+
await pushPreservedTranscriptEntry(sessionId, event.id, formatRawTranscriptEvent(event));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function processClaudeOutputText(sessionId, rawText, entryId) {
|
|
100
|
+
const session = deps.runtime.getSession(sessionId);
|
|
101
|
+
const roleSession = deps.sessionRegistry.get(sessionId);
|
|
102
|
+
if (!session && !roleSession) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const { settings, secrets } = await loadConfig();
|
|
106
|
+
if (!rawText.trim()) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const text = rawText;
|
|
110
|
+
const baseEntry = {
|
|
111
|
+
...createEntry({
|
|
112
|
+
taskSlug: roleSession?.taskSlug ?? session.taskSlug,
|
|
113
|
+
role: roleSession?.role ?? session.role,
|
|
114
|
+
direction: "cc-output-to-user",
|
|
115
|
+
sourceKind: "prose",
|
|
116
|
+
sourceText: text,
|
|
117
|
+
settings,
|
|
118
|
+
status: "translating",
|
|
119
|
+
contextUsed: false,
|
|
120
|
+
id: entryId
|
|
121
|
+
}),
|
|
122
|
+
translationStartedAt: now()
|
|
123
|
+
};
|
|
124
|
+
pushEntry(sessionId, baseEntry);
|
|
125
|
+
const queue = queues.getQueue(sessionId);
|
|
126
|
+
await queue.enqueue(async () => {
|
|
127
|
+
emit(sessionId, { type: "translation-status", status: "translating" });
|
|
128
|
+
try {
|
|
129
|
+
const prompt = buildTranslationPrompt({
|
|
130
|
+
direction: "cc-output-to-user",
|
|
131
|
+
text,
|
|
132
|
+
sourceKind: "prose",
|
|
133
|
+
settings
|
|
134
|
+
});
|
|
135
|
+
const result = await deps.provider.translate({
|
|
136
|
+
settings,
|
|
137
|
+
secrets,
|
|
138
|
+
systemPrompt: prompt.systemPrompt,
|
|
139
|
+
userPrompt: prompt.userPrompt
|
|
140
|
+
});
|
|
141
|
+
const completed = {
|
|
142
|
+
...baseEntry,
|
|
143
|
+
status: "translated",
|
|
144
|
+
translatedText: result.text,
|
|
145
|
+
completedAt: now(),
|
|
146
|
+
tokenUsage: result.tokenUsage
|
|
147
|
+
};
|
|
148
|
+
replaceEntry(sessionId, completed);
|
|
149
|
+
getState(sessionId).lastAssistantText = text;
|
|
150
|
+
emit(sessionId, { type: "translation-status", status: "ready" });
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
const failed = {
|
|
154
|
+
...baseEntry,
|
|
155
|
+
status: "failed",
|
|
156
|
+
error: error instanceof Error ? error.message : "Translation failed.",
|
|
157
|
+
completedAt: now()
|
|
158
|
+
};
|
|
159
|
+
replaceEntry(sessionId, failed);
|
|
160
|
+
emit(sessionId, { type: "translation-status", status: "failed" });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
function pushEntry(sessionId, entry) {
|
|
165
|
+
getState(sessionId).entries.push(entry);
|
|
166
|
+
emit(sessionId, { type: "translation-entry", entry });
|
|
167
|
+
}
|
|
168
|
+
async function pushPreservedTranscriptEntry(sessionId, entryId, sourceText) {
|
|
169
|
+
const session = deps.runtime.getSession(sessionId);
|
|
170
|
+
const roleSession = deps.sessionRegistry.get(sessionId);
|
|
171
|
+
if (!session && !roleSession) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const { settings } = await loadConfig();
|
|
175
|
+
const queue = queues.getQueue(sessionId);
|
|
176
|
+
await queue.enqueue(async () => {
|
|
177
|
+
const entry = createEntry({
|
|
178
|
+
taskSlug: roleSession?.taskSlug ?? session.taskSlug,
|
|
179
|
+
role: roleSession?.role ?? session.role,
|
|
180
|
+
direction: "cc-output-to-user",
|
|
181
|
+
sourceKind: "tool-output",
|
|
182
|
+
sourceText,
|
|
183
|
+
settings,
|
|
184
|
+
status: "preserved",
|
|
185
|
+
contextUsed: false,
|
|
186
|
+
id: entryId,
|
|
187
|
+
translatedText: sourceText,
|
|
188
|
+
completedAt: now()
|
|
189
|
+
});
|
|
190
|
+
pushEntry(sessionId, entry);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
function replaceEntry(sessionId, entry) {
|
|
194
|
+
const state = getState(sessionId);
|
|
195
|
+
state.entries = state.entries.map((current) => current.id === entry.id ? entry : current);
|
|
196
|
+
emit(sessionId, { type: "translation-entry", entry });
|
|
197
|
+
}
|
|
198
|
+
function createEntry(input) {
|
|
199
|
+
return {
|
|
200
|
+
id: input.id ?? id(),
|
|
201
|
+
taskSlug: input.taskSlug,
|
|
202
|
+
role: input.role,
|
|
203
|
+
direction: input.direction,
|
|
204
|
+
sourceKind: input.sourceKind,
|
|
205
|
+
sourceLanguage: input.direction === "user-input-to-english" ? input.settings.sourceLanguage : "en",
|
|
206
|
+
targetLanguage: input.direction === "user-input-to-english" ? "en" : input.settings.targetLanguage,
|
|
207
|
+
sourceText: input.sourceText,
|
|
208
|
+
translatedText: input.translatedText ?? "",
|
|
209
|
+
status: input.status,
|
|
210
|
+
contextUsed: input.contextUsed,
|
|
211
|
+
createdAt: now(),
|
|
212
|
+
completedAt: input.completedAt,
|
|
213
|
+
provider: input.settings.providerType,
|
|
214
|
+
model: input.settings.model
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
async getSettings() {
|
|
219
|
+
const { settings, secrets } = await loadConfig();
|
|
220
|
+
return exposeSettings(settings, secrets);
|
|
221
|
+
},
|
|
222
|
+
async updateSettings(input, secrets) {
|
|
223
|
+
const current = await loadConfig();
|
|
224
|
+
const next = {
|
|
225
|
+
settings: normalizeSettings({ ...current.settings, ...input }),
|
|
226
|
+
secrets: {
|
|
227
|
+
...current.secrets,
|
|
228
|
+
...(secrets?.apiKey !== undefined ? { apiKey: secrets.apiKey } : {})
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
await saveConfig(next);
|
|
232
|
+
return exposeSettings(next.settings, next.secrets);
|
|
233
|
+
},
|
|
234
|
+
async getPromptPreviews() {
|
|
235
|
+
const { settings } = await loadConfig();
|
|
236
|
+
return getTranslationPromptPreviews(settings);
|
|
237
|
+
},
|
|
238
|
+
async testProvider() {
|
|
239
|
+
const { settings, secrets } = await loadConfig();
|
|
240
|
+
return deps.provider.testConnection(settings, secrets);
|
|
241
|
+
},
|
|
242
|
+
async translateUserInput(input) {
|
|
243
|
+
const { settings, secrets } = await loadConfig();
|
|
244
|
+
if (!settings.enabled || !settings.translateUserInput) {
|
|
245
|
+
throw new VcmError({
|
|
246
|
+
code: "TRANSLATION_DISABLED",
|
|
247
|
+
message: "Translation input is disabled.",
|
|
248
|
+
statusCode: 409
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (!input.text.trim()) {
|
|
252
|
+
throw new VcmError({
|
|
253
|
+
code: "TRANSLATION_INPUT_EMPTY",
|
|
254
|
+
message: "Translation input cannot be empty.",
|
|
255
|
+
statusCode: 400
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
const roleSession = await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, input.role);
|
|
259
|
+
const sessionState = roleSession ? getState(roleSession.id) : undefined;
|
|
260
|
+
const contextText = settings.contextEnabled && input.useContext !== false
|
|
261
|
+
? sessionState?.lastAssistantText
|
|
262
|
+
: undefined;
|
|
263
|
+
const prompt = buildTranslationPrompt({
|
|
264
|
+
direction: "user-input-to-english",
|
|
265
|
+
text: input.text,
|
|
266
|
+
contextText,
|
|
267
|
+
settings
|
|
268
|
+
});
|
|
269
|
+
const entry = {
|
|
270
|
+
...createEntry({
|
|
271
|
+
taskSlug: input.taskSlug,
|
|
272
|
+
role: input.role,
|
|
273
|
+
direction: "user-input-to-english",
|
|
274
|
+
sourceKind: "prose",
|
|
275
|
+
sourceText: input.text,
|
|
276
|
+
settings,
|
|
277
|
+
status: "translating",
|
|
278
|
+
contextUsed: Boolean(contextText)
|
|
279
|
+
}),
|
|
280
|
+
translationStartedAt: now()
|
|
281
|
+
};
|
|
282
|
+
if (roleSession) {
|
|
283
|
+
pushEntry(roleSession.id, entry);
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
const result = await deps.provider.translate({
|
|
287
|
+
settings,
|
|
288
|
+
secrets,
|
|
289
|
+
systemPrompt: prompt.systemPrompt,
|
|
290
|
+
userPrompt: prompt.userPrompt
|
|
291
|
+
});
|
|
292
|
+
const parsed = prompt.parseWarning ? parseTranslationWarning(result.text) : { text: result.text };
|
|
293
|
+
const completed = {
|
|
294
|
+
...entry,
|
|
295
|
+
status: "translated",
|
|
296
|
+
translatedText: parsed.text,
|
|
297
|
+
warning: parsed.warning,
|
|
298
|
+
completedAt: now(),
|
|
299
|
+
tokenUsage: result.tokenUsage
|
|
300
|
+
};
|
|
301
|
+
if (roleSession) {
|
|
302
|
+
replaceEntry(roleSession.id, completed);
|
|
303
|
+
}
|
|
304
|
+
const mode = input.mode ?? settings.inputMode;
|
|
305
|
+
const shouldSend = input.send === true && mode === "auto-send" && !parsed.warning;
|
|
306
|
+
if (shouldSend) {
|
|
307
|
+
await writeToCurrentRole(input.repoRoot, input.taskSlug, input.role, parsed.text);
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
translation: completed,
|
|
311
|
+
englishPreview: parsed.text,
|
|
312
|
+
contextUsed: Boolean(contextText),
|
|
313
|
+
requiresReview: mode === "review-before-send" || Boolean(parsed.warning),
|
|
314
|
+
sent: shouldSend
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
const failed = {
|
|
319
|
+
...entry,
|
|
320
|
+
status: "failed",
|
|
321
|
+
error: normalizeTranslationError(error),
|
|
322
|
+
completedAt: now()
|
|
323
|
+
};
|
|
324
|
+
if (roleSession) {
|
|
325
|
+
replaceEntry(roleSession.id, failed);
|
|
326
|
+
}
|
|
327
|
+
throw new VcmError({
|
|
328
|
+
code: "TRANSLATION_FAILED",
|
|
329
|
+
message: failed.error ?? "Translation failed.",
|
|
330
|
+
statusCode: 502
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
async sendTranslatedInput(input) {
|
|
335
|
+
await writeToCurrentRole(input.repoRoot, input.taskSlug, input.role, input.englishText);
|
|
336
|
+
},
|
|
337
|
+
subscribeToSession(sessionId, listener) {
|
|
338
|
+
const session = deps.runtime.getSession(sessionId);
|
|
339
|
+
const roleSession = deps.sessionRegistry.get(sessionId);
|
|
340
|
+
if (!session && !roleSession) {
|
|
341
|
+
throw new VcmError({
|
|
342
|
+
code: "SESSION_MISSING",
|
|
343
|
+
message: `Terminal session does not exist: ${sessionId}`,
|
|
344
|
+
statusCode: 404
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
const state = getState(sessionId);
|
|
348
|
+
state.listeners.add(listener);
|
|
349
|
+
for (const entry of state.entries) {
|
|
350
|
+
listener({ type: "translation-entry", entry });
|
|
351
|
+
}
|
|
352
|
+
if (!state.unsubscribeTranscript) {
|
|
353
|
+
if (!roleSession) {
|
|
354
|
+
listener({
|
|
355
|
+
type: "translation-error",
|
|
356
|
+
message: "Claude transcript watcher is unavailable for this session."
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
const replaySince = getTranscriptReplaySince(roleSession);
|
|
361
|
+
state.unsubscribeTranscript = deps.transcripts.subscribeToRoleSession(roleSession, (event) => {
|
|
362
|
+
void handleTranscriptEvent(sessionId, event).catch((error) => {
|
|
363
|
+
emit(sessionId, {
|
|
364
|
+
type: "translation-error",
|
|
365
|
+
message: error instanceof Error ? error.message : "Translation failed."
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
}, {
|
|
369
|
+
onError(error) {
|
|
370
|
+
emit(sessionId, {
|
|
371
|
+
type: "translation-error",
|
|
372
|
+
message: error.message
|
|
373
|
+
});
|
|
374
|
+
},
|
|
375
|
+
replaySince
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
void loadConfig().then(({ settings }) => {
|
|
380
|
+
listener({ type: "translation-status", status: settings.enabled ? "ready" : "paused" });
|
|
381
|
+
});
|
|
382
|
+
return () => {
|
|
383
|
+
state.listeners.delete(listener);
|
|
384
|
+
if (state.listeners.size === 0 && state.unsubscribeTranscript) {
|
|
385
|
+
state.unsubscribeTranscript();
|
|
386
|
+
state.unsubscribeTranscript = undefined;
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
},
|
|
390
|
+
clearSession(sessionId) {
|
|
391
|
+
const state = getState(sessionId);
|
|
392
|
+
state.entries = [];
|
|
393
|
+
queues.clearQueue(sessionId);
|
|
394
|
+
},
|
|
395
|
+
async retryTranslation(sessionId, translationId) {
|
|
396
|
+
const state = getState(sessionId);
|
|
397
|
+
const original = state.entries.find((entry) => entry.id === translationId);
|
|
398
|
+
if (!original) {
|
|
399
|
+
throw new VcmError({
|
|
400
|
+
code: "TRANSLATION_ENTRY_MISSING",
|
|
401
|
+
message: `Translation entry not found: ${translationId}`,
|
|
402
|
+
statusCode: 404
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
if (original.direction !== "cc-output-to-user") {
|
|
406
|
+
throw new VcmError({
|
|
407
|
+
code: "TRANSLATION_RETRY_UNSUPPORTED",
|
|
408
|
+
message: "Only Claude Code output translation entries can be retried.",
|
|
409
|
+
statusCode: 400
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
await processClaudeOutputText(sessionId, original.sourceText);
|
|
413
|
+
return state.entries[state.entries.length - 1] ?? original;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
async function writeToCurrentRole(repoRoot, taskSlug, role, text) {
|
|
417
|
+
const record = await deps.sessionService.getRoleSession(repoRoot, taskSlug, role);
|
|
418
|
+
if (!record || record.status !== "running") {
|
|
419
|
+
throw new VcmError({
|
|
420
|
+
code: "SESSION_NOT_RUNNING",
|
|
421
|
+
message: `${role} session is not running.`,
|
|
422
|
+
statusCode: 409
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
deps.runtime.write(record.id, formatTerminalSubmit(text));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
export function formatTerminalSubmit(text) {
|
|
429
|
+
return `${text.replace(/[\r\n]+$/g, "")}\r`;
|
|
430
|
+
}
|
|
431
|
+
function getTranscriptReplaySince(roleSession) {
|
|
432
|
+
const rawTimestamp = roleSession.startedAt ?? roleSession.updatedAt;
|
|
433
|
+
const timestampMs = Date.parse(rawTimestamp);
|
|
434
|
+
if (!Number.isFinite(timestampMs)) {
|
|
435
|
+
return undefined;
|
|
436
|
+
}
|
|
437
|
+
return new Date(Math.max(0, timestampMs - TRANSCRIPT_REPLAY_GRACE_MS)).toISOString();
|
|
438
|
+
}
|
|
439
|
+
function formatStructuredTranscriptEvent(event) {
|
|
440
|
+
if (event.kind === "question") {
|
|
441
|
+
return event.question.questions.map((question, index) => {
|
|
442
|
+
const title = question.header ? `${question.header}: ${question.question}` : question.question;
|
|
443
|
+
const options = question.options.map((option) => {
|
|
444
|
+
const preview = option.preview ? `\n Preview: ${option.preview}` : "";
|
|
445
|
+
return `- ${option.label}: ${option.description}${preview}`;
|
|
446
|
+
});
|
|
447
|
+
return [`AskUserQuestion ${index + 1}`, title, `Multi-select: ${question.multiSelect ? "yes" : "no"}`, "Options:", ...options]
|
|
448
|
+
.filter(Boolean)
|
|
449
|
+
.join("\n");
|
|
450
|
+
}).join("\n\n");
|
|
451
|
+
}
|
|
452
|
+
if (event.kind === "todo") {
|
|
453
|
+
return [
|
|
454
|
+
"TodoWrite plan",
|
|
455
|
+
...event.todo.todos.map((todo) => {
|
|
456
|
+
const text = todo.status === "in_progress" && todo.activeForm ? todo.activeForm : todo.content;
|
|
457
|
+
return `- [${todo.status}] ${text}`;
|
|
458
|
+
})
|
|
459
|
+
].join("\n");
|
|
460
|
+
}
|
|
461
|
+
return [
|
|
462
|
+
`Agent dispatch${event.agent.subagent_type ? `: ${event.agent.subagent_type}` : ""}`,
|
|
463
|
+
event.agent.description ? `Description: ${event.agent.description}` : "",
|
|
464
|
+
event.agent.prompt ? `Prompt:\n${event.agent.prompt}` : ""
|
|
465
|
+
].filter(Boolean).join("\n");
|
|
466
|
+
}
|
|
467
|
+
function formatRawTranscriptEvent(event) {
|
|
468
|
+
if (event.kind === "tool_use") {
|
|
469
|
+
return `● ${event.toolUse.name}(${formatUnknown(event.toolUse.input)})`;
|
|
470
|
+
}
|
|
471
|
+
const errorPrefix = event.toolResult.isError ? "[error] " : "";
|
|
472
|
+
return `⎿ ${errorPrefix}${formatUnknown(event.toolResult.content)}`;
|
|
473
|
+
}
|
|
474
|
+
function formatUnknown(value) {
|
|
475
|
+
if (typeof value === "string") {
|
|
476
|
+
return value;
|
|
477
|
+
}
|
|
478
|
+
if (value === undefined) {
|
|
479
|
+
return "";
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
return JSON.stringify(value);
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
return String(value);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function normalizeSettings(input) {
|
|
489
|
+
const { apiKey: _apiKey, ...settings } = input;
|
|
490
|
+
return {
|
|
491
|
+
...DEFAULT_SETTINGS,
|
|
492
|
+
...settings,
|
|
493
|
+
version: 1,
|
|
494
|
+
providerType: "openai-compatible",
|
|
495
|
+
workingLanguage: "en",
|
|
496
|
+
requestTimeoutMs: clampNumber(input.requestTimeoutMs, 3000, 120000, DEFAULT_SETTINGS.requestTimeoutMs),
|
|
497
|
+
temperature: clampNumber(input.temperature, 0, 1, DEFAULT_SETTINGS.temperature),
|
|
498
|
+
prompts: normalizePromptMap(input.prompts)
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function exposeSettings(settings, secrets) {
|
|
502
|
+
return {
|
|
503
|
+
...settings,
|
|
504
|
+
apiKey: secrets.apiKey ?? ""
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
function normalizePromptMap(input) {
|
|
508
|
+
if (!input || typeof input !== "object") {
|
|
509
|
+
return undefined;
|
|
510
|
+
}
|
|
511
|
+
const prompts = {};
|
|
512
|
+
for (const [key, value] of Object.entries(input)) {
|
|
513
|
+
const normalizedKey = normalizePromptKey(key);
|
|
514
|
+
if (normalizedKey && typeof value === "string" && value.trim()) {
|
|
515
|
+
prompts[normalizedKey] = value;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return Object.keys(prompts).length > 0 ? prompts : undefined;
|
|
519
|
+
}
|
|
520
|
+
function normalizePromptKey(key) {
|
|
521
|
+
if (TRANSLATION_PROMPT_KEYS.includes(key)) {
|
|
522
|
+
return key;
|
|
523
|
+
}
|
|
524
|
+
if (key === "user-input-to-english") {
|
|
525
|
+
return "zh-to-en";
|
|
526
|
+
}
|
|
527
|
+
if (key === "user-input-to-english-with-context") {
|
|
528
|
+
return "zh-to-en-with-context";
|
|
529
|
+
}
|
|
530
|
+
if (key === "cc-output-to-user") {
|
|
531
|
+
return "en-to-zh";
|
|
532
|
+
}
|
|
533
|
+
return undefined;
|
|
534
|
+
}
|
|
535
|
+
function clampNumber(value, min, max, fallback) {
|
|
536
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
537
|
+
return fallback;
|
|
538
|
+
}
|
|
539
|
+
return Math.min(max, Math.max(min, value));
|
|
540
|
+
}
|
|
541
|
+
function normalizeTranslationError(error) {
|
|
542
|
+
if (error instanceof TranslationProviderError) {
|
|
543
|
+
return `${error.code}: ${error.message}`;
|
|
544
|
+
}
|
|
545
|
+
return error instanceof Error ? error.message : "Translation failed.";
|
|
546
|
+
}
|
|
@@ -74,3 +74,35 @@ TBD
|
|
|
74
74
|
TBD
|
|
75
75
|
`;
|
|
76
76
|
}
|
|
77
|
+
export function renderDocsSyncReportTemplate(taskSlug) {
|
|
78
|
+
return `# Docs Sync Report: ${taskSlug}
|
|
79
|
+
|
|
80
|
+
## Summary
|
|
81
|
+
|
|
82
|
+
TBD
|
|
83
|
+
|
|
84
|
+
## Architecture Drift Check
|
|
85
|
+
|
|
86
|
+
TBD
|
|
87
|
+
|
|
88
|
+
## Docs Updated
|
|
89
|
+
|
|
90
|
+
TBD
|
|
91
|
+
|
|
92
|
+
## Docs Reviewed And Left Unchanged
|
|
93
|
+
|
|
94
|
+
TBD
|
|
95
|
+
|
|
96
|
+
## Public Contract / Module Boundary Notes
|
|
97
|
+
|
|
98
|
+
TBD
|
|
99
|
+
|
|
100
|
+
## Remaining Documentation Risks
|
|
101
|
+
|
|
102
|
+
TBD
|
|
103
|
+
|
|
104
|
+
## Decision
|
|
105
|
+
|
|
106
|
+
TBD
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function renderArchitectHarnessRules() {
|
|
2
|
+
return `## VCM Architect Rules
|
|
3
|
+
|
|
4
|
+
- Own architecture planning, module boundaries, file responsibilities, public contracts, test contracts, risk, phases, and stop conditions.
|
|
5
|
+
- Write architecture-plan.md under the current task handoff directory before coder work starts.
|
|
6
|
+
- Do not implement production code.
|
|
7
|
+
- After reviewer completes, perform docs sync and architecture drift checks when requested by project-manager.
|
|
8
|
+
- Update stale architecture/module/testing/security/dependency docs when the final code made them stale.
|
|
9
|
+
- Write docs-sync-report.md with docs changed, docs intentionally left unchanged, remaining documentation risks, and decision.
|
|
10
|
+
- Stop and reply to project-manager if implementation drift changes architecture, public contracts, dependency direction, schema, auth, permission, payment, or design assumptions.
|
|
11
|
+
`;
|
|
12
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function renderRootClaudeHarnessRules() {
|
|
2
|
+
return `## VCM Shared Rules
|
|
3
|
+
|
|
4
|
+
- This repository uses VibeCodingMaster for multi-session Claude Code work.
|
|
5
|
+
- User-facing work starts with the project-manager role.
|
|
6
|
+
- Canonical task handoffs live under .ai/handoffs/<task-slug>/.
|
|
7
|
+
- Use only the current task's handoff directory for task-specific artifacts.
|
|
8
|
+
- Do not create or write .ai/handoffs/<other-task>/ for the current task.
|
|
9
|
+
- Use vcmctl for role-to-role messaging instead of asking the user to copy prompts.
|
|
10
|
+
- Non-PM roles only reply to project-manager; they do not message other roles directly.
|
|
11
|
+
- High-risk decisions involving schema, auth, permissions, payment, billing, security, data deletion, or unclear user intent must stop for project-manager/user approval.
|
|
12
|
+
- Required workflow gates: architect plan -> coder implementation/validation -> reviewer review -> architect docs sync -> project-manager final acceptance/commit/PR.
|
|
13
|
+
`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function renderCoderHarnessRules() {
|
|
2
|
+
return `## VCM Coder Rules
|
|
3
|
+
|
|
4
|
+
- Implement only the approved task scope and architecture plan.
|
|
5
|
+
- Read the task spec, architecture-plan.md, relevant module docs, and role command before editing.
|
|
6
|
+
- Add or update direct unit, contract, or regression tests for changed behavior.
|
|
7
|
+
- Maintain implementation-log.md and validation-log.md under the current task handoff directory.
|
|
8
|
+
- Do not change module boundaries, public contracts, dependency direction, or test strategy without project-manager/architect replan.
|
|
9
|
+
- Stop and reply to project-manager when blocked, unclear, or when the plan no longer matches reality.
|
|
10
|
+
`;
|
|
11
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function renderProjectManagerHarnessRules() {
|
|
2
|
+
return `## VCM Project Manager Rules
|
|
3
|
+
|
|
4
|
+
- You are the user-facing orchestration hub for the VCM task.
|
|
5
|
+
- Clarify the user's request, classify task risk, and choose the role route.
|
|
6
|
+
- Use vcmctl send to assign work to architect, coder, or reviewer.
|
|
7
|
+
- Send role work as durable instructions with artifact refs when possible.
|
|
8
|
+
- Track the workflow gates: architecture plan, implementation/validation, review, docs sync, final acceptance.
|
|
9
|
+
- Request architect post-review docs sync after reviewer completes.
|
|
10
|
+
- Prepare final acceptance, commit, and PR only after reviewer and docs-sync gates pass or an explicit exception is approved.
|
|
11
|
+
- Do not implement non-trivial production code directly.
|
|
12
|
+
- Stop and ask the user for high-risk decisions or unclear requirements.
|
|
13
|
+
`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function renderReviewerHarnessRules() {
|
|
2
|
+
return `## VCM Reviewer Rules
|
|
3
|
+
|
|
4
|
+
- Own independent acceptance review and final test adequacy.
|
|
5
|
+
- Read task spec, architecture-plan.md, implementation-log.md, validation-log.md, and git diff.
|
|
6
|
+
- Verify scope, role compliance, architecture compliance, public contract compliance, validation evidence, docs gaps, and risk.
|
|
7
|
+
- Add or strengthen missing tests only when the fix is small, local, low-risk, and review-scoped.
|
|
8
|
+
- Write review-report.md under the current task handoff directory.
|
|
9
|
+
- Escalate larger implementation issues to project-manager for coder follow-up.
|
|
10
|
+
- Escalate architecture, public contract, design, or documentation drift issues to project-manager for architect follow-up.
|
|
11
|
+
- Do not take over broad implementation and do not weaken tests to pass validation.
|
|
12
|
+
`;
|
|
13
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { WebSocketServer } from "ws";
|
|
2
|
+
import { toVcmError } from "../errors.js";
|
|
3
|
+
export function registerTranslationWs(app, deps) {
|
|
4
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
5
|
+
app.server.on("upgrade", (request, socket, head) => {
|
|
6
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
7
|
+
const match = /^\/ws\/translation\/([^/]+)$/.exec(url.pathname);
|
|
8
|
+
if (!match) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
12
|
+
bindTranslationSocket(ws, decodeURIComponent(match[1] ?? ""), deps.translationService);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function bindTranslationSocket(ws, sessionId, translationService) {
|
|
17
|
+
let unsubscribe = () => { };
|
|
18
|
+
try {
|
|
19
|
+
unsubscribe = translationService.subscribeToSession(sessionId, (message) => send(ws, message));
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
const vcmError = toVcmError(error);
|
|
23
|
+
send(ws, { type: "translation-error", message: vcmError.message });
|
|
24
|
+
ws.close();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
ws.on("close", () => {
|
|
28
|
+
unsubscribe();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function send(ws, message) {
|
|
32
|
+
if (ws.readyState === ws.OPEN) {
|
|
33
|
+
ws.send(JSON.stringify(message));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|