vibe-coding-master 0.3.32 → 0.4.1
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 +1 -1
- package/dist/backend/api/harness-routes.js +56 -0
- package/dist/backend/api/session-routes.js +5 -0
- package/dist/backend/api/translation-routes.js +4 -0
- package/dist/backend/cli/install-vcm-harness.js +12 -0
- package/dist/backend/server.js +10 -8
- package/dist/backend/services/claude-hook-service.js +42 -2
- package/dist/backend/services/harness-revision.js +32 -0
- package/dist/backend/services/harness-service.js +324 -118
- package/dist/backend/services/session-service.js +385 -13
- package/dist/backend/templates/harness/harness-engineer-agent.js +63 -0
- package/dist/shared/constants.js +11 -1
- package/dist-frontend/assets/index-kJDZAzjD.css +32 -0
- package/dist-frontend/assets/index-uYhgIwNK.js +94 -0
- package/dist-frontend/index.html +2 -2
- package/docs/full-harness-baseline.md +1 -1
- package/docs/product-design.md +1 -1
- package/docs/v0.4-harness-optimization-plan.md +662 -0
- package/docs/{v0.4-custom-workflow-plan.md → v0.5-custom-workflow-plan.md} +15 -11
- package/docs/vcm-cc-best-practices.md +7 -12
- package/package.json +1 -1
- package/dist-frontend/assets/index-BaIbh99h.js +0 -94
- package/dist-frontend/assets/index-BvUHLq4X.css +0 -32
package/README.md
CHANGED
|
@@ -486,7 +486,7 @@ For `.gitignore`, VCM uses a gitignore-native managed block:
|
|
|
486
486
|
|
|
487
487
|
VCM also JSON-merges `.claude/settings.json` to install Claude Code `PreToolUse`, `UserPromptSubmit`, `Stop`, and `PermissionRequest` hooks plus a managed `env.BASH_DEFAULT_TIMEOUT_MS` so foreground watch windows fit inside the Bash tool timeout. The hooks post directly to the local VCM backend, so roles do not need a VCM CLI command to confirm delivery or report turn completion. The `Stop` hook forwards the backend response to Claude Code, which lets VCM block turn-end while a validation job is still running.
|
|
488
488
|
|
|
489
|
-
Bootstrap is AI-assisted. VCM starts
|
|
489
|
+
Bootstrap is AI-assisted. VCM starts or resumes the project-scoped Harness Engineer Claude Code session in the connected repository and asks it to use the `vcm-harness-bootstrap` skill. Bootstrap fills project-specific content and generated context:
|
|
490
490
|
|
|
491
491
|
```text
|
|
492
492
|
CLAUDE.md project context outside the VCM managed block
|
|
@@ -16,6 +16,21 @@ export function registerHarnessRoutes(app, deps) {
|
|
|
16
16
|
const project = await requireCurrentProject(deps.projectService);
|
|
17
17
|
return deps.harnessService.applyHarness(project.repoRoot);
|
|
18
18
|
});
|
|
19
|
+
app.get("/api/projects/harness/file", async (request) => {
|
|
20
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
21
|
+
return deps.harnessService.getHarnessFileContent(project.repoRoot, request.query.path ?? "");
|
|
22
|
+
});
|
|
23
|
+
app.put("/api/projects/harness/file", async (request) => {
|
|
24
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
25
|
+
if (typeof request.body?.content !== "string") {
|
|
26
|
+
throw new VcmError({
|
|
27
|
+
code: "HARNESS_FILE_CONTENT_INVALID",
|
|
28
|
+
message: "Harness file content must be a string.",
|
|
29
|
+
statusCode: 400
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return deps.harnessService.updateHarnessFileContent(project.repoRoot, request.query.path ?? "", request.body.content);
|
|
33
|
+
});
|
|
19
34
|
app.post("/api/projects/harness/tasks/:taskSlug/commit-and-rebase", async (request) => {
|
|
20
35
|
const project = await requireCurrentProject(deps.projectService);
|
|
21
36
|
const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
@@ -42,10 +57,51 @@ export function registerHarnessRoutes(app, deps) {
|
|
|
42
57
|
const project = await requireCurrentProject(deps.projectService);
|
|
43
58
|
return deps.harnessService.startHarnessBootstrap(project.repoRoot, request.body ?? {});
|
|
44
59
|
});
|
|
60
|
+
app.post("/api/projects/harness/bootstrap/restart", async (request) => {
|
|
61
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
62
|
+
return deps.harnessService.restartHarnessBootstrap(project.repoRoot, request.body ?? {});
|
|
63
|
+
});
|
|
64
|
+
app.post("/api/projects/harness/bootstrap/stop", async () => {
|
|
65
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
66
|
+
return deps.harnessService.stopHarnessBootstrap(project.repoRoot);
|
|
67
|
+
});
|
|
68
|
+
app.post("/api/projects/harness/bootstrap/run", async () => {
|
|
69
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
70
|
+
return deps.harnessService.runHarnessBootstrap(project.repoRoot);
|
|
71
|
+
});
|
|
72
|
+
app.get("/api/projects/harness/engineer/session", async () => {
|
|
73
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
74
|
+
return (await deps.sessionService.getProjectHarnessEngineerSession(project.repoRoot)) ?? null;
|
|
75
|
+
});
|
|
76
|
+
app.post("/api/projects/harness/engineer/session/ensure", async (request) => {
|
|
77
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
78
|
+
return deps.sessionService.ensureProjectHarnessEngineerSession(project.repoRoot, request.body);
|
|
79
|
+
});
|
|
80
|
+
app.post("/api/projects/harness/engineer/session/start", async (request) => {
|
|
81
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
82
|
+
return deps.sessionService.startProjectHarnessEngineerSession(project.repoRoot, request.body);
|
|
83
|
+
});
|
|
84
|
+
app.post("/api/projects/harness/engineer/session/resume", async (request) => {
|
|
85
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
86
|
+
return deps.sessionService.resumeProjectHarnessEngineerSession(project.repoRoot, request.body);
|
|
87
|
+
});
|
|
88
|
+
app.post("/api/projects/harness/engineer/session/restart", async (request) => {
|
|
89
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
90
|
+
return deps.sessionService.restartProjectHarnessEngineerSession(project.repoRoot, request.body);
|
|
91
|
+
});
|
|
92
|
+
app.post("/api/projects/harness/engineer/session/stop", async () => {
|
|
93
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
94
|
+
return deps.sessionService.stopProjectHarnessEngineerSession(project.repoRoot);
|
|
95
|
+
});
|
|
96
|
+
app.post("/api/projects/harness/engineer/session/notify-harness", async () => {
|
|
97
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
98
|
+
return deps.sessionService.notifyProjectHarnessEngineerHarnessUpdated(project.repoRoot);
|
|
99
|
+
});
|
|
45
100
|
}
|
|
46
101
|
function degradedHarnessStatus(error) {
|
|
47
102
|
return {
|
|
48
103
|
version: 1,
|
|
104
|
+
harnessRevision: 0,
|
|
49
105
|
initialized: false,
|
|
50
106
|
files: [],
|
|
51
107
|
needsApply: false,
|
|
@@ -33,6 +33,11 @@ export function registerSessionRoutes(app, deps) {
|
|
|
33
33
|
const role = parseRole(request.params.role);
|
|
34
34
|
return deps.sessionService.resumeRoleSession(project.repoRoot, request.params.taskSlug, role, request.body);
|
|
35
35
|
});
|
|
36
|
+
app.post("/api/tasks/:taskSlug/sessions/:role/notify-harness", async (request) => {
|
|
37
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
38
|
+
const role = parseRole(request.params.role);
|
|
39
|
+
return deps.sessionService.notifyRoleHarnessUpdated(project.repoRoot, request.params.taskSlug, role);
|
|
40
|
+
});
|
|
36
41
|
app.post("/api/tasks/:taskSlug/sessions/:role/dispatch", async (request) => {
|
|
37
42
|
const project = await requireCurrentProject(deps.projectService);
|
|
38
43
|
if (!isDispatchableRole(request.params.role)) {
|
|
@@ -52,6 +52,10 @@ export function registerTranslationRoutes(app, deps) {
|
|
|
52
52
|
await deps.translationService.stopSession(request.params.sessionId);
|
|
53
53
|
return { ok: true };
|
|
54
54
|
});
|
|
55
|
+
app.post("/api/projects/translation/session/notify-harness", async () => {
|
|
56
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
57
|
+
return deps.sessionService.notifyProjectTranslatorHarnessUpdated(project.repoRoot);
|
|
58
|
+
});
|
|
55
59
|
app.post("/api/translation/sessions/:sessionId/retry/:translationId", async (request) => {
|
|
56
60
|
await requireCurrentProject(deps.projectService);
|
|
57
61
|
return deps.translationService.retryTranslation(request.params.sessionId, request.params.translationId);
|
|
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { renderArchitectHarnessRules } from "../templates/harness/architect-agent.js";
|
|
8
8
|
import { renderCoderHarnessRules } from "../templates/harness/coder-agent.js";
|
|
9
9
|
import { renderGateReviewerAgentRules, renderRequestGateReviewTool, renderTranslatorAgentRules, renderVcmGateReviewSkillRules } from "../templates/harness/gate-review.js";
|
|
10
|
+
import { renderHarnessEngineerHarnessRules } from "../templates/harness/harness-engineer-agent.js";
|
|
10
11
|
import { renderRootClaudeHarnessRules } from "../templates/harness/claude-root.js";
|
|
11
12
|
import { renderGitignoreHarnessRules } from "../templates/harness/gitignore.js";
|
|
12
13
|
import { renderProjectManagerHarnessRules } from "../templates/harness/project-manager-agent.js";
|
|
@@ -59,6 +60,9 @@ const AGENT_FRONTMATTER = {
|
|
|
59
60
|
},
|
|
60
61
|
translator: {
|
|
61
62
|
description: "VCM project translation tool role for conversation translation, file translation, bootstrap, and memory updates."
|
|
63
|
+
},
|
|
64
|
+
"harness-engineer": {
|
|
65
|
+
description: "VCM project-scoped harness maintenance role for harness diagnosis, diff proposals, and VCM issue drafts."
|
|
62
66
|
}
|
|
63
67
|
};
|
|
64
68
|
const MANAGED_FILES = [
|
|
@@ -132,6 +136,14 @@ const MANAGED_FILES = [
|
|
|
132
136
|
commentStyle: "html",
|
|
133
137
|
category: "agent-translator",
|
|
134
138
|
content: renderTranslatorAgentRules()
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
path: ".claude/agents/harness-engineer.md",
|
|
142
|
+
title: "Harness Engineer Agent",
|
|
143
|
+
agentName: "harness-engineer",
|
|
144
|
+
commentStyle: "html",
|
|
145
|
+
category: "agent-harness-engineer",
|
|
146
|
+
content: renderHarnessEngineerHarnessRules()
|
|
135
147
|
}
|
|
136
148
|
];
|
|
137
149
|
const DURABLE_DOC_TEMPLATES = [
|
package/dist/backend/server.js
CHANGED
|
@@ -79,6 +79,7 @@ export async function createServer(deps, options = {}) {
|
|
|
79
79
|
registerHarnessRoutes(app, {
|
|
80
80
|
projectService: deps.projectService,
|
|
81
81
|
harnessService: deps.harnessService,
|
|
82
|
+
sessionService: deps.sessionService,
|
|
82
83
|
taskService: deps.taskService
|
|
83
84
|
});
|
|
84
85
|
registerTaskRoutes(app, {
|
|
@@ -114,6 +115,7 @@ export async function createServer(deps, options = {}) {
|
|
|
114
115
|
registerTranslationRoutes(app, {
|
|
115
116
|
projectService: deps.projectService,
|
|
116
117
|
taskService: deps.taskService,
|
|
118
|
+
sessionService: deps.sessionService,
|
|
117
119
|
translationService: deps.translationService
|
|
118
120
|
});
|
|
119
121
|
registerGatewayRoutes(app, { gatewayService: deps.gatewayService });
|
|
@@ -165,14 +167,6 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
165
167
|
const registry = createSessionRegistry();
|
|
166
168
|
const artifactService = createArtifactService(fs);
|
|
167
169
|
const projectService = createProjectService({ fs, git, appSettings });
|
|
168
|
-
const harnessService = createHarnessService({
|
|
169
|
-
fs,
|
|
170
|
-
git,
|
|
171
|
-
runtime,
|
|
172
|
-
projectService,
|
|
173
|
-
apiUrl: options.apiUrl,
|
|
174
|
-
runFixedInstaller: createScriptFixedHarnessInstaller(path.join(getAppRoot(), "scripts/install-vcm-harness.mjs"))
|
|
175
|
-
});
|
|
176
170
|
const taskService = createTaskService({ fs, git, artifactService, projectService });
|
|
177
171
|
const sessionService = createSessionService({
|
|
178
172
|
fs,
|
|
@@ -184,6 +178,13 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
184
178
|
taskService,
|
|
185
179
|
apiUrl: options.apiUrl
|
|
186
180
|
});
|
|
181
|
+
const harnessService = createHarnessService({
|
|
182
|
+
fs,
|
|
183
|
+
git,
|
|
184
|
+
runtime,
|
|
185
|
+
harnessEngineerSessions: sessionService,
|
|
186
|
+
runFixedInstaller: createScriptFixedHarnessInstaller(path.join(getAppRoot(), "scripts/install-vcm-harness.mjs"))
|
|
187
|
+
});
|
|
187
188
|
const commandDispatcher = createCommandDispatcher({
|
|
188
189
|
runtime,
|
|
189
190
|
sessionService,
|
|
@@ -262,6 +263,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
262
263
|
translationService,
|
|
263
264
|
appSettings,
|
|
264
265
|
runtime,
|
|
266
|
+
harnessService,
|
|
265
267
|
gatewayService,
|
|
266
268
|
jobGuard: createJobGuardService(),
|
|
267
269
|
translationWorkerService
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isGateReviewerRoleName, isTranslatorToolRoleName, isVcmRoleName } from "../../shared/constants.js";
|
|
1
|
+
import { isGateReviewerRoleName, isHarnessEngineerToolRoleName, isTranslatorToolRoleName, isVcmRoleName } from "../../shared/constants.js";
|
|
2
2
|
import { VcmError } from "../errors.js";
|
|
3
3
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
4
4
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
@@ -44,6 +44,17 @@ export function createClaudeHookService(deps) {
|
|
|
44
44
|
}
|
|
45
45
|
return { project };
|
|
46
46
|
}
|
|
47
|
+
async function getProjectToolHookContext(toolLabel) {
|
|
48
|
+
const project = await deps.projectService.getCurrentProject();
|
|
49
|
+
if (!project) {
|
|
50
|
+
throw new VcmError({
|
|
51
|
+
code: "PROJECT_NOT_CONNECTED",
|
|
52
|
+
message: `Connect a repository before accepting ${toolLabel} hooks.`,
|
|
53
|
+
statusCode: 409
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return { project };
|
|
57
|
+
}
|
|
47
58
|
async function processTranslatorHook(input) {
|
|
48
59
|
const eventName = parseHookEvent(input.event.hook_event_name);
|
|
49
60
|
const context = await getTranslatorHookContext();
|
|
@@ -63,6 +74,29 @@ export function createClaudeHookService(deps) {
|
|
|
63
74
|
dispatchedCount: 0
|
|
64
75
|
};
|
|
65
76
|
}
|
|
77
|
+
async function processHarnessEngineerHook(input) {
|
|
78
|
+
const eventName = parseHookEvent(input.event.hook_event_name);
|
|
79
|
+
const context = await getProjectToolHookContext("Harness Engineer");
|
|
80
|
+
const session = await deps.sessionService.recordProjectHarnessEngineerHookEvent(context.project.repoRoot, {
|
|
81
|
+
eventName,
|
|
82
|
+
sessionId: stringOrUndefined(input.event.session_id),
|
|
83
|
+
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
84
|
+
cwd: stringOrUndefined(input.event.cwd)
|
|
85
|
+
});
|
|
86
|
+
await deps.harnessService?.recordHarnessBootstrapHook(context.project.repoRoot, {
|
|
87
|
+
eventName,
|
|
88
|
+
sessionId: session?.id,
|
|
89
|
+
claudeSessionId: stringOrUndefined(input.event.session_id)
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
ok: true,
|
|
93
|
+
eventName,
|
|
94
|
+
taskSlug: input.taskSlug,
|
|
95
|
+
role: input.role,
|
|
96
|
+
sessionUpdated: Boolean(session),
|
|
97
|
+
dispatchedCount: 0
|
|
98
|
+
};
|
|
99
|
+
}
|
|
66
100
|
async function resolveHookTaskSlug(repoRoot, input) {
|
|
67
101
|
if (!isGateReviewerRoleName(input.role)) {
|
|
68
102
|
return input.taskSlug;
|
|
@@ -337,7 +371,7 @@ export function createClaudeHookService(deps) {
|
|
|
337
371
|
}
|
|
338
372
|
async function handlePermissionRequestHook(input) {
|
|
339
373
|
if (!isVcmRoleName(input.role)) {
|
|
340
|
-
if (isTranslatorToolRoleName(input.role)) {
|
|
374
|
+
if (isTranslatorToolRoleName(input.role) || isHarnessEngineerToolRoleName(input.role)) {
|
|
341
375
|
const preferences = await deps.appSettings.getPreferences();
|
|
342
376
|
if (preferences.permissionRequestMode !== "allowAll") {
|
|
343
377
|
return undefined;
|
|
@@ -384,6 +418,9 @@ export function createClaudeHookService(deps) {
|
|
|
384
418
|
if (isTranslatorToolRoleName(input.role)) {
|
|
385
419
|
return processTranslatorHook(input);
|
|
386
420
|
}
|
|
421
|
+
if (isHarnessEngineerToolRoleName(input.role)) {
|
|
422
|
+
return processHarnessEngineerHook(input);
|
|
423
|
+
}
|
|
387
424
|
const eventName = parseHookEvent(input.event.hook_event_name);
|
|
388
425
|
if (eventName === "UserPromptSubmit") {
|
|
389
426
|
return handleUserPromptSubmitHook(input);
|
|
@@ -402,6 +439,9 @@ export function createClaudeHookService(deps) {
|
|
|
402
439
|
if (isTranslatorToolRoleName(input.role)) {
|
|
403
440
|
return processTranslatorHook(input);
|
|
404
441
|
}
|
|
442
|
+
if (isHarnessEngineerToolRoleName(input.role)) {
|
|
443
|
+
return processHarnessEngineerHook(input);
|
|
444
|
+
}
|
|
405
445
|
return processStopHook(input, { allowBlock: true });
|
|
406
446
|
},
|
|
407
447
|
handlePermissionRequestHook
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
2
|
+
const HARNESS_REVISION_PATH = ".ai/vcm/harness/revision.json";
|
|
3
|
+
export async function readHarnessRevisionState(fs, repoRoot) {
|
|
4
|
+
const revisionPath = resolveRepoPath(repoRoot, HARNESS_REVISION_PATH);
|
|
5
|
+
if (!await fs.pathExists(revisionPath)) {
|
|
6
|
+
return {
|
|
7
|
+
version: 1,
|
|
8
|
+
revision: 0
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
const payload = await fs.readJson(revisionPath);
|
|
12
|
+
return {
|
|
13
|
+
version: 1,
|
|
14
|
+
revision: normalizeRevision(payload.revision),
|
|
15
|
+
updatedAt: typeof payload.updatedAt === "string" ? payload.updatedAt : undefined
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export async function bumpHarnessRevision(fs, repoRoot, updatedAt) {
|
|
19
|
+
const current = await readHarnessRevisionState(fs, repoRoot);
|
|
20
|
+
const next = {
|
|
21
|
+
version: 1,
|
|
22
|
+
revision: current.revision + 1,
|
|
23
|
+
updatedAt
|
|
24
|
+
};
|
|
25
|
+
await fs.writeJsonAtomic(resolveRepoPath(repoRoot, HARNESS_REVISION_PATH), next);
|
|
26
|
+
return next;
|
|
27
|
+
}
|
|
28
|
+
function normalizeRevision(value) {
|
|
29
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0
|
|
30
|
+
? value
|
|
31
|
+
: 0;
|
|
32
|
+
}
|