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.
- package/README.md +10 -5
- package/dist/backend/api/task-routes.js +31 -2
- package/dist/backend/api/workflow-control-routes.js +0 -31
- package/dist/backend/cli/install-vcm-harness.js +40 -6
- package/dist/backend/role-tool-policy.js +1 -1
- package/dist/backend/server.js +12 -5
- package/dist/backend/services/artifact-service.js +3 -2
- package/dist/backend/services/auto-memory-service.js +2 -2
- package/dist/backend/services/claude-hook-service.js +103 -4
- package/dist/backend/services/harness-feedback-service.js +105 -3
- package/dist/backend/services/harness-service.js +38 -6
- package/dist/backend/services/memory-review-paths.js +13 -0
- package/dist/backend/services/role-stall-detector-service.js +322 -0
- package/dist/backend/services/round-service.js +25 -0
- package/dist/backend/services/runtime-coordinator-service.js +10 -0
- package/dist/backend/services/session-service.js +70 -3
- package/dist/backend/services/workflow-control-service.js +439 -203
- package/dist/backend/templates/handoff.js +2 -3
- package/dist/backend/templates/harness/architect-agent.js +2 -2
- package/dist/backend/templates/harness/coder-agent.js +4 -5
- package/dist/backend/templates/harness/gate-review.js +7 -9
- package/dist/backend/templates/harness/harness-engineer-agent.js +18 -8
- package/dist/backend/templates/harness/project-manager-agent.js +5 -5
- package/dist/backend/templates/harness/vcm-code-navigation-skill.js +6 -7
- package/dist/backend/templates/harness/vcm-workflow-review-skill.js +7 -9
- package/dist/shared/types/role-stall.js +1 -0
- package/dist/shared/types/workflow.js +14 -0
- package/dist/shared/validation/artifact-registry.js +1 -1
- package/dist-frontend/assets/{index-C_XHGNBD.css → index-B0d4Z6ny.css} +1 -1
- package/dist-frontend/assets/{index-Bocc2DWF.js → index-VW9tYPP5.js} +38 -38
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- 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
|
|
478
|
-
|
|
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`.
|
|
482
|
-
|
|
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
|
|
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
|
|
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"
|
|
47
|
+
const LSP_ROLE_NAMES = new Set(["architect"]);
|
|
48
48
|
export function roleUsesLsp(role) {
|
|
49
49
|
return LSP_ROLE_NAMES.has(role);
|
|
50
50
|
}
|
package/dist/backend/server.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
318
|
-
throw artifactRejected(input.kind, ["Memory proposal path is
|
|
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:
|
|
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 =
|
|
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 =
|
|
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: "
|
|
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",
|