vibe-coding-master 0.8.2 → 0.8.4
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 +7 -9
- package/dist/backend/api/task-routes.js +2 -31
- package/dist/backend/cli/install-vcm-harness.js +0 -7
- package/dist/backend/server.js +4 -10
- package/dist/backend/services/artifact-service.js +14 -0
- package/dist/backend/services/claude-hook-service.js +6 -102
- package/dist/backend/services/harness-service.js +0 -7
- package/dist/backend/services/pm-user-question.js +62 -0
- package/dist/backend/services/runtime-coordinator-service.js +0 -10
- package/dist/backend/services/session-service.js +3 -70
- package/dist/backend/services/workflow-control-service.js +73 -33
- package/dist/backend/templates/harness/project-manager-agent.js +9 -3
- package/dist/backend/templates/harness/vcm-workflow-review-skill.js +7 -1
- package/dist-frontend/assets/{index-BDUbu5St.js → index-ClaVB8jM.js} +45 -45
- package/dist-frontend/assets/{index-DDmygnV6.css → index-Dc0_KSf4.css} +1 -1
- package/dist-frontend/index.html +2 -2
- package/package.json +4 -1
- package/scripts/harness-tools/run-long-check +90 -43
- package/dist/backend/services/role-stall-detector-service.js +0 -322
- package/dist/shared/types/role-stall.js +0 -1
package/README.md
CHANGED
|
@@ -195,7 +195,10 @@ Whenever Project Manager asks the user a question, it first uses
|
|
|
195
195
|
`vcm-ask-user`. VCM records the wait and cancels any pending role dispatch in
|
|
196
196
|
one state update. Stop Hook then ends the PM turn without route scanning. Only
|
|
197
197
|
a new direct user message clears the wait, and the next role dispatch requires
|
|
198
|
-
a fresh Workflow Review.
|
|
198
|
+
a fresh Workflow Review. PM treats text emitted while a Round continues as
|
|
199
|
+
intermediate output, not as delivered user communication. When the Round pauses
|
|
200
|
+
or completes, its Round Final Reply restates the complete user-facing result or
|
|
201
|
+
question without relying on earlier PM output.
|
|
199
202
|
|
|
200
203
|
Typical flow:
|
|
201
204
|
|
|
@@ -506,14 +509,9 @@ provide the language server for each detected language: `rust-analyzer`,
|
|
|
506
509
|
relationships. Accessors, backing fields, trait items, implementations, wrappers,
|
|
507
510
|
and aliases are queried as separate symbols; a bounded source-text fallback may
|
|
508
511
|
only locate candidates after the correct LSP query is demonstrably partial.
|
|
509
|
-
Other roles use generated context and ordinary source reads.
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
real workspace indexing and semantic query retries. VCM uses Claude Code progress
|
|
513
|
-
hooks to detect a model response, tool call, subagent, or compaction that may be
|
|
514
|
-
stalled. Detection only opens a warning; it does not interrupt the Session or
|
|
515
|
-
change the Round. The user can ignore that warning or explicitly stop and resume
|
|
516
|
-
the same Claude Session from the warning dialog.
|
|
512
|
+
Other roles use generated context and ordinary source reads. Harness Studio
|
|
513
|
+
reports whether the server executable and plugin can run; the role Session
|
|
514
|
+
performs the real workspace indexing and semantic query retries.
|
|
517
515
|
|
|
518
516
|
Harness Engineer is task-scoped and runs from the active task worktree. The
|
|
519
517
|
backend automatically starts a fresh Harness Engineer for each active task or
|
|
@@ -77,8 +77,7 @@ export function registerTaskRoutes(app, deps) {
|
|
|
77
77
|
orchestration,
|
|
78
78
|
roundState: displayedRoundState,
|
|
79
79
|
workflowState,
|
|
80
|
-
architectRestart: deps.architectRestartService.getState(project.repoRoot, taskSlug)
|
|
81
|
-
roleStallWarning: deps.roleStallDetector.getWarning(project.repoRoot, taskSlug)
|
|
80
|
+
architectRestart: deps.architectRestartService.getState(project.repoRoot, taskSlug)
|
|
82
81
|
};
|
|
83
82
|
}
|
|
84
83
|
catch (error) {
|
|
@@ -93,30 +92,12 @@ export function registerTaskRoutes(app, deps) {
|
|
|
93
92
|
},
|
|
94
93
|
roundState: degradedRoundState(taskSlug),
|
|
95
94
|
workflowState: degradedWorkflowState(taskSlug),
|
|
96
|
-
architectRestart: deps.architectRestartService.getState(repoRoot, taskSlug)
|
|
97
|
-
roleStallWarning: deps.roleStallDetector.getWarning(repoRoot, taskSlug)
|
|
95
|
+
architectRestart: deps.architectRestartService.getState(repoRoot, taskSlug)
|
|
98
96
|
};
|
|
99
97
|
}
|
|
100
98
|
throw error;
|
|
101
99
|
}
|
|
102
100
|
});
|
|
103
|
-
app.post("/api/tasks/:taskSlug/role-stall/ignore", async (request) => {
|
|
104
|
-
const project = await requireCurrentProject(deps.projectService);
|
|
105
|
-
return deps.roleStallDetector.ignoreWarning(project.repoRoot, request.params.taskSlug, requireWarningId(request.body?.warningId));
|
|
106
|
-
});
|
|
107
|
-
app.post("/api/tasks/:taskSlug/role-stall/recover", async (request) => {
|
|
108
|
-
const project = await requireCurrentProject(deps.projectService);
|
|
109
|
-
const config = await deps.projectService.loadConfig(project.repoRoot);
|
|
110
|
-
const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
111
|
-
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
112
|
-
return deps.roleStallDetector.recoverWarning({
|
|
113
|
-
repoRoot: project.repoRoot,
|
|
114
|
-
taskRepoRoot,
|
|
115
|
-
stateRoot: config.stateRoot,
|
|
116
|
-
taskSlug: task.taskSlug,
|
|
117
|
-
warningId: requireWarningId(request.body?.warningId)
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
101
|
app.post("/api/tasks/:taskSlug/workflow-state", async (request) => {
|
|
121
102
|
const project = await requireCurrentProject(deps.projectService);
|
|
122
103
|
const config = await deps.projectService.loadConfig(project.repoRoot);
|
|
@@ -155,16 +136,6 @@ async function delayFlowPauseForTranslation(deps, input) {
|
|
|
155
136
|
return input.roundState;
|
|
156
137
|
}
|
|
157
138
|
}
|
|
158
|
-
function requireWarningId(value) {
|
|
159
|
-
if (typeof value === "string" && value.trim()) {
|
|
160
|
-
return value.trim();
|
|
161
|
-
}
|
|
162
|
-
throw new VcmError({
|
|
163
|
-
code: "ROLE_STALL_WARNING_ID_REQUIRED",
|
|
164
|
-
message: "A role stall warning id is required.",
|
|
165
|
-
statusCode: 400
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
139
|
async function requireCurrentProject(projectService) {
|
|
169
140
|
const project = await projectService.getCurrentProject();
|
|
170
141
|
if (!project) {
|
|
@@ -63,15 +63,8 @@ const VCM_HOOK_DEFINITIONS = [
|
|
|
63
63
|
{ eventName: "PreToolUse", matcher: "Write|Edit", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
|
|
64
64
|
{ eventName: "PreToolUse", matcher: "Agent", command: VCM_SUBAGENT_GUARD_HOOK_COMMAND, timeout: 10 },
|
|
65
65
|
{ eventName: "UserPromptSubmit", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
66
|
-
{ eventName: "PreToolUse", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
67
|
-
{ eventName: "PostToolUse", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
68
|
-
{ eventName: "PostToolUseFailure", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
69
|
-
{ eventName: "PostToolBatch", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
70
|
-
{ eventName: "SubagentStart", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
71
|
-
{ eventName: "SubagentStop", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
72
66
|
{ eventName: "Stop", command: VCM_STOP_HOOK_COMMAND, timeout: 10 },
|
|
73
67
|
{ eventName: "StopFailure", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
74
|
-
{ eventName: "PreCompact", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
75
68
|
{ eventName: "PostCompact", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
76
69
|
{ eventName: "PermissionRequest", command: VCM_PERMISSION_REQUEST_HOOK_COMMAND, timeout: 5 }
|
|
77
70
|
];
|
package/dist/backend/server.js
CHANGED
|
@@ -14,7 +14,6 @@ import { createCodexBridgeIntegrationService } from "./services/codex-bridge-int
|
|
|
14
14
|
import { createAutoMemoryService } from "./services/auto-memory-service.js";
|
|
15
15
|
import { createArchitectRestartService } from "./services/architect-restart-service.js";
|
|
16
16
|
import { createRoleContextRestartService } from "./services/role-context-restart-service.js";
|
|
17
|
-
import { createRoleStallDetectorService } from "./services/role-stall-detector-service.js";
|
|
18
17
|
import { createClaudeTranscriptService } from "./services/claude-transcript-service.js";
|
|
19
18
|
import { createGateReviewService } from "./services/gate-review-service.js";
|
|
20
19
|
import { createHarnessFeedbackService } from "./services/harness-feedback-service.js";
|
|
@@ -137,7 +136,6 @@ export async function createServer(deps, options = {}) {
|
|
|
137
136
|
roundService: deps.roundService,
|
|
138
137
|
taskWorkflowService: deps.taskWorkflowService,
|
|
139
138
|
architectRestartService: deps.architectRestartService,
|
|
140
|
-
roleStallDetector: deps.roleStallDetector,
|
|
141
139
|
translationService: deps.translationService
|
|
142
140
|
});
|
|
143
141
|
registerSessionRoutes(app, {
|
|
@@ -245,7 +243,10 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
245
243
|
const runtime = createNodePtyTerminalRuntime({ fs });
|
|
246
244
|
const registry = createSessionRegistry();
|
|
247
245
|
const workflowControlService = createWorkflowControlService({ fs });
|
|
248
|
-
const artifactService = createArtifactService(fs, {
|
|
246
|
+
const artifactService = createArtifactService(fs, {
|
|
247
|
+
workflowControlService,
|
|
248
|
+
isRoleMemoryTurn: (taskRepoRoot, role) => autoMemoryService.isRoleMemoryTurn(taskRepoRoot, role)
|
|
249
|
+
});
|
|
249
250
|
const projectService = createProjectService({ fs, git, appSettings });
|
|
250
251
|
const taskService = createTaskService({ fs, git, artifactService, projectService });
|
|
251
252
|
const taskWorkflowService = createTaskWorkflowService({ fs });
|
|
@@ -351,10 +352,6 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
351
352
|
sessionService
|
|
352
353
|
});
|
|
353
354
|
const transcripts = createClaudeTranscriptService();
|
|
354
|
-
const roleStallDetector = createRoleStallDetectorService({
|
|
355
|
-
sessionService,
|
|
356
|
-
roundService
|
|
357
|
-
});
|
|
358
355
|
const translationService = createTranslationService({
|
|
359
356
|
runtime,
|
|
360
357
|
sessionRegistry: registry,
|
|
@@ -432,7 +429,6 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
432
429
|
translationWorkerService,
|
|
433
430
|
architectRestartService,
|
|
434
431
|
roleContextRestartService,
|
|
435
|
-
roleStallDetector,
|
|
436
432
|
workflowControlService
|
|
437
433
|
});
|
|
438
434
|
const runtimeCoordinator = createRuntimeCoordinatorService({
|
|
@@ -445,7 +441,6 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
445
441
|
harnessFeedbackService,
|
|
446
442
|
autoMemoryService,
|
|
447
443
|
roundService,
|
|
448
|
-
roleStallDetector,
|
|
449
444
|
gatewayService,
|
|
450
445
|
async getStateRoot(repoRoot) {
|
|
451
446
|
return (await projectService.loadConfig(repoRoot)).stateRoot;
|
|
@@ -487,7 +482,6 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
487
482
|
autoMemoryService,
|
|
488
483
|
commandDispatcher,
|
|
489
484
|
claudeHookService,
|
|
490
|
-
roleStallDetector,
|
|
491
485
|
messageService,
|
|
492
486
|
taskLaunchService,
|
|
493
487
|
gateReviewService,
|
|
@@ -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";
|
|
@@ -257,12 +258,6 @@ export function createClaudeHookService(deps) {
|
|
|
257
258
|
role: input.role,
|
|
258
259
|
eventName
|
|
259
260
|
});
|
|
260
|
-
await deps.roleStallDetector?.recordHook({
|
|
261
|
-
...createStallContext(context),
|
|
262
|
-
role: input.role,
|
|
263
|
-
eventName,
|
|
264
|
-
event: input.event
|
|
265
|
-
});
|
|
266
261
|
}
|
|
267
262
|
if (session) {
|
|
268
263
|
await deps.translationService.recordConversationBoundary({
|
|
@@ -344,7 +339,10 @@ export function createClaudeHookService(deps) {
|
|
|
344
339
|
}
|
|
345
340
|
else {
|
|
346
341
|
const lastAssistantMessage = stringOrUndefined(input.event.last_assistant_message);
|
|
347
|
-
|
|
342
|
+
const question = options.allowBlock && lastAssistantMessage
|
|
343
|
+
? findPmUserQuestion(lastAssistantMessage)
|
|
344
|
+
: undefined;
|
|
345
|
+
if (question) {
|
|
348
346
|
return {
|
|
349
347
|
ok: true,
|
|
350
348
|
eventName,
|
|
@@ -354,18 +352,12 @@ export function createClaudeHookService(deps) {
|
|
|
354
352
|
dispatchedCount: 0,
|
|
355
353
|
stopDecision: {
|
|
356
354
|
behavior: "block",
|
|
357
|
-
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 exact question, then ask it and end the turn without advancing or routing the workflow. 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.`
|
|
358
356
|
}
|
|
359
357
|
};
|
|
360
358
|
}
|
|
361
359
|
}
|
|
362
360
|
}
|
|
363
|
-
await deps.roleStallDetector?.recordHook({
|
|
364
|
-
...createStallContext(context),
|
|
365
|
-
role: input.role,
|
|
366
|
-
eventName,
|
|
367
|
-
event: input.event
|
|
368
|
-
});
|
|
369
361
|
if (pmAwaitingUser) {
|
|
370
362
|
return recordTurnEnd(input, context, eventName, {
|
|
371
363
|
dispatchRouteFiles: false,
|
|
@@ -417,12 +409,6 @@ export function createClaudeHookService(deps) {
|
|
|
417
409
|
if (memoryResult) {
|
|
418
410
|
return memoryResult;
|
|
419
411
|
}
|
|
420
|
-
await deps.roleStallDetector?.recordHook({
|
|
421
|
-
...createStallContext(context),
|
|
422
|
-
role: input.role,
|
|
423
|
-
eventName,
|
|
424
|
-
event: input.event
|
|
425
|
-
});
|
|
426
412
|
const routeDispatchInput = createRouteDispatchInput(input, context);
|
|
427
413
|
const pending = await deps.messageService.listPendingRouteFiles(routeDispatchInput);
|
|
428
414
|
const hasCompletionEvidence = pending.some((routeFile) => routeFile.fromRole === input.role);
|
|
@@ -496,14 +482,6 @@ export function createClaudeHookService(deps) {
|
|
|
496
482
|
runtimeSessionId: stringOrUndefined(input.event.vcm_runtime_session_id),
|
|
497
483
|
runtimeSessionToken: input.runtimeSessionToken
|
|
498
484
|
});
|
|
499
|
-
if (session) {
|
|
500
|
-
await deps.roleStallDetector?.recordHook({
|
|
501
|
-
...createStallContext(context),
|
|
502
|
-
role: input.role,
|
|
503
|
-
eventName,
|
|
504
|
-
event: input.event
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
485
|
return {
|
|
508
486
|
ok: true,
|
|
509
487
|
eventName,
|
|
@@ -637,14 +615,6 @@ export function createClaudeHookService(deps) {
|
|
|
637
615
|
eventName
|
|
638
616
|
});
|
|
639
617
|
}
|
|
640
|
-
if (boundToTask) {
|
|
641
|
-
await deps.roleStallDetector?.recordHook({
|
|
642
|
-
...createStallContext(context),
|
|
643
|
-
role: input.role,
|
|
644
|
-
eventName,
|
|
645
|
-
event: input.event
|
|
646
|
-
});
|
|
647
|
-
}
|
|
648
618
|
await deps.autoMemoryService.handleRoleHook({
|
|
649
619
|
baseRepoRoot: context.project.repoRoot,
|
|
650
620
|
taskRepoRoot: context.taskRepoRoot,
|
|
@@ -869,31 +839,6 @@ export function createClaudeHookService(deps) {
|
|
|
869
839
|
taskSlug: context.taskSlug
|
|
870
840
|
};
|
|
871
841
|
}
|
|
872
|
-
function createStallContext(context) {
|
|
873
|
-
return {
|
|
874
|
-
repoRoot: context.project.repoRoot,
|
|
875
|
-
taskRepoRoot: context.taskRepoRoot,
|
|
876
|
-
stateRoot: context.config.stateRoot,
|
|
877
|
-
taskSlug: context.taskSlug
|
|
878
|
-
};
|
|
879
|
-
}
|
|
880
|
-
async function processProgressHook(input, eventName) {
|
|
881
|
-
const role = input.role;
|
|
882
|
-
if (!isVcmRoleName(role)) {
|
|
883
|
-
return completedHookResult(input, eventName);
|
|
884
|
-
}
|
|
885
|
-
const context = await getHookContext(input);
|
|
886
|
-
if (!(await isCurrentRoleHook(context, input, eventName))) {
|
|
887
|
-
return completedHookResult(input, eventName);
|
|
888
|
-
}
|
|
889
|
-
await deps.roleStallDetector?.recordHook({
|
|
890
|
-
...createStallContext(context),
|
|
891
|
-
role,
|
|
892
|
-
eventName,
|
|
893
|
-
event: input.event
|
|
894
|
-
});
|
|
895
|
-
return completedHookResult(input, eventName);
|
|
896
|
-
}
|
|
897
842
|
function renderStopFailureRecoveryPrompt() {
|
|
898
843
|
return [
|
|
899
844
|
"[VCM Recovery]",
|
|
@@ -936,17 +881,6 @@ export function createClaudeHookService(deps) {
|
|
|
936
881
|
});
|
|
937
882
|
}
|
|
938
883
|
const preferences = await deps.appSettings.getPreferences();
|
|
939
|
-
if (deps.roleStallDetector) {
|
|
940
|
-
const context = await getHookContext(input);
|
|
941
|
-
if (await isCurrentRoleHook(context, input, "PermissionRequest")) {
|
|
942
|
-
await deps.roleStallDetector.recordHook({
|
|
943
|
-
...createStallContext(context),
|
|
944
|
-
role: input.role,
|
|
945
|
-
eventName: "PermissionRequest",
|
|
946
|
-
event: input.event
|
|
947
|
-
});
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
884
|
if (preferences.permissionRequestMode !== "allowAll") {
|
|
951
885
|
return undefined;
|
|
952
886
|
}
|
|
@@ -963,9 +897,6 @@ export function createClaudeHookService(deps) {
|
|
|
963
897
|
async handleHook(input) {
|
|
964
898
|
return withRoleHookLock(input, async () => {
|
|
965
899
|
const eventName = parseHookEvent(input.event.hook_event_name);
|
|
966
|
-
if (isProgressHook(eventName)) {
|
|
967
|
-
return processProgressHook(input, eventName);
|
|
968
|
-
}
|
|
969
900
|
if (isTranslatorToolRoleName(input.role)) {
|
|
970
901
|
return processTranslatorHook(input);
|
|
971
902
|
}
|
|
@@ -1004,13 +935,6 @@ function parseHookEvent(value) {
|
|
|
1004
935
|
if (value === "UserPromptSubmit"
|
|
1005
936
|
|| value === "Stop"
|
|
1006
937
|
|| value === "StopFailure"
|
|
1007
|
-
|| value === "PreToolUse"
|
|
1008
|
-
|| value === "PostToolUse"
|
|
1009
|
-
|| value === "PostToolUseFailure"
|
|
1010
|
-
|| value === "PostToolBatch"
|
|
1011
|
-
|| value === "SubagentStart"
|
|
1012
|
-
|| value === "SubagentStop"
|
|
1013
|
-
|| value === "PreCompact"
|
|
1014
938
|
|| value === "PostCompact") {
|
|
1015
939
|
return value;
|
|
1016
940
|
}
|
|
@@ -1021,15 +945,6 @@ function parseHookEvent(value) {
|
|
|
1021
945
|
hint: "Use a Claude Code event installed by the VCM Harness."
|
|
1022
946
|
});
|
|
1023
947
|
}
|
|
1024
|
-
function isProgressHook(eventName) {
|
|
1025
|
-
return eventName === "PreToolUse"
|
|
1026
|
-
|| eventName === "PostToolUse"
|
|
1027
|
-
|| eventName === "PostToolUseFailure"
|
|
1028
|
-
|| eventName === "PostToolBatch"
|
|
1029
|
-
|| eventName === "SubagentStart"
|
|
1030
|
-
|| eventName === "SubagentStop"
|
|
1031
|
-
|| eventName === "PreCompact";
|
|
1032
|
-
}
|
|
1033
948
|
function parseBusinessHookEvent(value) {
|
|
1034
949
|
const eventName = parseHookEvent(value);
|
|
1035
950
|
if (eventName === "UserPromptSubmit"
|
|
@@ -1092,14 +1007,3 @@ function isDirectUserPrompt(prompt) {
|
|
|
1092
1007
|
const normalized = prompt.trim();
|
|
1093
1008
|
return normalized.length > 0 && !/^\[VCM(?:\s|\])/i.test(normalized);
|
|
1094
1009
|
}
|
|
1095
|
-
function asksUserQuestion(message) {
|
|
1096
|
-
const withoutCodeBlocks = message.replace(/```[\s\S]*?```/g, "");
|
|
1097
|
-
const lines = withoutCodeBlocks
|
|
1098
|
-
.split(/\r?\n/)
|
|
1099
|
-
.map((line) => line.trim())
|
|
1100
|
-
.filter(Boolean);
|
|
1101
|
-
return lines.some((line) => /[??]/.test(line)
|
|
1102
|
-
|| /^(?:please\s+(?:answer|choose|confirm|decide|provide|select|tell)|(?:can|could|do|does|is|are|should|will|would)\s+you\b)/i.test(line)
|
|
1103
|
-
|| /\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)
|
|
1104
|
-
|| /(?:请(?:回答|回复|选择|确认|决定|提供|告知|给出|说明)|你(?:是否|能否|要不要|可否)|是否需要你|需要你(?:选择|确认|决定|提供|告知)|需要您的(?:选择|确认|决定|回复)|等待您的(?:选择|确认|决定|回复)|告诉我|告知我)/.test(line));
|
|
1105
|
-
}
|
|
@@ -63,15 +63,8 @@ const VCM_HOOK_DEFINITIONS = [
|
|
|
63
63
|
{ eventName: "PreToolUse", matcher: "Write|Edit", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
|
|
64
64
|
{ eventName: "PreToolUse", matcher: "Agent", command: VCM_SUBAGENT_GUARD_HOOK_COMMAND, timeout: 10 },
|
|
65
65
|
{ eventName: "UserPromptSubmit", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
66
|
-
{ eventName: "PreToolUse", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
67
|
-
{ eventName: "PostToolUse", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
68
|
-
{ eventName: "PostToolUseFailure", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
69
|
-
{ eventName: "PostToolBatch", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
70
|
-
{ eventName: "SubagentStart", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
71
|
-
{ eventName: "SubagentStop", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
72
66
|
{ eventName: "Stop", command: VCM_STOP_HOOK_COMMAND, timeout: 10 },
|
|
73
67
|
{ eventName: "StopFailure", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
74
|
-
{ eventName: "PreCompact", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
75
68
|
{ eventName: "PostCompact", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
76
69
|
{ eventName: "PermissionRequest", command: VCM_PERMISSION_REQUEST_HOOK_COMMAND, timeout: 5 }
|
|
77
70
|
];
|
|
@@ -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
|
+
}
|
|
@@ -42,17 +42,9 @@ export function createRuntimeCoordinatorService(deps) {
|
|
|
42
42
|
]);
|
|
43
43
|
const preferences = await deps.appSettings.getPreferences();
|
|
44
44
|
if (!activeTask) {
|
|
45
|
-
deps.roleStallDetector.clearProject(repoRoot);
|
|
46
45
|
return { activeTask: null, gatewayStatus };
|
|
47
46
|
}
|
|
48
47
|
const taskRepoRoot = getTaskRuntimeRepoRoot(activeTask);
|
|
49
|
-
const stateRoot = await deps.getStateRoot(repoRoot);
|
|
50
|
-
await deps.roleStallDetector.reconcileTask({
|
|
51
|
-
repoRoot,
|
|
52
|
-
taskRepoRoot,
|
|
53
|
-
stateRoot,
|
|
54
|
-
taskSlug: activeTask.taskSlug
|
|
55
|
-
});
|
|
56
48
|
const harnessInitialized = await deps.harnessService.getHarnessStatus(taskRepoRoot)
|
|
57
49
|
.then((status) => status.initialized)
|
|
58
50
|
.catch(() => false);
|
|
@@ -86,7 +78,6 @@ export function createRuntimeCoordinatorService(deps) {
|
|
|
86
78
|
void reconcileCurrentProject().catch(() => undefined);
|
|
87
79
|
},
|
|
88
80
|
stop() {
|
|
89
|
-
deps.roleStallDetector.stop();
|
|
90
81
|
if (reconcileTimer === undefined) {
|
|
91
82
|
return;
|
|
92
83
|
}
|
|
@@ -98,7 +89,6 @@ export function createRuntimeCoordinatorService(deps) {
|
|
|
98
89
|
async function reconcileCurrentProject() {
|
|
99
90
|
const project = await deps.projectService.getCurrentProject();
|
|
100
91
|
if (!project) {
|
|
101
|
-
deps.roleStallDetector.stop();
|
|
102
92
|
return;
|
|
103
93
|
}
|
|
104
94
|
await reconcileProject(project.repoRoot);
|
|
@@ -721,8 +721,7 @@ export function createSessionService(deps) {
|
|
|
721
721
|
return undefined;
|
|
722
722
|
}
|
|
723
723
|
const timestamp = now();
|
|
724
|
-
const turnWasRunning = current.activityStatus === "running"
|
|
725
|
-
&& current.expectedRuntimeExitReason !== "role-stall-recovery";
|
|
724
|
+
const turnWasRunning = current.activityStatus === "running";
|
|
726
725
|
const updated = {
|
|
727
726
|
...current,
|
|
728
727
|
status: input.status,
|
|
@@ -738,71 +737,6 @@ export function createSessionService(deps) {
|
|
|
738
737
|
await persistRoleSessionRecord(deps.fs, repoRoot, getTaskRuntimeRepoRoot(task), config.stateRoot, updated);
|
|
739
738
|
return { record: updated, turnWasRunning };
|
|
740
739
|
}
|
|
741
|
-
async function persistTaskRoleRecord(repoRoot, taskSlug, record) {
|
|
742
|
-
deps.registry.upsert(record);
|
|
743
|
-
const config = await deps.projectService.loadConfig(repoRoot);
|
|
744
|
-
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
745
|
-
await persistRoleSessionRecord(deps.fs, repoRoot, getTaskRuntimeRepoRoot(task), config.stateRoot, record);
|
|
746
|
-
return record;
|
|
747
|
-
}
|
|
748
|
-
async function recoverRoleSession(repoRoot, taskSlug, input) {
|
|
749
|
-
const current = await getTaskRoleSessionView(repoRoot, taskSlug, input.role);
|
|
750
|
-
if (!current
|
|
751
|
-
|| current.id !== input.expectedSessionId
|
|
752
|
-
|| current.runtimeSessionToken !== input.expectedRuntimeSessionToken
|
|
753
|
-
|| current.status !== "running"
|
|
754
|
-
|| current.activityStatus !== "running") {
|
|
755
|
-
throw new VcmError({
|
|
756
|
-
code: "ROLE_STALL_SESSION_CHANGED",
|
|
757
|
-
message: `${input.role} session changed before recovery could begin.`,
|
|
758
|
-
statusCode: 409
|
|
759
|
-
});
|
|
760
|
-
}
|
|
761
|
-
if (!current.claudeSessionId) {
|
|
762
|
-
throw new VcmError({
|
|
763
|
-
code: "CLAUDE_SESSION_MISSING",
|
|
764
|
-
message: `${input.role} does not have a confirmed Claude session id to resume.`,
|
|
765
|
-
statusCode: 409
|
|
766
|
-
});
|
|
767
|
-
}
|
|
768
|
-
const prepared = await persistTaskRoleRecord(repoRoot, taskSlug, {
|
|
769
|
-
...current,
|
|
770
|
-
expectedRuntimeExitReason: "role-stall-recovery",
|
|
771
|
-
updatedAt: now()
|
|
772
|
-
});
|
|
773
|
-
if (deps.runtime.getSession(prepared.id)) {
|
|
774
|
-
await deps.runtime.stop(prepared.id);
|
|
775
|
-
}
|
|
776
|
-
await persistTaskRoleRecord(repoRoot, taskSlug, {
|
|
777
|
-
...prepared,
|
|
778
|
-
status: "exited",
|
|
779
|
-
activityStatus: "idle",
|
|
780
|
-
pid: undefined,
|
|
781
|
-
expectedRuntimeExitReason: undefined,
|
|
782
|
-
updatedAt: now()
|
|
783
|
-
});
|
|
784
|
-
const resumed = await launchRoleSession(repoRoot, taskSlug, input.role, {
|
|
785
|
-
permissionMode: prepared.permissionMode,
|
|
786
|
-
model: prepared.model,
|
|
787
|
-
effort: prepared.effort
|
|
788
|
-
}, "resume");
|
|
789
|
-
if ((await waitForSessionInputReady(resumed.id)) === "exited") {
|
|
790
|
-
throw new VcmError({
|
|
791
|
-
code: "ROLE_STALL_RESUME_FAILED",
|
|
792
|
-
message: `${input.role} session exited before recovery input could be submitted.`,
|
|
793
|
-
statusCode: 409
|
|
794
|
-
});
|
|
795
|
-
}
|
|
796
|
-
await submitTerminalInput(deps.runtime, resumed.id, input.recoveryPrompt);
|
|
797
|
-
return persistTaskRoleRecord(repoRoot, taskSlug, {
|
|
798
|
-
...resumed,
|
|
799
|
-
activityStatus: "running",
|
|
800
|
-
lastTurnStartedAt: now(),
|
|
801
|
-
lastHookEventAt: now(),
|
|
802
|
-
expectedRuntimeExitReason: undefined,
|
|
803
|
-
updatedAt: now()
|
|
804
|
-
});
|
|
805
|
-
}
|
|
806
740
|
return {
|
|
807
741
|
async assertModelLaunchReady(model = "default") {
|
|
808
742
|
await getModelLaunchEnvironment(normalizeClaudeModel(model));
|
|
@@ -1307,8 +1241,7 @@ export function createSessionService(deps) {
|
|
|
1307
1241
|
: undefined;
|
|
1308
1242
|
}
|
|
1309
1243
|
return markTaskRoleActivityIdle(repoRoot, taskSlug, role);
|
|
1310
|
-
}
|
|
1311
|
-
recoverRoleSession
|
|
1244
|
+
}
|
|
1312
1245
|
};
|
|
1313
1246
|
}
|
|
1314
1247
|
function toRoleSessionRecordView(record, runtime) {
|
|
@@ -1398,7 +1331,7 @@ function isSessionMissingError(error) {
|
|
|
1398
1331
|
error.code === "SESSION_MISSING";
|
|
1399
1332
|
}
|
|
1400
1333
|
function withoutRuntimeOnlySessionFields(session) {
|
|
1401
|
-
const { pid: _pid, runtimeSessionToken: _runtimeSessionToken,
|
|
1334
|
+
const { pid: _pid, runtimeSessionToken: _runtimeSessionToken, ...persisted } = session;
|
|
1402
1335
|
return persisted;
|
|
1403
1336
|
}
|
|
1404
1337
|
function defaultIsProcessAlive(pid) {
|