vibe-coding-master 0.7.42 → 0.7.43

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.
Files changed (33) hide show
  1. package/README.md +10 -5
  2. package/dist/backend/api/task-routes.js +31 -2
  3. package/dist/backend/api/workflow-control-routes.js +0 -31
  4. package/dist/backend/cli/install-vcm-harness.js +40 -6
  5. package/dist/backend/role-tool-policy.js +1 -1
  6. package/dist/backend/server.js +12 -5
  7. package/dist/backend/services/artifact-service.js +3 -2
  8. package/dist/backend/services/auto-memory-service.js +2 -2
  9. package/dist/backend/services/claude-hook-service.js +103 -4
  10. package/dist/backend/services/harness-feedback-service.js +105 -3
  11. package/dist/backend/services/harness-service.js +38 -6
  12. package/dist/backend/services/memory-review-paths.js +13 -0
  13. package/dist/backend/services/role-stall-detector-service.js +322 -0
  14. package/dist/backend/services/round-service.js +25 -0
  15. package/dist/backend/services/runtime-coordinator-service.js +10 -0
  16. package/dist/backend/services/session-service.js +70 -3
  17. package/dist/backend/services/workflow-control-service.js +439 -203
  18. package/dist/backend/templates/handoff.js +2 -3
  19. package/dist/backend/templates/harness/architect-agent.js +2 -2
  20. package/dist/backend/templates/harness/coder-agent.js +4 -5
  21. package/dist/backend/templates/harness/gate-review.js +7 -9
  22. package/dist/backend/templates/harness/harness-engineer-agent.js +18 -8
  23. package/dist/backend/templates/harness/project-manager-agent.js +5 -5
  24. package/dist/backend/templates/harness/vcm-code-navigation-skill.js +6 -7
  25. package/dist/backend/templates/harness/vcm-workflow-review-skill.js +7 -9
  26. package/dist/shared/types/role-stall.js +1 -0
  27. package/dist/shared/types/workflow.js +14 -0
  28. package/dist/shared/validation/artifact-registry.js +1 -1
  29. package/dist-frontend/assets/{index-C_XHGNBD.css → index-B0d4Z6ny.css} +1 -1
  30. package/dist-frontend/assets/{index-Bocc2DWF.js → index-VW9tYPP5.js} +38 -38
  31. package/dist-frontend/index.html +2 -2
  32. package/package.json +1 -1
  33. package/scripts/harness-tools/vcm-bash-guard +203 -13
package/README.md CHANGED
@@ -474,14 +474,19 @@ Use it to:
474
474
  - merge task harness commits back to the connected repository branch when
475
475
  appropriate
476
476
 
477
- VCM bundles the Claude Code LSP bridge and loads it for Architect, Coder, and
478
- Reviewer sessions, including CCR launches. The project environment must still
477
+ VCM bundles the Claude Code LSP bridge and loads it only for Architect sessions,
478
+ including CCR launches. The project environment must still
479
479
  provide the language server for each detected language: `rust-analyzer`,
480
480
  `typescript-language-server`, `pyright-langserver`, `gopls`, `clangd`, or
481
- `jdtls`. Their Agent definitions preload `vcm-code-navigation`. Architect,
482
- Coder, and Reviewer use LSP for semantic relationships. Harness Studio reports
481
+ `jdtls`. Architect preloads `vcm-code-navigation` and uses LSP for semantic
482
+ relationships; other roles use generated context and ordinary source reads.
483
+ Harness Studio reports
483
484
  whether the server executable and plugin can run; the role Session performs the
484
- real workspace warm-up and semantic query retries.
485
+ real workspace indexing and semantic query retries. VCM uses Claude Code progress
486
+ hooks to detect a model response, tool call, subagent, or compaction that may be
487
+ stalled. Detection only opens a warning; it does not interrupt the Session or
488
+ change the Round. The user can ignore that warning or explicitly stop and resume
489
+ the same Claude Session from the warning dialog.
485
490
 
486
491
  Harness Engineer is task-scoped and runs from the active task worktree. The
487
492
  backend automatically starts a fresh Harness Engineer for each active task or
@@ -71,7 +71,8 @@ export function registerTaskRoutes(app, deps) {
71
71
  orchestration,
72
72
  roundState,
73
73
  workflowState,
74
- architectRestart: deps.architectRestartService.getState(project.repoRoot, taskSlug)
74
+ architectRestart: deps.architectRestartService.getState(project.repoRoot, taskSlug),
75
+ roleStallWarning: deps.roleStallDetector.getWarning(project.repoRoot, taskSlug)
75
76
  };
76
77
  }
77
78
  catch (error) {
@@ -86,12 +87,30 @@ export function registerTaskRoutes(app, deps) {
86
87
  },
87
88
  roundState: degradedRoundState(taskSlug),
88
89
  workflowState: degradedWorkflowState(taskSlug),
89
- architectRestart: deps.architectRestartService.getState(repoRoot, taskSlug)
90
+ architectRestart: deps.architectRestartService.getState(repoRoot, taskSlug),
91
+ roleStallWarning: deps.roleStallDetector.getWarning(repoRoot, taskSlug)
90
92
  };
91
93
  }
92
94
  throw error;
93
95
  }
94
96
  });
97
+ app.post("/api/tasks/:taskSlug/role-stall/ignore", async (request) => {
98
+ const project = await requireCurrentProject(deps.projectService);
99
+ return deps.roleStallDetector.ignoreWarning(project.repoRoot, request.params.taskSlug, requireWarningId(request.body?.warningId));
100
+ });
101
+ app.post("/api/tasks/:taskSlug/role-stall/recover", async (request) => {
102
+ const project = await requireCurrentProject(deps.projectService);
103
+ const config = await deps.projectService.loadConfig(project.repoRoot);
104
+ const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
105
+ const taskRepoRoot = getTaskRuntimeRepoRoot(task);
106
+ return deps.roleStallDetector.recoverWarning({
107
+ repoRoot: project.repoRoot,
108
+ taskRepoRoot,
109
+ stateRoot: config.stateRoot,
110
+ taskSlug: task.taskSlug,
111
+ warningId: requireWarningId(request.body?.warningId)
112
+ });
113
+ });
95
114
  app.post("/api/tasks/:taskSlug/workflow-state", async (request) => {
96
115
  const project = await requireCurrentProject(deps.projectService);
97
116
  const config = await deps.projectService.loadConfig(project.repoRoot);
@@ -117,6 +136,16 @@ export function registerTaskRoutes(app, deps) {
117
136
  return deps.taskCloseService.closeTask(project.repoRoot, request.params.taskSlug);
118
137
  });
119
138
  }
139
+ function requireWarningId(value) {
140
+ if (typeof value === "string" && value.trim()) {
141
+ return value.trim();
142
+ }
143
+ throw new VcmError({
144
+ code: "ROLE_STALL_WARNING_ID_REQUIRED",
145
+ message: "A role stall warning id is required.",
146
+ statusCode: 400
147
+ });
148
+ }
120
149
  async function requireCurrentProject(projectService) {
121
150
  const project = await projectService.getCurrentProject();
122
151
  if (!project) {
@@ -1,31 +1,10 @@
1
1
  import { VcmError } from "../errors.js";
2
- import { submitTerminalInput } from "../runtime/terminal-submit.js";
3
2
  import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
4
3
  export function registerWorkflowControlRoutes(app, deps) {
5
4
  app.get("/api/tasks/:taskSlug/workflow-control", async (request) => {
6
5
  const context = await getContext(deps, request.params.taskSlug);
7
6
  return deps.workflowControlService.getState(context);
8
7
  });
9
- app.post("/api/tasks/:taskSlug/workflow-overrides/:overrideId/approve", async (request) => {
10
- const authorizationText = request.body?.authorizationText?.trim();
11
- if (!authorizationText) {
12
- throw new VcmError({
13
- code: "WORKFLOW_OVERRIDE_AUTHORIZATION_REQUIRED",
14
- message: "Enter the exact workflow exception that the user authorizes.",
15
- statusCode: 400
16
- });
17
- }
18
- const context = await getContext(deps, request.params.taskSlug);
19
- const state = await deps.workflowControlService.approveOverride(context, request.params.overrideId, authorizationText);
20
- await notifyProjectManager(deps, context, request.params.overrideId, "approved", authorizationText);
21
- return state;
22
- });
23
- app.post("/api/tasks/:taskSlug/workflow-overrides/:overrideId/reject", async (request) => {
24
- const context = await getContext(deps, request.params.taskSlug);
25
- const state = await deps.workflowControlService.rejectOverride(context, request.params.overrideId);
26
- await notifyProjectManager(deps, context, request.params.overrideId, "rejected");
27
- return state;
28
- });
29
8
  }
30
9
  async function getContext(deps, taskSlug) {
31
10
  const project = await deps.projectService.getCurrentProject();
@@ -44,13 +23,3 @@ async function getContext(deps, taskSlug) {
44
23
  taskSlug: task.taskSlug
45
24
  };
46
25
  }
47
- async function notifyProjectManager(deps, context, overrideId, decision, authorizationText) {
48
- const session = await deps.sessionService.getRoleSession(context.repoRoot, context.taskSlug, "project-manager");
49
- if (!session || session.status !== "running" || session.activityStatus === "running")
50
- return;
51
- const prompt = decision === "approved"
52
- ? `[VCM Workflow Override Decision]\nDecision: approved\nAuthorization ID: ${overrideId}\nAuthorization Text: ${authorizationText}\n\nResubmit workflow-progress.md with this Authorization ID and exact Authorization Text.`
53
- : `[VCM Workflow Override Decision]\nDecision: rejected\nAuthorization ID: ${overrideId}\n\nKeep the current workflow and choose a legal next dispatch.`;
54
- await submitTerminalInput(deps.runtime, session.id, prompt);
55
- await deps.sessionService.markRoleActivityRunning(context.repoRoot, context.taskSlug, "project-manager", session.id);
56
- }
@@ -57,8 +57,15 @@ const VCM_HOOK_DEFINITIONS = [
57
57
  { eventName: "PreToolUse", matcher: "Bash", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
58
58
  { eventName: "PreToolUse", matcher: "Write|Edit", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
59
59
  { eventName: "UserPromptSubmit", command: VCM_HOOK_COMMAND, timeout: 5 },
60
+ { eventName: "PreToolUse", command: VCM_HOOK_COMMAND, timeout: 5 },
61
+ { eventName: "PostToolUse", command: VCM_HOOK_COMMAND, timeout: 5 },
62
+ { eventName: "PostToolUseFailure", command: VCM_HOOK_COMMAND, timeout: 5 },
63
+ { eventName: "PostToolBatch", command: VCM_HOOK_COMMAND, timeout: 5 },
64
+ { eventName: "SubagentStart", command: VCM_HOOK_COMMAND, timeout: 5 },
65
+ { eventName: "SubagentStop", command: VCM_HOOK_COMMAND, timeout: 5 },
60
66
  { eventName: "Stop", command: VCM_STOP_HOOK_COMMAND, timeout: 10 },
61
67
  { eventName: "StopFailure", command: VCM_HOOK_COMMAND, timeout: 5 },
68
+ { eventName: "PreCompact", command: VCM_HOOK_COMMAND, timeout: 5 },
62
69
  { eventName: "PostCompact", command: VCM_HOOK_COMMAND, timeout: 5 },
63
70
  { eventName: "PermissionRequest", command: VCM_PERMISSION_REQUEST_HOOK_COMMAND, timeout: 5 }
64
71
  ];
@@ -74,8 +81,7 @@ const AGENT_FRONTMATTER = {
74
81
  },
75
82
  coder: {
76
83
  description: "VCM implementation role for scoped code changes and focused tests.",
77
- disallowedTools: CODE_ROLE_DISALLOWED_TOOLS.join(", "),
78
- skills: ["vcm-code-navigation"]
84
+ disallowedTools: CODE_ROLE_DISALLOWED_TOOLS.join(", ")
79
85
  },
80
86
  tester: {
81
87
  description: "VCM testing role for validation, test adequacy, approved-scope validation, and risk findings.",
@@ -83,8 +89,7 @@ const AGENT_FRONTMATTER = {
83
89
  },
84
90
  reviewer: {
85
91
  description: "VCM independent gate review role for architecture plans, validation adequacy, and code diffs.",
86
- disallowedTools: REVIEWER_DISALLOWED_TOOLS.join(", "),
87
- skills: ["vcm-code-navigation"]
92
+ disallowedTools: REVIEWER_DISALLOWED_TOOLS.join(", ")
88
93
  },
89
94
  translator: {
90
95
  description: "VCM task-scoped translation tool role for conversation translation, file translation, bootstrap, and memory updates."
@@ -114,7 +119,9 @@ const REQUIRED_AGENT_TOOLS = {
114
119
  "harness-engineer": ["Skill"]
115
120
  };
116
121
  const REQUIRED_AGENT_SKILLS = {
117
- architect: ["vcm-code-navigation"],
122
+ architect: ["vcm-code-navigation"]
123
+ };
124
+ const REMOVED_AGENT_SKILLS = {
118
125
  coder: ["vcm-code-navigation"],
119
126
  reviewer: ["vcm-code-navigation"]
120
127
  };
@@ -285,7 +292,7 @@ const WHOLE_FILES = [
285
292
  path: ".claude/skills/vcm-code-navigation/SKILL.md",
286
293
  category: "skill",
287
294
  mode: 0o644,
288
- content: renderSkillFile("VCM Code Navigation Skill", "vcm-code-navigation", "Use when Architect, Coder, or Reviewer must resolve code symbols, references, implementations, call hierarchies, or bounded dependency paths.", renderVcmCodeNavigationSkillRules())
295
+ content: renderSkillFile("VCM Code Navigation Skill", "vcm-code-navigation", "Use when Architect must resolve code symbols, references, implementations, call hierarchies, or bounded dependency paths.", renderVcmCodeNavigationSkillRules())
289
296
  },
290
297
  {
291
298
  path: ".claude/skills/vcm-final-acceptance/SKILL.md",
@@ -711,6 +718,9 @@ async function installManagedFile({ projectRoot, definition, dryRun, operations
711
718
  for (const requiredSkill of REQUIRED_AGENT_SKILLS[definition.agentName] ?? []) {
712
719
  nextContent = ensureAgentSkill(nextContent, requiredSkill);
713
720
  }
721
+ for (const removedSkill of REMOVED_AGENT_SKILLS[definition.agentName] ?? []) {
722
+ nextContent = removeAgentSkill(nextContent, removedSkill);
723
+ }
714
724
  await writeIfChanged({
715
725
  targetPath,
716
726
  relativePath: definition.path,
@@ -829,6 +839,30 @@ function ensureAgentSkill(content, requiredSkill) {
829
839
  const nextSkills = `${skillsMatch[0].trimEnd()}\n - ${requiredSkill}`;
830
840
  return content.replace(frontmatter, frontmatter.replace(skillsMatch[0], nextSkills));
831
841
  }
842
+ function removeAgentSkill(content, removedSkill) {
843
+ const frontmatterMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---/);
844
+ if (!frontmatterMatch) {
845
+ return content;
846
+ }
847
+ const frontmatter = frontmatterMatch[0];
848
+ const skillsMatch = frontmatter.match(/^skills:[ \t]*(?:\r?\n((?:\s+-\s+[^\r\n]+\r?\n?)*))?/m);
849
+ if (!skillsMatch) {
850
+ return content;
851
+ }
852
+ const listedSkills = (skillsMatch[1] ?? "")
853
+ .split(/\r?\n/)
854
+ .map((line) => line.match(/^\s+-\s+(.+)$/)?.[1]?.trim())
855
+ .filter(Boolean);
856
+ const remainingSkills = listedSkills.filter((skill) => skill !== removedSkill);
857
+ if (remainingSkills.length === listedSkills.length) {
858
+ return content;
859
+ }
860
+ const lineEnding = skillsMatch[0].includes("\r\n") ? "\r\n" : "\n";
861
+ const replacement = remainingSkills.length > 0
862
+ ? `skills:${lineEnding}${remainingSkills.map((skill) => ` - ${skill}`).join(lineEnding)}${lineEnding}`
863
+ : "";
864
+ return content.replace(frontmatter, frontmatter.replace(skillsMatch[0], replacement));
865
+ }
832
866
  function migrateLegacyManagedFile(definition, currentContent, block) {
833
867
  const legacyContent = definition.legacyWholeFile?.trimEnd();
834
868
  if (!legacyContent) {
@@ -44,7 +44,7 @@ export const REVIEWER_RUNTIME_DISALLOWED_TOOLS = [
44
44
  "Edit",
45
45
  ...NON_VCM_ROLE_RUNTIME_TOOLS
46
46
  ];
47
- const LSP_ROLE_NAMES = new Set(["architect", "coder", "reviewer"]);
47
+ const LSP_ROLE_NAMES = new Set(["architect"]);
48
48
  export function roleUsesLsp(role) {
49
49
  return LSP_ROLE_NAMES.has(role);
50
50
  }
@@ -13,6 +13,7 @@ import { createAppSettingsService } from "./services/app-settings-service.js";
13
13
  import { createCcrIntegrationService } from "./services/ccr-integration-service.js";
14
14
  import { createAutoMemoryService } from "./services/auto-memory-service.js";
15
15
  import { createArchitectRestartService } from "./services/architect-restart-service.js";
16
+ import { createRoleStallDetectorService } from "./services/role-stall-detector-service.js";
16
17
  import { createClaudeTranscriptService } from "./services/claude-transcript-service.js";
17
18
  import { createGateReviewService } from "./services/gate-review-service.js";
18
19
  import { createHarnessFeedbackService } from "./services/harness-feedback-service.js";
@@ -134,7 +135,8 @@ export async function createServer(deps, options = {}) {
134
135
  taskLaunchService: deps.taskLaunchService,
135
136
  roundService: deps.roundService,
136
137
  taskWorkflowService: deps.taskWorkflowService,
137
- architectRestartService: deps.architectRestartService
138
+ architectRestartService: deps.architectRestartService,
139
+ roleStallDetector: deps.roleStallDetector
138
140
  });
139
141
  registerSessionRoutes(app, {
140
142
  projectService: deps.projectService,
@@ -154,9 +156,7 @@ export async function createServer(deps, options = {}) {
154
156
  registerWorkflowControlRoutes(app, {
155
157
  projectService: deps.projectService,
156
158
  taskService: deps.taskService,
157
- sessionService: deps.sessionService,
158
- workflowControlService: deps.workflowControlService,
159
- runtime: deps.runtime
159
+ workflowControlService: deps.workflowControlService
160
160
  });
161
161
  }
162
162
  registerMessageRoutes(app, {
@@ -340,6 +340,10 @@ export function createDefaultServerDeps(options = {}) {
340
340
  sessionService
341
341
  });
342
342
  const transcripts = createClaudeTranscriptService();
343
+ const roleStallDetector = createRoleStallDetectorService({
344
+ sessionService,
345
+ roundService
346
+ });
343
347
  const translationService = createTranslationService({
344
348
  runtime,
345
349
  sessionRegistry: registry,
@@ -411,7 +415,8 @@ export function createDefaultServerDeps(options = {}) {
411
415
  gatewayService,
412
416
  jobGuard: createJobGuardService(),
413
417
  translationWorkerService,
414
- architectRestartService
418
+ architectRestartService,
419
+ roleStallDetector
415
420
  });
416
421
  const runtimeCoordinator = createRuntimeCoordinatorService({
417
422
  appSettings,
@@ -423,6 +428,7 @@ export function createDefaultServerDeps(options = {}) {
423
428
  harnessFeedbackService,
424
429
  autoMemoryService,
425
430
  roundService,
431
+ roleStallDetector,
426
432
  gatewayService,
427
433
  async getStateRoot(repoRoot) {
428
434
  return (await projectService.loadConfig(repoRoot)).stateRoot;
@@ -463,6 +469,7 @@ export function createDefaultServerDeps(options = {}) {
463
469
  autoMemoryService,
464
470
  commandDispatcher,
465
471
  claudeHookService,
472
+ roleStallDetector,
466
473
  messageService,
467
474
  taskLaunchService,
468
475
  gateReviewService,
@@ -7,6 +7,7 @@ import { resolveRepoPath } from "../adapters/filesystem.js";
7
7
  import { renderArchitectureBriefTemplate, renderArchitectureDiagnosisTemplate, renderArchitectureEvidenceTemplate, renderArchitecturePlanTemplate, renderArchitectDebugTemplate, renderCoderCompletionTemplate, renderDocsSyncReportTemplate, renderFinalAcceptanceTemplate, renderKnownIssuesTemplate, renderPlanningProgressTemplate, renderMessageRouteTemplate, renderTestReportTemplate, renderWorkflowProgressTemplate } from "../templates/handoff.js";
8
8
  import { renderRoleCommandTemplate } from "../templates/role-command.js";
9
9
  import { validateMemoryProposal } from "./memory-proposal-validation.js";
10
+ import { isMemoryProposalSubmissionPath } from "./memory-review-paths.js";
10
11
  const ARTIFACT_PATH_KEYS = [
11
12
  ["architecture-brief", "architectureBriefPath"],
12
13
  ["architecture-evidence", "architectureEvidencePath"],
@@ -314,8 +315,8 @@ async function validateDynamicArtifact(fs, input, content, workflowControlServic
314
315
  if (!VCM_ROLE_NAMES.includes(input.role)) {
315
316
  throw artifactRejected(input.kind, ["Only a VCM workflow role may submit a Memory Proposal."]);
316
317
  }
317
- if (!/^\.ai\/vcm\/memory-review\/(?:runs\/[A-Za-z0-9._-]+\/drafts|candidates)\/[A-Za-z0-9._-]+\.md$/.test(artifactPath)) {
318
- throw artifactRejected(input.kind, ["Memory proposal path is outside the VCM memory-review draft locations."]);
318
+ if (!isMemoryProposalSubmissionPath(artifactPath, input.role)) {
319
+ throw artifactRejected(input.kind, ["Memory proposal path is not assigned to the submitting role."]);
319
320
  }
320
321
  const memoryError = validateMemoryProposal(content);
321
322
  if (memoryError) {
@@ -5,7 +5,7 @@ import { resolveRepoPath } from "../adapters/filesystem.js";
5
5
  import { VcmError } from "../errors.js";
6
6
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
7
7
  import { readVcmMemoryBlock, replaceVcmMemoryBlock } from "../templates/harness/memory-block.js";
8
- import { ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH, architectPlanningCandidateSnapshotPath, MEMORY_REVIEW_RUNS_ROOT, MEMORY_REVIEW_STATE_PATH } from "./memory-review-paths.js";
8
+ import { ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH, architectPlanningCandidateSnapshotPath, memoryReviewRoleDraftPath, MEMORY_REVIEW_RUNS_ROOT, MEMORY_REVIEW_STATE_PATH } from "./memory-review-paths.js";
9
9
  import { parseMemoryProposal, validateMemoryProposal } from "./memory-proposal-validation.js";
10
10
  const MEMORY_FILE_DEFINITIONS = [
11
11
  { path: "CLAUDE.md", title: "Shared Memory" },
@@ -175,7 +175,7 @@ export function createAutoMemoryService(deps) {
175
175
  const runId = createRunId(timestamp, "auto");
176
176
  const drafts = roles.map((role) => ({
177
177
  role,
178
- path: `${MEMORY_REVIEW_RUNS_ROOT}/${runId}/drafts/${role}.md`,
178
+ path: memoryReviewRoleDraftPath(runId, role),
179
179
  status: "pending"
180
180
  }));
181
181
  const state = {
@@ -80,7 +80,7 @@ export function createClaudeHookService(deps) {
80
80
  return { project };
81
81
  }
82
82
  async function processTranslatorHook(input) {
83
- const eventName = parseHookEvent(input.event.hook_event_name);
83
+ const eventName = parseBusinessHookEvent(input.event.hook_event_name);
84
84
  const context = await getTranslatorHookContext();
85
85
  const session = input.taskSlug === "__project__"
86
86
  ? await deps.sessionService.recordProjectTranslatorHookEvent(context.project.repoRoot, {
@@ -114,7 +114,7 @@ export function createClaudeHookService(deps) {
114
114
  };
115
115
  }
116
116
  async function processHarnessEngineerHook(input) {
117
- const eventName = parseHookEvent(input.event.hook_event_name);
117
+ const eventName = parseBusinessHookEvent(input.event.hook_event_name);
118
118
  const context = await getProjectToolHookContext("Harness Engineer");
119
119
  const projectScoped = input.taskSlug === "__project_harness_engineer__";
120
120
  const session = projectScoped
@@ -240,6 +240,12 @@ export function createClaudeHookService(deps) {
240
240
  role: input.role,
241
241
  eventName
242
242
  });
243
+ await deps.roleStallDetector?.recordHook({
244
+ ...createStallContext(context),
245
+ role: input.role,
246
+ eventName,
247
+ event: input.event
248
+ });
243
249
  }
244
250
  if (session) {
245
251
  await deps.translationService.recordConversationBoundary({
@@ -313,6 +319,12 @@ export function createClaudeHookService(deps) {
313
319
  };
314
320
  }
315
321
  }
322
+ await deps.roleStallDetector?.recordHook({
323
+ ...createStallContext(context),
324
+ role: input.role,
325
+ eventName,
326
+ event: input.event
327
+ });
316
328
  return recordTurnEnd(input, context, eventName, {
317
329
  dispatchRouteFiles: !isReviewerRoleName(input.role),
318
330
  notifyGateway: true,
@@ -357,6 +369,12 @@ export function createClaudeHookService(deps) {
357
369
  if (memoryResult) {
358
370
  return memoryResult;
359
371
  }
372
+ await deps.roleStallDetector?.recordHook({
373
+ ...createStallContext(context),
374
+ role: input.role,
375
+ eventName,
376
+ event: input.event
377
+ });
360
378
  const routeDispatchInput = createRouteDispatchInput(input, context);
361
379
  const pending = await deps.messageService.listPendingRouteFiles(routeDispatchInput);
362
380
  const hasCompletionEvidence = pending.some((routeFile) => routeFile.fromRole === input.role);
@@ -430,6 +448,14 @@ export function createClaudeHookService(deps) {
430
448
  runtimeSessionId: stringOrUndefined(input.event.vcm_runtime_session_id),
431
449
  runtimeSessionToken: input.runtimeSessionToken
432
450
  });
451
+ if (session) {
452
+ await deps.roleStallDetector?.recordHook({
453
+ ...createStallContext(context),
454
+ role: input.role,
455
+ eventName,
456
+ event: input.event
457
+ });
458
+ }
433
459
  return {
434
460
  ok: true,
435
461
  eventName,
@@ -552,6 +578,14 @@ export function createClaudeHookService(deps) {
552
578
  eventName
553
579
  });
554
580
  }
581
+ if (boundToTask) {
582
+ await deps.roleStallDetector?.recordHook({
583
+ ...createStallContext(context),
584
+ role: input.role,
585
+ eventName,
586
+ event: input.event
587
+ });
588
+ }
555
589
  await deps.autoMemoryService.handleRoleHook({
556
590
  baseRepoRoot: context.project.repoRoot,
557
591
  taskRepoRoot: context.taskRepoRoot,
@@ -764,6 +798,31 @@ export function createClaudeHookService(deps) {
764
798
  taskSlug: context.taskSlug
765
799
  };
766
800
  }
801
+ function createStallContext(context) {
802
+ return {
803
+ repoRoot: context.project.repoRoot,
804
+ taskRepoRoot: context.taskRepoRoot,
805
+ stateRoot: context.config.stateRoot,
806
+ taskSlug: context.taskSlug
807
+ };
808
+ }
809
+ async function processProgressHook(input, eventName) {
810
+ const role = input.role;
811
+ if (!isVcmRoleName(role)) {
812
+ return completedHookResult(input, eventName);
813
+ }
814
+ const context = await getHookContext(input);
815
+ if (!(await isCurrentRoleHook(context, input, eventName))) {
816
+ return completedHookResult(input, eventName);
817
+ }
818
+ await deps.roleStallDetector?.recordHook({
819
+ ...createStallContext(context),
820
+ role,
821
+ eventName,
822
+ event: input.event
823
+ });
824
+ return completedHookResult(input, eventName);
825
+ }
767
826
  function renderStopFailureRecoveryPrompt() {
768
827
  return [
769
828
  "[VCM Recovery]",
@@ -806,6 +865,17 @@ export function createClaudeHookService(deps) {
806
865
  });
807
866
  }
808
867
  const preferences = await deps.appSettings.getPreferences();
868
+ if (deps.roleStallDetector) {
869
+ const context = await getHookContext(input);
870
+ if (await isCurrentRoleHook(context, input, "PermissionRequest")) {
871
+ await deps.roleStallDetector.recordHook({
872
+ ...createStallContext(context),
873
+ role: input.role,
874
+ eventName: "PermissionRequest",
875
+ event: input.event
876
+ });
877
+ }
878
+ }
809
879
  if (preferences.permissionRequestMode !== "allowAll") {
810
880
  return undefined;
811
881
  }
@@ -821,13 +891,16 @@ export function createClaudeHookService(deps) {
821
891
  return {
822
892
  async handleHook(input) {
823
893
  return withRoleHookLock(input, async () => {
894
+ const eventName = parseHookEvent(input.event.hook_event_name);
895
+ if (isProgressHook(eventName)) {
896
+ return processProgressHook(input, eventName);
897
+ }
824
898
  if (isTranslatorToolRoleName(input.role)) {
825
899
  return processTranslatorHook(input);
826
900
  }
827
901
  if (isHarnessEngineerToolRoleName(input.role)) {
828
902
  return processHarnessEngineerHook(input);
829
903
  }
830
- const eventName = parseHookEvent(input.event.hook_event_name);
831
904
  if (eventName === "UserPromptSubmit") {
832
905
  return handleUserPromptSubmitHook(input);
833
906
  }
@@ -860,6 +933,13 @@ function parseHookEvent(value) {
860
933
  if (value === "UserPromptSubmit"
861
934
  || value === "Stop"
862
935
  || value === "StopFailure"
936
+ || value === "PreToolUse"
937
+ || value === "PostToolUse"
938
+ || value === "PostToolUseFailure"
939
+ || value === "PostToolBatch"
940
+ || value === "SubagentStart"
941
+ || value === "SubagentStop"
942
+ || value === "PreCompact"
863
943
  || value === "PostCompact") {
864
944
  return value;
865
945
  }
@@ -867,9 +947,28 @@ function parseHookEvent(value) {
867
947
  code: "HOOK_EVENT_UNSUPPORTED",
868
948
  message: `Unsupported Claude Code hook event: ${String(value)}`,
869
949
  statusCode: 400,
870
- hint: "VCM accepts UserPromptSubmit, Stop, StopFailure, and PostCompact hooks only."
950
+ hint: "Use a Claude Code event installed by the VCM Harness."
871
951
  });
872
952
  }
953
+ function isProgressHook(eventName) {
954
+ return eventName === "PreToolUse"
955
+ || eventName === "PostToolUse"
956
+ || eventName === "PostToolUseFailure"
957
+ || eventName === "PostToolBatch"
958
+ || eventName === "SubagentStart"
959
+ || eventName === "SubagentStop"
960
+ || eventName === "PreCompact";
961
+ }
962
+ function parseBusinessHookEvent(value) {
963
+ const eventName = parseHookEvent(value);
964
+ if (eventName === "UserPromptSubmit"
965
+ || eventName === "Stop"
966
+ || eventName === "StopFailure"
967
+ || eventName === "PostCompact") {
968
+ return eventName;
969
+ }
970
+ throwUnsupportedEvent(eventName);
971
+ }
873
972
  function throwUnsupportedEvent(eventName) {
874
973
  throw new VcmError({
875
974
  code: "HOOK_EVENT_UNSUPPORTED",