vibe-coding-master 0.8.3 → 0.8.5
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 +11 -3
- package/dist/backend/api/task-routes.js +6 -0
- package/dist/backend/gateway/gateway-service.js +18 -3
- package/dist/backend/server.js +8 -2
- package/dist/backend/services/artifact-service.js +14 -0
- package/dist/backend/services/claude-hook-service.js +21 -13
- package/dist/backend/services/gate-review-service.js +4 -1
- package/dist/backend/services/pm-user-question.js +62 -0
- package/dist/backend/services/translation-service.js +49 -1
- package/dist/backend/services/user-question-reply.js +10 -0
- package/dist/backend/services/workflow-control-service.js +120 -34
- package/dist/backend/templates/harness/project-manager-agent.js +9 -3
- package/dist/backend/templates/harness/vcm-ask-user-skill.js +2 -2
- package/dist/backend/templates/harness/vcm-workflow-review-skill.js +7 -1
- package/dist-frontend/assets/index-BIFZPhDN.css +32 -0
- package/dist-frontend/assets/{index-C21rIHsU.js → index-c2YwYw6n.js} +33 -33
- package/dist-frontend/index.html +2 -2
- package/package.json +4 -1
- package/scripts/harness-tools/run-long-check +90 -43
- package/dist-frontend/assets/index-Dc0_KSf4.css +0 -32
package/README.md
CHANGED
|
@@ -192,10 +192,18 @@ artifacts, Gate Review state, Round/Turn state, and the role rules remain
|
|
|
192
192
|
authoritative.
|
|
193
193
|
|
|
194
194
|
Whenever Project Manager asks the user a question, it first uses
|
|
195
|
-
`vcm-ask-user
|
|
196
|
-
|
|
195
|
+
`vcm-ask-user` with the complete question and necessary context. VCM records the
|
|
196
|
+
wait and cancels any pending role dispatch in one state update. Stop Hook then
|
|
197
|
+
ends the PM turn without route scanning and records the question as the official
|
|
198
|
+
reply. The existing Pause Alert displays it even when translation is off; the
|
|
199
|
+
translation panel and Gateway reuse that same reply instead of PM's separate
|
|
200
|
+
closing text. While waiting, new dispatches, Gate requests/retries, and workflow
|
|
201
|
+
completion are blocked; already-running roles may still return results. Only
|
|
197
202
|
a new direct user message clears the wait, and the next role dispatch requires
|
|
198
|
-
a fresh Workflow Review.
|
|
203
|
+
a fresh Workflow Review. PM treats text emitted while a Round continues as
|
|
204
|
+
intermediate output, not as delivered user communication. When the Round pauses
|
|
205
|
+
or completes, its Round Final Reply contains the complete user-facing result or
|
|
206
|
+
registered question without relying on earlier PM output.
|
|
199
207
|
|
|
200
208
|
Typical flow:
|
|
201
209
|
|
|
@@ -65,6 +65,12 @@ export function registerTaskRoutes(app, deps) {
|
|
|
65
65
|
taskSlug
|
|
66
66
|
}) ?? Promise.resolve(degradedWorkflowState(taskSlug))
|
|
67
67
|
]);
|
|
68
|
+
if (roundState.flowPause?.paused && deps.workflowControlService) {
|
|
69
|
+
const workflow = await deps.workflowControlService.getState(context);
|
|
70
|
+
const reply = workflow.userQuestionReplies?.find((item) => item.requestedAt === workflow.awaitingUser?.requestedAt);
|
|
71
|
+
if (reply)
|
|
72
|
+
roundState.flowPause = { ...roundState.flowPause, message: reply.question };
|
|
73
|
+
}
|
|
68
74
|
const displayedRoundState = await delayFlowPauseForTranslation(deps, {
|
|
69
75
|
repoRoot: project.repoRoot,
|
|
70
76
|
taskRepoRoot,
|
|
@@ -2,6 +2,7 @@ import { getRoleDefinition, isVcmRoleName, VCM_ROLE_NAMES } from "../../shared/c
|
|
|
2
2
|
import { VcmError } from "../errors.js";
|
|
3
3
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
4
4
|
import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
|
|
5
|
+
import { findUserQuestionReply } from "../services/user-question-reply.js";
|
|
5
6
|
import { resolveExistingClaudeTranscriptPath } from "../services/claude-transcript-service.js";
|
|
6
7
|
import { isFinalTurnTextEvent, readTranscriptTextEvents } from "../services/claude-transcript-reply.js";
|
|
7
8
|
import { parseGatewayCommand } from "./gateway-command-parser.js";
|
|
@@ -1162,11 +1163,16 @@ export function createGatewayService(deps) {
|
|
|
1162
1163
|
|| settings.targetRole !== role) {
|
|
1163
1164
|
return;
|
|
1164
1165
|
}
|
|
1166
|
+
const questionReply = role === "project-manager" && deps.workflowControlService
|
|
1167
|
+
? await readRegisteredQuestionReply(input)
|
|
1168
|
+
: undefined;
|
|
1165
1169
|
const transcriptPath = resolveExistingClaudeTranscriptPath(input.session);
|
|
1166
|
-
if (!transcriptPath)
|
|
1170
|
+
if (!questionReply && !transcriptPath)
|
|
1167
1171
|
return;
|
|
1168
|
-
|
|
1169
|
-
|
|
1172
|
+
const events = questionReply ? [{
|
|
1173
|
+
id: questionReply.id, timestamp: questionReply.completedAt,
|
|
1174
|
+
text: questionReply.question, stopReason: "end_turn"
|
|
1175
|
+
}] : await readTranscriptTextEvents(transcriptPath);
|
|
1170
1176
|
const round = await waitForGatewayRoundFinal(input.repoRoot, input.taskSlug, role);
|
|
1171
1177
|
if (!round) {
|
|
1172
1178
|
return;
|
|
@@ -1285,6 +1291,15 @@ export function createGatewayService(deps) {
|
|
|
1285
1291
|
};
|
|
1286
1292
|
}
|
|
1287
1293
|
};
|
|
1294
|
+
async function readRegisteredQuestionReply(input) {
|
|
1295
|
+
const task = await deps.taskService.loadTask(input.repoRoot, input.taskSlug);
|
|
1296
|
+
const config = await deps.projectService.loadConfig(input.repoRoot);
|
|
1297
|
+
const state = await deps.workflowControlService.getState({
|
|
1298
|
+
taskRepoRoot: getTaskRuntimeRepoRoot(task), taskSlug: input.taskSlug,
|
|
1299
|
+
stateRoot: config.stateRoot, handoffDir: task.handoffDir
|
|
1300
|
+
});
|
|
1301
|
+
return findUserQuestionReply(state, input.session.id, input.session.lastTurnEndedAt ?? input.session.updatedAt);
|
|
1302
|
+
}
|
|
1288
1303
|
function getLatestRoleReply(settings) {
|
|
1289
1304
|
if (!settings.currentProjectId || !settings.currentTaskSlug) {
|
|
1290
1305
|
return undefined;
|
package/dist/backend/server.js
CHANGED
|
@@ -136,7 +136,8 @@ export async function createServer(deps, options = {}) {
|
|
|
136
136
|
roundService: deps.roundService,
|
|
137
137
|
taskWorkflowService: deps.taskWorkflowService,
|
|
138
138
|
architectRestartService: deps.architectRestartService,
|
|
139
|
-
translationService: deps.translationService
|
|
139
|
+
translationService: deps.translationService,
|
|
140
|
+
workflowControlService: deps.workflowControlService
|
|
140
141
|
});
|
|
141
142
|
registerSessionRoutes(app, {
|
|
142
143
|
projectService: deps.projectService,
|
|
@@ -243,7 +244,10 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
243
244
|
const runtime = createNodePtyTerminalRuntime({ fs });
|
|
244
245
|
const registry = createSessionRegistry();
|
|
245
246
|
const workflowControlService = createWorkflowControlService({ fs });
|
|
246
|
-
const artifactService = createArtifactService(fs, {
|
|
247
|
+
const artifactService = createArtifactService(fs, {
|
|
248
|
+
workflowControlService,
|
|
249
|
+
isRoleMemoryTurn: (taskRepoRoot, role) => autoMemoryService.isRoleMemoryTurn(taskRepoRoot, role)
|
|
250
|
+
});
|
|
247
251
|
const projectService = createProjectService({ fs, git, appSettings });
|
|
248
252
|
const taskService = createTaskService({ fs, git, artifactService, projectService });
|
|
249
253
|
const taskWorkflowService = createTaskWorkflowService({ fs });
|
|
@@ -350,6 +354,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
350
354
|
});
|
|
351
355
|
const transcripts = createClaudeTranscriptService();
|
|
352
356
|
const translationService = createTranslationService({
|
|
357
|
+
workflowControlService,
|
|
353
358
|
runtime,
|
|
354
359
|
sessionRegistry: registry,
|
|
355
360
|
transcripts,
|
|
@@ -385,6 +390,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
385
390
|
roleContextRestartService
|
|
386
391
|
});
|
|
387
392
|
const gatewayService = createGatewayService({
|
|
393
|
+
workflowControlService,
|
|
388
394
|
fs,
|
|
389
395
|
settings: gatewaySettings,
|
|
390
396
|
audit: gatewayAudit,
|
|
@@ -219,6 +219,20 @@ export function createArtifactService(fs, deps = {}) {
|
|
|
219
219
|
if (validation.errors.length > 0) {
|
|
220
220
|
throw artifactRejected(input.kind, validation.errors);
|
|
221
221
|
}
|
|
222
|
+
if ((input.kind === "docs-update-report" || input.kind === "docs-sync-report")
|
|
223
|
+
&& deps.workflowControlService) {
|
|
224
|
+
// Memory-owned documentation work is independent of PM's active dispatch.
|
|
225
|
+
const memoryUpdate = input.kind === "docs-update-report"
|
|
226
|
+
&& await deps.isRoleMemoryTurn?.(input.repoRoot, input.role);
|
|
227
|
+
if (!memoryUpdate) {
|
|
228
|
+
await deps.workflowControlService.assertDocsArtifactAllowed({
|
|
229
|
+
taskRepoRoot: input.repoRoot,
|
|
230
|
+
stateRoot: input.stateRoot ?? ".ai/vcm",
|
|
231
|
+
handoffDir: input.handoffDir,
|
|
232
|
+
taskSlug: input.taskSlug
|
|
233
|
+
}, input.kind, input.role);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
222
236
|
if (input.kind === "workflow-progress") {
|
|
223
237
|
if (input.mode !== "final") {
|
|
224
238
|
throw artifactRejected(input.kind, ["Workflow Progress must be submitted in final mode."]);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isHarnessEngineerToolRoleName, isReviewerRoleName, isTranslatorToolRoleName, isVcmRoleName } from "../../shared/constants.js";
|
|
2
2
|
import { VcmError } from "../errors.js";
|
|
3
3
|
import { readTranscriptTurnEvidence } from "./claude-transcript-reply.js";
|
|
4
|
+
import { findPmUserQuestion } from "./pm-user-question.js";
|
|
4
5
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
5
6
|
import { matchesRoleHookSession } from "./session-service.js";
|
|
6
7
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
@@ -338,7 +339,10 @@ export function createClaudeHookService(deps) {
|
|
|
338
339
|
}
|
|
339
340
|
else {
|
|
340
341
|
const lastAssistantMessage = stringOrUndefined(input.event.last_assistant_message);
|
|
341
|
-
|
|
342
|
+
const question = options.allowBlock && lastAssistantMessage
|
|
343
|
+
? findPmUserQuestion(lastAssistantMessage)
|
|
344
|
+
: undefined;
|
|
345
|
+
if (question) {
|
|
342
346
|
return {
|
|
343
347
|
ok: true,
|
|
344
348
|
eventName,
|
|
@@ -348,7 +352,7 @@ export function createClaudeHookService(deps) {
|
|
|
348
352
|
dispatchedCount: 0,
|
|
349
353
|
stopDecision: {
|
|
350
354
|
behavior: "block",
|
|
351
|
-
reason:
|
|
355
|
+
reason: `Possible unregistered user question: ${JSON.stringify(question.slice(0, 600))}. If you are asking the user, use vcm-ask-user with the complete question and necessary context, then end the turn without advancing or routing the workflow. VCM delivers the registered question. If this only reports or quotes another question, clarify that context and end normally; do not invent a user question or register a false wait.`
|
|
352
356
|
}
|
|
353
357
|
};
|
|
354
358
|
}
|
|
@@ -503,6 +507,21 @@ export function createClaudeHookService(deps) {
|
|
|
503
507
|
if (!session) {
|
|
504
508
|
return completedHookResult(input, eventName);
|
|
505
509
|
}
|
|
510
|
+
if (eventName === "Stop" && input.role === "project-manager") {
|
|
511
|
+
const reply = await deps.workflowControlService?.completeUserQuestion?.(createWorkflowControlContext(context), {
|
|
512
|
+
sessionId: session.id,
|
|
513
|
+
startedAt: session.lastTurnStartedAt ?? session.updatedAt,
|
|
514
|
+
endedAt: session.lastTurnEndedAt ?? session.updatedAt
|
|
515
|
+
});
|
|
516
|
+
if (reply) {
|
|
517
|
+
await deps.translationService.recordUserQuestionReply?.({
|
|
518
|
+
repoRoot: context.project.repoRoot,
|
|
519
|
+
taskRepoRoot: context.taskRepoRoot,
|
|
520
|
+
taskSlug: context.taskSlug,
|
|
521
|
+
reply
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
506
525
|
const boundToTask = await isHookSessionBoundToTask(context, input.role);
|
|
507
526
|
if (boundToTask) {
|
|
508
527
|
await deps.roundService.recordClaudeHookEvent({
|
|
@@ -1003,14 +1022,3 @@ function isDirectUserPrompt(prompt) {
|
|
|
1003
1022
|
const normalized = prompt.trim();
|
|
1004
1023
|
return normalized.length > 0 && !/^\[VCM(?:\s|\])/i.test(normalized);
|
|
1005
1024
|
}
|
|
1006
|
-
function asksUserQuestion(message) {
|
|
1007
|
-
const withoutCodeBlocks = message.replace(/```[\s\S]*?```/g, "");
|
|
1008
|
-
const lines = withoutCodeBlocks
|
|
1009
|
-
.split(/\r?\n/)
|
|
1010
|
-
.map((line) => line.trim())
|
|
1011
|
-
.filter(Boolean);
|
|
1012
|
-
return lines.some((line) => /[??]/.test(line)
|
|
1013
|
-
|| /^(?:please\s+(?:answer|choose|confirm|decide|provide|select|tell)|(?:can|could|do|does|is|are|should|will|would)\s+you\b)/i.test(line)
|
|
1014
|
-
|| /\b(?:please\s+(?:reply|respond|choose|confirm|decide|provide|select|tell|give)|i\s+need\s+your\s+(?:decision|choice|confirmation|answer)|let\s+me\s+know|your\s+(?:decision|choice|confirmation|answer)\s+is\s+required|waiting\s+for\s+your)\b/i.test(line)
|
|
1015
|
-
|| /(?:请(?:回答|回复|选择|确认|决定|提供|告知|给出|说明)|你(?:是否|能否|要不要|可否)|是否需要你|需要你(?:选择|确认|决定|提供|告知)|需要您的(?:选择|确认|决定|回复)|等待您的(?:选择|确认|决定|回复)|告诉我|告知我)/.test(line));
|
|
1016
|
-
}
|
|
@@ -72,7 +72,10 @@ export function createGateReviewService(deps) {
|
|
|
72
72
|
}
|
|
73
73
|
async function requestReviewGateInternal(repoRoot, taskSlug, gate, options = {}) {
|
|
74
74
|
const context = await getContext(repoRoot, taskSlug);
|
|
75
|
-
|
|
75
|
+
const run = () => withGateStateLock(context, () => requestReviewGateLocked(context, gate, options));
|
|
76
|
+
return deps.workflowControlService?.runWhileNotAwaitingUser
|
|
77
|
+
? deps.workflowControlService.runWhileNotAwaitingUser(context, run)
|
|
78
|
+
: run();
|
|
76
79
|
}
|
|
77
80
|
async function requestReviewGateLocked(context, gate, options) {
|
|
78
81
|
let index = await loadIndex(deps.fs, context, now());
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import remarkGfm from "remark-gfm";
|
|
2
|
+
import remarkParse from "remark-parse";
|
|
3
|
+
import { unified } from "unified";
|
|
4
|
+
const markdown = unified().use(remarkParse).use(remarkGfm).freeze();
|
|
5
|
+
// Match direct requests, not punctuation or requests mentioned inside a report.
|
|
6
|
+
const DIRECT_REQUESTS = [
|
|
7
|
+
/^(?:can|could|do|did|are|have|should|will|would)\s+you\b/i,
|
|
8
|
+
/^(?:should|shall|may|can|could)\s+(?:i|we)\b/i,
|
|
9
|
+
/^(?:which|what|how|where|when|who)\b[^.!?]*\b(?:you|your|(?:should|shall|can|may)\s+(?:i|we))\b/i,
|
|
10
|
+
/^(?:which|what)\s+(?:option|approach|behavior|behaviour|choice)\b[^.!?]*\bshould\s+be\b/i,
|
|
11
|
+
/^(?:please\s+)?(?:answer|reply|respond|choose|confirm|decide|provide|select|tell\s+me|let\s+me\s+know|give\s+(?:me|your))\b/i,
|
|
12
|
+
/^i\s+need\s+your\s+(?:decision|choice|confirmation|answer|approval)\b/i,
|
|
13
|
+
/^your\s+(?:decision|choice|confirmation|answer|approval)\s+is\s+required\b/i,
|
|
14
|
+
/^(?:(?:i\s+am|i'm|we\s+are|we're)\s+)?waiting\s+for\s+your\s+(?:decision|choice|confirmation|answer|reply|approval)\b/i,
|
|
15
|
+
/^(?:请(?:您|你)?(?:回答|回复|选择|确认|决定|提供|告知|给出|说明)|[你您](?:是否|能否|要不要|可否|希望|想要)|是否需要[你您]|需要[你您](?:选择|确认|决定|提供|告知)|(?:需要|等待)[你您]的(?:选择|确认|决定|回复)|告诉我|告知我)/
|
|
16
|
+
];
|
|
17
|
+
export function findPmUserQuestion(message) {
|
|
18
|
+
return findQuestion(markdown.parse(message));
|
|
19
|
+
}
|
|
20
|
+
function findQuestion(node) {
|
|
21
|
+
// Quoted evidence and code are not PM addressing the user. Headings and table
|
|
22
|
+
// cells remain eligible: a real request does not stop being one when formatted.
|
|
23
|
+
if (["blockquote", "code", "html", "definition", "footnoteDefinition", "delete"].includes(node.type)) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
if (node.type === "paragraph" || node.type === "heading" || node.type === "tableCell") {
|
|
27
|
+
const prose = removeQuotedText(proseText(node)).replace(/\s+/g, " ");
|
|
28
|
+
for (const sentence of prose.split(/(?<=[.!?。!?;])\s*/)) {
|
|
29
|
+
const request = sentence.trim().replace(/^(?:(?:question(?:\s+for\s+you)?|your\s+decision|问题|请问)\s*[::]\s*|before\s+(?:i|we)\s+(?:continue|proceed),?\s+)/i, "");
|
|
30
|
+
if (DIRECT_REQUESTS.some((pattern) => pattern.test(request))) {
|
|
31
|
+
return sentence.trim();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
if ("children" in node) {
|
|
37
|
+
for (const child of node.children) {
|
|
38
|
+
const question = findQuestion(child);
|
|
39
|
+
if (question)
|
|
40
|
+
return question;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
function proseText(node) {
|
|
46
|
+
if (node.type === "text")
|
|
47
|
+
return node.value;
|
|
48
|
+
if (node.type === "break")
|
|
49
|
+
return "\n";
|
|
50
|
+
if (["inlineCode", "html", "image", "imageReference", "delete", "footnoteReference"].includes(node.type))
|
|
51
|
+
return " ";
|
|
52
|
+
if (node.type === "link") {
|
|
53
|
+
const label = node.children.map(proseText).join("");
|
|
54
|
+
return label === node.url || /^(?:https?:\/\/|www\.)/i.test(label) ? " " : label;
|
|
55
|
+
}
|
|
56
|
+
if ("children" in node)
|
|
57
|
+
return node.children.map(proseText).join("");
|
|
58
|
+
return " ";
|
|
59
|
+
}
|
|
60
|
+
function removeQuotedText(text) {
|
|
61
|
+
return text.replace(/"[^"]*"|“[^”]*”|‘[^’]*’|「[^」]*」|『[^』]*』|(?<!\w)'[^']+'(?!\w)/g, " ");
|
|
62
|
+
}
|
|
@@ -5,6 +5,7 @@ import { VcmError } from "../errors.js";
|
|
|
5
5
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
6
6
|
import { readLatestRoleTurnReply } from "./claude-transcript-reply.js";
|
|
7
7
|
import { createTranslationQueueRegistry } from "./translation-queue.js";
|
|
8
|
+
import { findUserQuestionReply, isPendingUserQuestion } from "./user-question-reply.js";
|
|
8
9
|
const TRANSLATION_SOURCE_LANGUAGE = "auto";
|
|
9
10
|
const TRANSLATION_INPUT_MODE = "review-before-send";
|
|
10
11
|
const TRANSLATION_CONTEXT_ENABLED = false;
|
|
@@ -25,6 +26,7 @@ export function createTranslationService(deps) {
|
|
|
25
26
|
async function loadConfig() {
|
|
26
27
|
const preferences = await deps.appSettings.getPreferences();
|
|
27
28
|
return {
|
|
29
|
+
enabled: preferences.translationEnabled,
|
|
28
30
|
sourceLanguage: TRANSLATION_SOURCE_LANGUAGE,
|
|
29
31
|
targetLanguage: preferences.translationTargetLanguage,
|
|
30
32
|
inputMode: TRANSLATION_INPUT_MODE,
|
|
@@ -239,6 +241,22 @@ export function createTranslationService(deps) {
|
|
|
239
241
|
return;
|
|
240
242
|
}
|
|
241
243
|
const config = await loadConfig();
|
|
244
|
+
if (state.role === "project-manager" && event.kind === "text" && event.stopReason === "end_turn"
|
|
245
|
+
&& !event.isSidechain && state.repoRoot && state.taskSlug && deps.workflowControlService && deps.projectService) {
|
|
246
|
+
const projectConfig = await deps.projectService.loadConfig(state.baseRepoRoot ?? state.repoRoot);
|
|
247
|
+
const workflow = await deps.workflowControlService.getState({
|
|
248
|
+
taskRepoRoot: state.repoRoot, taskSlug: state.taskSlug,
|
|
249
|
+
stateRoot: projectConfig.stateRoot, handoffDir: projectConfig.handoffRoot
|
|
250
|
+
});
|
|
251
|
+
const reply = findUserQuestionReply(workflow, sessionId, event.timestamp);
|
|
252
|
+
if (reply) {
|
|
253
|
+
publishUserQuestionReply(reply, config);
|
|
254
|
+
state.seenTranscriptIds.add(event.id);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (isPendingUserQuestion(workflow, event.timestamp))
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
242
260
|
let displayed = false;
|
|
243
261
|
if (event.kind === "text") {
|
|
244
262
|
const shouldTranslate = shouldTranslateTextTranscriptEvent(state, event, config);
|
|
@@ -281,6 +299,20 @@ export function createTranslationService(deps) {
|
|
|
281
299
|
}
|
|
282
300
|
return true;
|
|
283
301
|
}
|
|
302
|
+
function publishUserQuestionReply(reply, config) {
|
|
303
|
+
const state = getState(reply.sessionId);
|
|
304
|
+
if (state.seenTranscriptIds.has(reply.id))
|
|
305
|
+
return;
|
|
306
|
+
const metadata = { transcriptStopReason: "end_turn", transcriptTimestamp: reply.completedAt };
|
|
307
|
+
// Claim before scheduling translation so Stop and transcript replay share one entry/job.
|
|
308
|
+
state.seenTranscriptIds.add(reply.id);
|
|
309
|
+
if (config.enabled === false || config.outputMode === "round-final") {
|
|
310
|
+
pushPreservedProseEntry(reply.sessionId, reply.id, reply.question, config, metadata);
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
processClaudeOutputText(reply.sessionId, reply.question, config, reply.id, { flushImmediately: true, metadata });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
284
316
|
function processClaudeOutputText(sessionId, rawText, config, entryId, options = {}) {
|
|
285
317
|
return startClaudeOutputTranslation(sessionId, rawText, config, {
|
|
286
318
|
entryId,
|
|
@@ -647,6 +679,12 @@ export function createTranslationService(deps) {
|
|
|
647
679
|
return state;
|
|
648
680
|
}
|
|
649
681
|
return {
|
|
682
|
+
async recordUserQuestionReply(input) {
|
|
683
|
+
await prepareCache({ repoRoot: input.taskRepoRoot, baseRepoRoot: input.repoRoot,
|
|
684
|
+
taskSlug: input.taskSlug, role: "project-manager", sessionId: input.reply.sessionId });
|
|
685
|
+
publishUserQuestionReply(input.reply, await loadConfig());
|
|
686
|
+
await persistEvents(getState(input.reply.sessionId));
|
|
687
|
+
},
|
|
650
688
|
async startSession(input) {
|
|
651
689
|
const roleSession = await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, input.role);
|
|
652
690
|
if (!roleSession || roleSession.status !== "running") {
|
|
@@ -720,6 +758,16 @@ export function createTranslationService(deps) {
|
|
|
720
758
|
status: state.status
|
|
721
759
|
});
|
|
722
760
|
}
|
|
761
|
+
if (deps.workflowControlService && deps.projectService) {
|
|
762
|
+
const projectConfig = await deps.projectService.loadConfig(input.repoRoot);
|
|
763
|
+
const workflow = await deps.workflowControlService.getState({ ...input,
|
|
764
|
+
stateRoot: projectConfig.stateRoot, handoffDir: projectConfig.handoffRoot });
|
|
765
|
+
for (const reply of workflow.userQuestionReplies ?? []) {
|
|
766
|
+
if (feedSessions.some((session) => session.role === "project-manager" && session.sessionId === reply.sessionId)) {
|
|
767
|
+
publishUserQuestionReply(reply, config);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
723
771
|
if (config.outputMode === "round-final") {
|
|
724
772
|
await translateRoundFinalReplyIfReady(input, config);
|
|
725
773
|
}
|
|
@@ -1173,7 +1221,7 @@ export function createTranslationService(deps) {
|
|
|
1173
1221
|
}
|
|
1174
1222
|
};
|
|
1175
1223
|
async function translateRoundFinalReplyIfReady(input, config) {
|
|
1176
|
-
if (!deps.roundService || !deps.projectService) {
|
|
1224
|
+
if (config.enabled === false || !deps.roundService || !deps.projectService) {
|
|
1177
1225
|
return;
|
|
1178
1226
|
}
|
|
1179
1227
|
const projectConfig = await deps.projectService.loadConfig(input.repoRoot);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Use the recorded turn window, not the current wait flag: replies must survive
|
|
2
|
+
// the user's answer and transcript replay without becoming a second message.
|
|
3
|
+
export function findUserQuestionReply(state, sessionId, timestamp) {
|
|
4
|
+
return state.userQuestionReplies?.slice().reverse().find((reply) => reply.sessionId === sessionId
|
|
5
|
+
&& timestamp >= reply.turnStartedAt && timestamp <= reply.completedAt);
|
|
6
|
+
}
|
|
7
|
+
export function isPendingUserQuestion(state, timestamp) {
|
|
8
|
+
return Boolean(state.awaitingUser && timestamp >= state.awaitingUser.requestedAt
|
|
9
|
+
&& !state.userQuestionReplies?.some((reply) => reply.requestedAt === state.awaitingUser?.requestedAt));
|
|
10
|
+
}
|