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 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`. VCM records the wait and cancels any pending role dispatch in
196
- one state update. Stop Hook then ends the PM turn without route scanning. Only
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
- const events = await readTranscriptTextEvents(transcriptPath);
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;
@@ -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, { workflowControlService });
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
- if (options.allowBlock && lastAssistantMessage && asksUserQuestion(lastAssistantMessage)) {
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: "You asked the user a question without registering the wait. Use vcm-ask-user with the exact question, then ask that question and stop. Do not advance or route the workflow."
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
- return withGateStateLock(context, () => requestReviewGateLocked(context, gate, options));
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
+ }