vibe-coding-master 0.4.14 → 0.4.16
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 +2 -1
- package/dist/backend/adapters/claude-adapter.js +2 -2
- package/dist/backend/api/harness-routes.js +33 -0
- package/dist/backend/cli/install-vcm-harness.js +8 -0
- package/dist/backend/server.js +9 -0
- package/dist/backend/services/app-settings-service.js +1 -1
- package/dist/backend/services/claude-hook-service.js +1 -0
- package/dist/backend/services/harness-feedback-service.js +406 -0
- package/dist/backend/services/harness-service.js +9 -0
- package/dist/backend/services/session-service.js +8 -0
- package/dist/backend/templates/harness/claude-root.js +7 -0
- package/dist/backend/templates/harness/vcm-report-harness-issue-skill.js +45 -0
- package/dist/shared/types/app-settings.js +1 -1
- package/dist/shared/types/session.js +17 -0
- package/dist-frontend/assets/index-D0LBjl2L.css +32 -0
- package/dist-frontend/assets/index-Dg_NXKA5.js +95 -0
- package/dist-frontend/index.html +2 -2
- package/docs/product-design.md +2 -1
- package/package.json +1 -1
- package/dist-frontend/assets/index-C40of4Lv.css +0 -32
- package/dist-frontend/assets/index-T-lTlw_o.js +0 -95
package/README.md
CHANGED
|
@@ -24,8 +24,9 @@ When Gate Review Gates are enabled for a task, or when a Gate Reviewer session a
|
|
|
24
24
|
- Optional Gate Reviewer VCM flow role when any Gate Review Gate is enabled.
|
|
25
25
|
- Role session recovery through persisted Claude session ids and `claude --resume`.
|
|
26
26
|
- Permission mode selection before start, resume, or restart:
|
|
27
|
-
- `default`
|
|
28
27
|
- `bypassPermissions`
|
|
28
|
+
- `plan`
|
|
29
|
+
- `default`
|
|
29
30
|
- PM-mediated role messaging through VCM-dispatched route files.
|
|
30
31
|
- Manual and automatic orchestration modes.
|
|
31
32
|
- Two-stage VCM harness setup: deterministic fixed install plus AI-assisted bootstrap.
|
|
@@ -29,8 +29,8 @@ export function createClaudeAdapter(runner) {
|
|
|
29
29
|
else if (effort !== "default") {
|
|
30
30
|
args.push("--effort", effort);
|
|
31
31
|
}
|
|
32
|
-
if (permissionMode
|
|
33
|
-
args.push("--permission-mode",
|
|
32
|
+
if (permissionMode !== "default") {
|
|
33
|
+
args.push("--permission-mode", permissionMode);
|
|
34
34
|
}
|
|
35
35
|
return {
|
|
36
36
|
command,
|
|
@@ -52,10 +52,12 @@ export function registerHarnessRoutes(app, deps) {
|
|
|
52
52
|
});
|
|
53
53
|
app.post("/api/projects/harness/bootstrap/start", async (request) => {
|
|
54
54
|
const { project, task } = await requireHarnessTaskContext(deps, request.body?.taskSlug);
|
|
55
|
+
await deps.harnessFeedbackService.assertHarnessEngineerAvailable(project.repoRoot);
|
|
55
56
|
return deps.harnessService.startHarnessBootstrap(project.repoRoot, task.worktreePath, request.body ?? {});
|
|
56
57
|
});
|
|
57
58
|
app.post("/api/projects/harness/bootstrap/restart", async (request) => {
|
|
58
59
|
const { project, task } = await requireHarnessTaskContext(deps, request.body?.taskSlug);
|
|
60
|
+
await deps.harnessFeedbackService.assertHarnessEngineerAvailable(project.repoRoot);
|
|
59
61
|
return deps.harnessService.restartHarnessBootstrap(project.repoRoot, task.worktreePath, request.body ?? {});
|
|
60
62
|
});
|
|
61
63
|
app.post("/api/projects/harness/bootstrap/stop", async () => {
|
|
@@ -64,6 +66,7 @@ export function registerHarnessRoutes(app, deps) {
|
|
|
64
66
|
});
|
|
65
67
|
app.post("/api/projects/harness/bootstrap/run", async (request) => {
|
|
66
68
|
const { project, task } = await requireHarnessTaskContext(deps, request.body?.taskSlug);
|
|
69
|
+
await deps.harnessFeedbackService.assertHarnessEngineerAvailable(project.repoRoot);
|
|
67
70
|
return deps.harnessService.runHarnessBootstrap(project.repoRoot, task.worktreePath);
|
|
68
71
|
});
|
|
69
72
|
app.get("/api/projects/harness/engineer/session", async () => {
|
|
@@ -94,6 +97,28 @@ export function registerHarnessRoutes(app, deps) {
|
|
|
94
97
|
const project = await requireCurrentProject(deps.projectService);
|
|
95
98
|
return deps.sessionService.notifyProjectHarnessEngineerHarnessUpdated(project.repoRoot);
|
|
96
99
|
});
|
|
100
|
+
app.get("/api/projects/harness/feedback", async (request) => {
|
|
101
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
102
|
+
const taskSlug = await normalizeOptionalTaskSlug(deps, project.repoRoot, request.query.taskSlug);
|
|
103
|
+
return deps.harnessFeedbackService.getState(project.repoRoot, taskSlug);
|
|
104
|
+
});
|
|
105
|
+
app.post("/api/projects/harness/feedback/decision", async (request) => {
|
|
106
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
107
|
+
const action = request.body?.action;
|
|
108
|
+
if (action !== "approve" && action !== "reject" && action !== "comment") {
|
|
109
|
+
throw new VcmError({
|
|
110
|
+
code: "HARNESS_FEEDBACK_DECISION_INVALID",
|
|
111
|
+
message: "Harness feedback decision action is invalid.",
|
|
112
|
+
statusCode: 400
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const taskSlug = await normalizeOptionalTaskSlug(deps, project.repoRoot, request.body?.taskSlug);
|
|
116
|
+
return deps.harnessFeedbackService.decide(project.repoRoot, {
|
|
117
|
+
action,
|
|
118
|
+
taskSlug,
|
|
119
|
+
comment: typeof request.body?.comment === "string" ? request.body.comment : undefined
|
|
120
|
+
});
|
|
121
|
+
});
|
|
97
122
|
}
|
|
98
123
|
function degradedHarnessStatus(error) {
|
|
99
124
|
return {
|
|
@@ -151,3 +176,11 @@ async function requireHarnessTaskContext(deps, taskSlug) {
|
|
|
151
176
|
const task = await deps.taskService.loadTask(project.repoRoot, normalizedTaskSlug);
|
|
152
177
|
return { project, task };
|
|
153
178
|
}
|
|
179
|
+
async function normalizeOptionalTaskSlug(deps, repoRoot, taskSlug) {
|
|
180
|
+
const normalizedTaskSlug = taskSlug?.trim();
|
|
181
|
+
if (!normalizedTaskSlug) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
await deps.taskService.loadTask(repoRoot, normalizedTaskSlug);
|
|
185
|
+
return normalizedTaskSlug;
|
|
186
|
+
}
|
|
@@ -16,6 +16,7 @@ import { renderReviewerHarnessRules } from "../templates/harness/reviewer-agent.
|
|
|
16
16
|
import { renderVcmFinalAcceptanceSkillRules } from "../templates/harness/vcm-final-acceptance-skill.js";
|
|
17
17
|
import { renderVcmHarnessBootstrapSkillRules } from "../templates/harness/vcm-harness-bootstrap-skill.js";
|
|
18
18
|
import { renderVcmLongRunningValidationSkillRules } from "../templates/harness/vcm-long-running-validation-skill.js";
|
|
19
|
+
import { renderVcmReportHarnessIssueSkillRules } from "../templates/harness/vcm-report-harness-issue-skill.js";
|
|
19
20
|
import { renderVcmRouteMessageSkillRules } from "../templates/harness/vcm-route-message-skill.js";
|
|
20
21
|
const HARNESS_VERSION = "0.3.0-fixed";
|
|
21
22
|
const CLI_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -203,6 +204,12 @@ const WHOLE_FILES = [
|
|
|
203
204
|
mode: 0o644,
|
|
204
205
|
content: renderSkillFile("VCM Gate Review Skill", "vcm-gate-review", "Use when project-manager reaches a Gate Review trigger or receives a VCM Gate Review callback.", renderVcmGateReviewSkillRules())
|
|
205
206
|
},
|
|
207
|
+
{
|
|
208
|
+
path: ".claude/skills/vcm-report-harness-issue/SKILL.md",
|
|
209
|
+
category: "skill",
|
|
210
|
+
mode: 0o644,
|
|
211
|
+
content: renderSkillFile("VCM Report Harness Issue Skill", "vcm-report-harness-issue", "Use when a VCM role notices a reusable harness problem and needs to record feedback for Harness Engineer review.", renderVcmReportHarnessIssueSkillRules())
|
|
212
|
+
},
|
|
206
213
|
{
|
|
207
214
|
path: ".ai/tools/request-gate-review",
|
|
208
215
|
category: "runtime-tool",
|
|
@@ -422,6 +429,7 @@ function fixedDirectories() {
|
|
|
422
429
|
".claude/skills/vcm-long-running-validation/",
|
|
423
430
|
".claude/skills/vcm-route-message/",
|
|
424
431
|
".claude/skills/vcm-gate-review/",
|
|
432
|
+
".claude/skills/vcm-report-harness-issue/",
|
|
425
433
|
".ai/vcm/translations/",
|
|
426
434
|
".ai/vcm/gate-reviews/",
|
|
427
435
|
".ai/tools/",
|
package/dist/backend/server.js
CHANGED
|
@@ -11,6 +11,7 @@ import { createGitAdapter } from "./adapters/git-adapter.js";
|
|
|
11
11
|
import { createAppSettingsService } from "./services/app-settings-service.js";
|
|
12
12
|
import { createClaudeTranscriptService } from "./services/claude-transcript-service.js";
|
|
13
13
|
import { createGateReviewService } from "./services/gate-review-service.js";
|
|
14
|
+
import { createHarnessFeedbackService } from "./services/harness-feedback-service.js";
|
|
14
15
|
import { createTranslationWorkerService } from "./services/translation-worker-service.js";
|
|
15
16
|
import { createHarnessService, createScriptFixedHarnessInstaller } from "./services/harness-service.js";
|
|
16
17
|
import { createNodeFileSystemAdapter } from "./adapters/filesystem.js";
|
|
@@ -79,6 +80,7 @@ export async function createServer(deps, options = {}) {
|
|
|
79
80
|
registerHarnessRoutes(app, {
|
|
80
81
|
projectService: deps.projectService,
|
|
81
82
|
harnessService: deps.harnessService,
|
|
83
|
+
harnessFeedbackService: deps.harnessFeedbackService,
|
|
82
84
|
sessionService: deps.sessionService,
|
|
83
85
|
taskService: deps.taskService
|
|
84
86
|
});
|
|
@@ -185,6 +187,11 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
185
187
|
harnessEngineerSessions: sessionService,
|
|
186
188
|
runFixedInstaller: createScriptFixedHarnessInstaller(path.join(getAppRoot(), "scripts/install-vcm-harness.mjs"))
|
|
187
189
|
});
|
|
190
|
+
const harnessFeedbackService = createHarnessFeedbackService({
|
|
191
|
+
fs,
|
|
192
|
+
runtime,
|
|
193
|
+
sessionService
|
|
194
|
+
});
|
|
188
195
|
const commandDispatcher = createCommandDispatcher({
|
|
189
196
|
runtime,
|
|
190
197
|
sessionService,
|
|
@@ -264,6 +271,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
264
271
|
appSettings,
|
|
265
272
|
runtime,
|
|
266
273
|
harnessService,
|
|
274
|
+
harnessFeedbackService,
|
|
267
275
|
gatewayService,
|
|
268
276
|
jobGuard: createJobGuardService(),
|
|
269
277
|
translationWorkerService
|
|
@@ -281,6 +289,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
281
289
|
sessionService,
|
|
282
290
|
artifactService,
|
|
283
291
|
harnessService,
|
|
292
|
+
harnessFeedbackService,
|
|
284
293
|
commandDispatcher,
|
|
285
294
|
claudeHookService,
|
|
286
295
|
messageService,
|
|
@@ -283,7 +283,7 @@ function normalizeRoleLaunchTemplateEntry(input, fallback) {
|
|
|
283
283
|
};
|
|
284
284
|
}
|
|
285
285
|
function normalizeClaudePermissionMode(input, fallback) {
|
|
286
|
-
if (input === "bypassPermissions" || input === "default") {
|
|
286
|
+
if (input === "bypassPermissions" || input === "plan" || input === "default") {
|
|
287
287
|
return input;
|
|
288
288
|
}
|
|
289
289
|
return fallback;
|
|
@@ -88,6 +88,7 @@ export function createClaudeHookService(deps) {
|
|
|
88
88
|
sessionId: session?.id,
|
|
89
89
|
claudeSessionId: stringOrUndefined(input.event.session_id)
|
|
90
90
|
});
|
|
91
|
+
await deps.harnessFeedbackService?.recordHarnessEngineerHook(context.project.repoRoot, eventName);
|
|
91
92
|
return {
|
|
92
93
|
ok: true,
|
|
93
94
|
eventName,
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { resolveRepoPath, toRepoRelativePath } from "../adapters/filesystem.js";
|
|
3
|
+
import { VcmError } from "../errors.js";
|
|
4
|
+
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
5
|
+
const FEEDBACK_ROOT = ".ai/vcm/harness-feedback";
|
|
6
|
+
const PENDING_DIR = `${FEEDBACK_ROOT}/pending`;
|
|
7
|
+
const ACTIVE_DIR = `${FEEDBACK_ROOT}/active`;
|
|
8
|
+
const COMPLETED_DIR = `${FEEDBACK_ROOT}/completed`;
|
|
9
|
+
const STATE_PATH = `${FEEDBACK_ROOT}/state.json`;
|
|
10
|
+
export function createHarnessFeedbackService(deps) {
|
|
11
|
+
const now = deps.now ?? (() => new Date().toISOString());
|
|
12
|
+
async function getState(repoRoot, activeTaskSlug) {
|
|
13
|
+
await maybeDispatchNext(repoRoot, activeTaskSlug);
|
|
14
|
+
return buildStateReport(repoRoot);
|
|
15
|
+
}
|
|
16
|
+
async function decide(repoRoot, input) {
|
|
17
|
+
const state = await loadStoredState(repoRoot);
|
|
18
|
+
if (!state || state.status !== "awaiting_user_approval") {
|
|
19
|
+
throw new VcmError({
|
|
20
|
+
code: "HARNESS_FEEDBACK_NOT_AWAITING_APPROVAL",
|
|
21
|
+
message: "There is no Harness feedback waiting for user approval.",
|
|
22
|
+
statusCode: 409
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
if (input.action === "reject") {
|
|
26
|
+
await completeActive(repoRoot, state, "rejected", input.comment);
|
|
27
|
+
await clearStoredState(repoRoot);
|
|
28
|
+
return getState(repoRoot, input.taskSlug);
|
|
29
|
+
}
|
|
30
|
+
const taskSlug = input.taskSlug?.trim();
|
|
31
|
+
if (!taskSlug) {
|
|
32
|
+
throw new VcmError({
|
|
33
|
+
code: "HARNESS_FEEDBACK_TASK_REQUIRED",
|
|
34
|
+
message: "Select an active task before asking Harness Engineer to continue feedback work.",
|
|
35
|
+
statusCode: 409
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (input.action === "comment") {
|
|
39
|
+
const session = await ensureIdleHarnessEngineer(repoRoot, taskSlug);
|
|
40
|
+
const timestamp = now();
|
|
41
|
+
const nextState = {
|
|
42
|
+
...state,
|
|
43
|
+
status: "analyzing",
|
|
44
|
+
active: {
|
|
45
|
+
...state.active,
|
|
46
|
+
updatedAt: timestamp,
|
|
47
|
+
lastPromptAt: timestamp
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
await persistStoredState(repoRoot, nextState);
|
|
51
|
+
await submitTerminalInput(deps.runtime, session.id, buildFeedbackCommentPrompt(repoRoot, nextState.active, input.comment ?? ""));
|
|
52
|
+
return buildStateReport(repoRoot);
|
|
53
|
+
}
|
|
54
|
+
const session = await ensureIdleHarnessEngineer(repoRoot, taskSlug);
|
|
55
|
+
const timestamp = now();
|
|
56
|
+
const nextState = {
|
|
57
|
+
...state,
|
|
58
|
+
status: "applying",
|
|
59
|
+
active: {
|
|
60
|
+
...state.active,
|
|
61
|
+
updatedAt: timestamp,
|
|
62
|
+
lastPromptAt: timestamp
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
await persistStoredState(repoRoot, nextState);
|
|
66
|
+
await submitTerminalInput(deps.runtime, session.id, buildFeedbackApplyPrompt(repoRoot, nextState.active, input.comment ?? ""));
|
|
67
|
+
return buildStateReport(repoRoot);
|
|
68
|
+
}
|
|
69
|
+
async function recordHarnessEngineerHook(repoRoot, eventName) {
|
|
70
|
+
if (eventName !== "Stop" && eventName !== "StopFailure") {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const state = await loadStoredState(repoRoot);
|
|
74
|
+
if (!state) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const timestamp = now();
|
|
78
|
+
if (state.status === "analyzing") {
|
|
79
|
+
await persistStoredState(repoRoot, {
|
|
80
|
+
...state,
|
|
81
|
+
status: "awaiting_user_approval",
|
|
82
|
+
active: {
|
|
83
|
+
...state.active,
|
|
84
|
+
updatedAt: timestamp
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (state.status === "applying") {
|
|
90
|
+
await completeActive(repoRoot, {
|
|
91
|
+
...state,
|
|
92
|
+
active: {
|
|
93
|
+
...state.active,
|
|
94
|
+
updatedAt: timestamp
|
|
95
|
+
}
|
|
96
|
+
}, "applied");
|
|
97
|
+
await clearStoredState(repoRoot);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function assertHarnessEngineerAvailable(repoRoot) {
|
|
101
|
+
const state = await loadStoredState(repoRoot);
|
|
102
|
+
if (!state) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
throw new VcmError({
|
|
106
|
+
code: "HARNESS_ENGINEER_FEEDBACK_ACTIVE",
|
|
107
|
+
message: "Harness Engineer is reserved for an active Harness feedback item.",
|
|
108
|
+
statusCode: 409,
|
|
109
|
+
hint: state.status === "awaiting_user_approval"
|
|
110
|
+
? "Review, approve, comment, or reject the current Harness feedback before starting another Harness Engineer task."
|
|
111
|
+
: "Wait for the current Harness feedback turn to finish before starting another Harness Engineer task."
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
async function maybeDispatchNext(repoRoot, activeTaskSlug) {
|
|
115
|
+
const state = await loadStoredState(repoRoot);
|
|
116
|
+
if (state) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const pending = await listPendingFeedback(repoRoot);
|
|
120
|
+
const next = pending[0];
|
|
121
|
+
const taskSlug = activeTaskSlug?.trim();
|
|
122
|
+
if (!next || !taskSlug) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const session = await getIdleHarnessEngineer(repoRoot, taskSlug);
|
|
126
|
+
if (!session) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const timestamp = now();
|
|
130
|
+
const analysisPath = `${ACTIVE_DIR}/${next.id}/analysis.md`;
|
|
131
|
+
const applyReportPath = `${ACTIVE_DIR}/${next.id}/apply-report.md`;
|
|
132
|
+
const active = {
|
|
133
|
+
...next,
|
|
134
|
+
feedbackPath: next.path,
|
|
135
|
+
analysisPath,
|
|
136
|
+
applyReportPath,
|
|
137
|
+
startedAt: timestamp,
|
|
138
|
+
updatedAt: timestamp,
|
|
139
|
+
lastPromptAt: timestamp
|
|
140
|
+
};
|
|
141
|
+
const nextState = {
|
|
142
|
+
version: 1,
|
|
143
|
+
status: "analyzing",
|
|
144
|
+
active
|
|
145
|
+
};
|
|
146
|
+
await persistStoredState(repoRoot, nextState);
|
|
147
|
+
await deps.fs.ensureDir(resolveRepoPath(repoRoot, path.posix.dirname(analysisPath)));
|
|
148
|
+
await submitTerminalInput(deps.runtime, session.id, await buildFeedbackAnalysisPrompt(repoRoot, active));
|
|
149
|
+
}
|
|
150
|
+
async function getIdleHarnessEngineer(repoRoot, taskSlug) {
|
|
151
|
+
const existing = await deps.sessionService.getProjectHarnessEngineerSession(repoRoot);
|
|
152
|
+
if (existing?.status === "running" && existing.activityStatus === "running") {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
const session = await deps.sessionService.ensureProjectHarnessEngineerSession(repoRoot, {
|
|
156
|
+
taskSlug,
|
|
157
|
+
cols: 120,
|
|
158
|
+
rows: 32
|
|
159
|
+
});
|
|
160
|
+
if (session.status !== "running" || session.activityStatus === "running") {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
if (!deps.runtime.getSession(session.id)) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
return session;
|
|
167
|
+
}
|
|
168
|
+
async function ensureIdleHarnessEngineer(repoRoot, taskSlug) {
|
|
169
|
+
const session = await getIdleHarnessEngineer(repoRoot, taskSlug);
|
|
170
|
+
if (!session) {
|
|
171
|
+
throw new VcmError({
|
|
172
|
+
code: "HARNESS_ENGINEER_BUSY",
|
|
173
|
+
message: "Harness Engineer is busy or unavailable.",
|
|
174
|
+
statusCode: 409,
|
|
175
|
+
hint: "Wait for the current Harness Engineer turn to finish, then retry."
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return session;
|
|
179
|
+
}
|
|
180
|
+
async function buildStateReport(repoRoot) {
|
|
181
|
+
const [state, pending] = await Promise.all([
|
|
182
|
+
loadStoredState(repoRoot),
|
|
183
|
+
listPendingFeedback(repoRoot)
|
|
184
|
+
]);
|
|
185
|
+
if (!state) {
|
|
186
|
+
return {
|
|
187
|
+
version: 1,
|
|
188
|
+
status: pending.length > 0 ? "queued" : "idle",
|
|
189
|
+
queuedCount: pending.length,
|
|
190
|
+
pending,
|
|
191
|
+
warnings: []
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const active = await readActiveItem(repoRoot, state);
|
|
195
|
+
return {
|
|
196
|
+
version: 1,
|
|
197
|
+
status: state.status,
|
|
198
|
+
queuedCount: Math.max(0, pending.length - (pending.some((item) => item.id === state.active.id) ? 1 : 0)),
|
|
199
|
+
pending: pending.filter((item) => item.id !== state.active.id),
|
|
200
|
+
active,
|
|
201
|
+
warnings: []
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
async function readActiveItem(repoRoot, state) {
|
|
205
|
+
const feedbackContent = await readOptionalText(repoRoot, state.active.feedbackPath) ?? "";
|
|
206
|
+
const analysisContent = await readOptionalText(repoRoot, state.active.analysisPath);
|
|
207
|
+
const applyReportContent = await readOptionalText(repoRoot, state.active.applyReportPath);
|
|
208
|
+
return {
|
|
209
|
+
id: state.active.id,
|
|
210
|
+
title: state.active.title,
|
|
211
|
+
path: state.active.path,
|
|
212
|
+
reporterRole: state.active.reporterRole,
|
|
213
|
+
taskSlug: state.active.taskSlug,
|
|
214
|
+
summary: state.active.summary,
|
|
215
|
+
status: state.status,
|
|
216
|
+
startedAt: state.active.startedAt,
|
|
217
|
+
updatedAt: state.active.updatedAt,
|
|
218
|
+
feedbackContent,
|
|
219
|
+
analysisPath: state.active.analysisPath,
|
|
220
|
+
analysisContent,
|
|
221
|
+
applyReportPath: state.active.applyReportPath,
|
|
222
|
+
applyReportContent
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
async function listPendingFeedback(repoRoot) {
|
|
226
|
+
const pendingDir = resolveRepoPath(repoRoot, PENDING_DIR);
|
|
227
|
+
if (!(await deps.fs.pathExists(pendingDir))) {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
const names = await deps.fs.readDir(pendingDir);
|
|
231
|
+
const markdownFiles = names
|
|
232
|
+
.filter((name) => name.endsWith(".md") && !name.includes("/") && !name.includes("\\"))
|
|
233
|
+
.sort();
|
|
234
|
+
const items = await Promise.all(markdownFiles.map(async (name) => {
|
|
235
|
+
const relativePath = `${PENDING_DIR}/${name}`;
|
|
236
|
+
const content = await readOptionalText(repoRoot, relativePath) ?? "";
|
|
237
|
+
return parseFeedbackItem(relativePath, content);
|
|
238
|
+
}));
|
|
239
|
+
return items;
|
|
240
|
+
}
|
|
241
|
+
function parseFeedbackItem(relativePath, content) {
|
|
242
|
+
const id = sanitizeFeedbackId(path.posix.basename(relativePath, ".md"));
|
|
243
|
+
const metadata = parseSimpleMetadata(content);
|
|
244
|
+
const title = firstHeading(content)
|
|
245
|
+
?? metadata.summary
|
|
246
|
+
?? metadata["observed problem"]
|
|
247
|
+
?? id;
|
|
248
|
+
return {
|
|
249
|
+
id,
|
|
250
|
+
title: compactLine(title),
|
|
251
|
+
path: relativePath,
|
|
252
|
+
reporterRole: metadata["reporter role"] ?? metadata.reporter,
|
|
253
|
+
taskSlug: metadata["task slug"] ?? metadata.task,
|
|
254
|
+
summary: metadata.summary
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
async function buildFeedbackAnalysisPrompt(repoRoot, active) {
|
|
258
|
+
const feedback = await readOptionalText(repoRoot, active.feedbackPath) ?? "";
|
|
259
|
+
return [
|
|
260
|
+
"[VCM Harness Feedback Analysis]",
|
|
261
|
+
"",
|
|
262
|
+
"Analyze this harness feedback. Do not edit files yet.",
|
|
263
|
+
"",
|
|
264
|
+
`Base repository root: ${repoRoot}`,
|
|
265
|
+
`Feedback file: ${resolveRepoPath(repoRoot, active.feedbackPath)}`,
|
|
266
|
+
`Result path: ${resolveRepoPath(repoRoot, active.analysisPath)}`,
|
|
267
|
+
"",
|
|
268
|
+
"Rules:",
|
|
269
|
+
"- Decide whether the reported issue is a real reusable harness problem.",
|
|
270
|
+
"- Inspect relevant harness files before judging.",
|
|
271
|
+
"- If the issue is not real or does not need a harness change, say so in the result file.",
|
|
272
|
+
"- If it should be fixed, write a short proposal with affected files, proposed diff shape, risks, validation, and whether a VCM GitHub issue is needed.",
|
|
273
|
+
"- Do not edit harness files or product source during this analysis turn.",
|
|
274
|
+
"- End your turn after writing the result file.",
|
|
275
|
+
"",
|
|
276
|
+
"<HARNESS_FEEDBACK>",
|
|
277
|
+
feedback.trimEnd(),
|
|
278
|
+
"</HARNESS_FEEDBACK>"
|
|
279
|
+
].join("\n");
|
|
280
|
+
}
|
|
281
|
+
function buildFeedbackCommentPrompt(repoRoot, active, comment) {
|
|
282
|
+
return [
|
|
283
|
+
"[VCM Harness Feedback Revision]",
|
|
284
|
+
"",
|
|
285
|
+
"The user reviewed your harness feedback analysis and added comments.",
|
|
286
|
+
"",
|
|
287
|
+
`Feedback file: ${resolveRepoPath(repoRoot, active.feedbackPath)}`,
|
|
288
|
+
`Current analysis path: ${resolveRepoPath(repoRoot, active.analysisPath)}`,
|
|
289
|
+
`Rewrite the analysis result at: ${resolveRepoPath(repoRoot, active.analysisPath)}`,
|
|
290
|
+
"",
|
|
291
|
+
"Rules:",
|
|
292
|
+
"- Do not edit harness files yet.",
|
|
293
|
+
"- Address the user's comments and keep the proposal concise.",
|
|
294
|
+
"- End your turn after updating the analysis result file.",
|
|
295
|
+
"",
|
|
296
|
+
"<USER_COMMENT>",
|
|
297
|
+
comment.trim(),
|
|
298
|
+
"</USER_COMMENT>"
|
|
299
|
+
].join("\n");
|
|
300
|
+
}
|
|
301
|
+
function buildFeedbackApplyPrompt(repoRoot, active, comment) {
|
|
302
|
+
return [
|
|
303
|
+
"[VCM Harness Feedback Approved]",
|
|
304
|
+
"",
|
|
305
|
+
"The user approved this harness improvement. Apply only the approved harness changes.",
|
|
306
|
+
"",
|
|
307
|
+
`Base repository root: ${repoRoot}`,
|
|
308
|
+
`Feedback file: ${resolveRepoPath(repoRoot, active.feedbackPath)}`,
|
|
309
|
+
`Approved analysis path: ${resolveRepoPath(repoRoot, active.analysisPath)}`,
|
|
310
|
+
`Write completion report to: ${resolveRepoPath(repoRoot, active.applyReportPath)}`,
|
|
311
|
+
"",
|
|
312
|
+
"Rules:",
|
|
313
|
+
"- Work in the active task worktree.",
|
|
314
|
+
"- Edit only harness files and project harness docs that are necessary for the approved change.",
|
|
315
|
+
"- Do not edit product source code.",
|
|
316
|
+
"- Do not overwrite VCM fixed managed blocks; draft an issue instead if a fixed template is wrong.",
|
|
317
|
+
"- Stage the harness changes and create a commit yourself.",
|
|
318
|
+
"- Write the completion report with files changed, commit id if available, validation run, and any follow-up.",
|
|
319
|
+
"- End your turn after the report is written.",
|
|
320
|
+
...(comment.trim()
|
|
321
|
+
? ["", "<USER_APPROVAL_COMMENT>", comment.trim(), "</USER_APPROVAL_COMMENT>"]
|
|
322
|
+
: [])
|
|
323
|
+
].join("\n");
|
|
324
|
+
}
|
|
325
|
+
async function completeActive(repoRoot, state, outcome, comment = "") {
|
|
326
|
+
const completedDir = `${COMPLETED_DIR}/${state.active.id}`;
|
|
327
|
+
await deps.fs.ensureDir(resolveRepoPath(repoRoot, completedDir));
|
|
328
|
+
const feedbackContent = await readOptionalText(repoRoot, state.active.feedbackPath);
|
|
329
|
+
const analysisContent = await readOptionalText(repoRoot, state.active.analysisPath);
|
|
330
|
+
const applyReportContent = await readOptionalText(repoRoot, state.active.applyReportPath);
|
|
331
|
+
if (feedbackContent !== undefined) {
|
|
332
|
+
await deps.fs.writeText(resolveRepoPath(repoRoot, `${completedDir}/feedback.md`), feedbackContent);
|
|
333
|
+
}
|
|
334
|
+
if (analysisContent !== undefined) {
|
|
335
|
+
await deps.fs.writeText(resolveRepoPath(repoRoot, `${completedDir}/analysis.md`), analysisContent);
|
|
336
|
+
}
|
|
337
|
+
if (applyReportContent !== undefined) {
|
|
338
|
+
await deps.fs.writeText(resolveRepoPath(repoRoot, `${completedDir}/apply-report.md`), applyReportContent);
|
|
339
|
+
}
|
|
340
|
+
await deps.fs.writeJsonAtomic(resolveRepoPath(repoRoot, `${completedDir}/decision.json`), {
|
|
341
|
+
version: 1,
|
|
342
|
+
id: state.active.id,
|
|
343
|
+
title: state.active.title,
|
|
344
|
+
outcome,
|
|
345
|
+
comment,
|
|
346
|
+
completedAt: now()
|
|
347
|
+
});
|
|
348
|
+
await deps.fs.removePath?.(resolveRepoPath(repoRoot, state.active.feedbackPath), { force: true });
|
|
349
|
+
await deps.fs.removePath?.(resolveRepoPath(repoRoot, path.posix.dirname(state.active.analysisPath)), { recursive: true, force: true });
|
|
350
|
+
}
|
|
351
|
+
async function loadStoredState(repoRoot) {
|
|
352
|
+
const statePath = resolveRepoPath(repoRoot, STATE_PATH);
|
|
353
|
+
if (!(await deps.fs.pathExists(statePath))) {
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
const state = await deps.fs.readJson(statePath);
|
|
357
|
+
if (state?.version !== 1 || !state.active?.id || !state.status) {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
return state;
|
|
361
|
+
}
|
|
362
|
+
async function persistStoredState(repoRoot, state) {
|
|
363
|
+
await deps.fs.writeJsonAtomic(resolveRepoPath(repoRoot, STATE_PATH), state);
|
|
364
|
+
}
|
|
365
|
+
async function clearStoredState(repoRoot) {
|
|
366
|
+
await deps.fs.removePath?.(resolveRepoPath(repoRoot, STATE_PATH), { force: true });
|
|
367
|
+
}
|
|
368
|
+
async function readOptionalText(repoRoot, relativePath) {
|
|
369
|
+
const absolutePath = resolveRepoPath(repoRoot, relativePath);
|
|
370
|
+
if (!(await deps.fs.pathExists(absolutePath))) {
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
return deps.fs.readText(absolutePath);
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
getState,
|
|
377
|
+
decide,
|
|
378
|
+
recordHarnessEngineerHook,
|
|
379
|
+
assertHarnessEngineerAvailable
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
function parseSimpleMetadata(content) {
|
|
383
|
+
const result = {};
|
|
384
|
+
for (const line of content.split(/\r?\n/).slice(0, 80)) {
|
|
385
|
+
const match = /^[-*]?\s*([A-Za-z][A-Za-z -]{1,40})\s*:\s*(.+)$/.exec(line.trim());
|
|
386
|
+
if (!match) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
result[match[1].trim().toLowerCase()] = match[2].trim();
|
|
390
|
+
}
|
|
391
|
+
return result;
|
|
392
|
+
}
|
|
393
|
+
function firstHeading(content) {
|
|
394
|
+
const heading = content.split(/\r?\n/).find((line) => /^#{1,3}\s+\S/.test(line));
|
|
395
|
+
return heading?.replace(/^#{1,3}\s+/, "").trim();
|
|
396
|
+
}
|
|
397
|
+
function compactLine(value) {
|
|
398
|
+
return value.replace(/\s+/g, " ").trim().slice(0, 160);
|
|
399
|
+
}
|
|
400
|
+
function sanitizeFeedbackId(value) {
|
|
401
|
+
const sanitized = value.replace(/[^A-Za-z0-9._-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
402
|
+
return sanitized || "feedback";
|
|
403
|
+
}
|
|
404
|
+
export function getHarnessFeedbackRelativePath(repoRoot, absolutePath) {
|
|
405
|
+
return toRepoRelativePath(repoRoot, absolutePath);
|
|
406
|
+
}
|
|
@@ -13,6 +13,7 @@ import { renderReviewerHarnessRules } from "../templates/harness/reviewer-agent.
|
|
|
13
13
|
import { renderVcmFinalAcceptanceSkillRules } from "../templates/harness/vcm-final-acceptance-skill.js";
|
|
14
14
|
import { renderVcmHarnessBootstrapSkillRules } from "../templates/harness/vcm-harness-bootstrap-skill.js";
|
|
15
15
|
import { renderVcmLongRunningValidationSkillRules } from "../templates/harness/vcm-long-running-validation-skill.js";
|
|
16
|
+
import { renderVcmReportHarnessIssueSkillRules } from "../templates/harness/vcm-report-harness-issue-skill.js";
|
|
16
17
|
import { renderVcmRouteMessageSkillRules } from "../templates/harness/vcm-route-message-skill.js";
|
|
17
18
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
18
19
|
import { VcmError } from "../errors.js";
|
|
@@ -104,6 +105,14 @@ const HARNESS_FILES = [
|
|
|
104
105
|
ownership: "whole-file",
|
|
105
106
|
renderRules: renderVcmGateReviewSkillRules
|
|
106
107
|
},
|
|
108
|
+
{
|
|
109
|
+
kind: "skill-vcm-report-harness-issue",
|
|
110
|
+
path: ".claude/skills/vcm-report-harness-issue/SKILL.md",
|
|
111
|
+
title: "VCM Report Harness Issue Skill",
|
|
112
|
+
frontmatter: renderSkillFrontmatter("vcm-report-harness-issue", "Use when a VCM role notices a reusable harness problem and needs to record feedback for Harness Engineer review."),
|
|
113
|
+
ownership: "whole-file",
|
|
114
|
+
renderRules: renderVcmReportHarnessIssueSkillRules
|
|
115
|
+
},
|
|
107
116
|
{
|
|
108
117
|
kind: "agent-gate-reviewer",
|
|
109
118
|
path: ".claude/agents/gate-reviewer.md",
|
|
@@ -72,6 +72,7 @@ export function createSessionService(deps) {
|
|
|
72
72
|
cwd: startCommand.cwd,
|
|
73
73
|
env: {
|
|
74
74
|
VCM_API_URL: deps.apiUrl,
|
|
75
|
+
VCM_BASE_REPO_ROOT: repoRoot,
|
|
75
76
|
VCM_TASK_REPO_ROOT: taskRepoRoot,
|
|
76
77
|
VCM_TASK_SLUG: taskSlug,
|
|
77
78
|
VCM_ROLE: role,
|
|
@@ -151,6 +152,7 @@ export function createSessionService(deps) {
|
|
|
151
152
|
cwd: startCommand.cwd,
|
|
152
153
|
env: {
|
|
153
154
|
VCM_API_URL: deps.apiUrl,
|
|
155
|
+
VCM_BASE_REPO_ROOT: repoRoot,
|
|
154
156
|
VCM_TASK_REPO_ROOT: activeTaskRepoRoot ?? repoRoot,
|
|
155
157
|
VCM_TASK_SLUG: activeTaskSlug ?? PROJECT_GATE_REVIEWER_SCOPE,
|
|
156
158
|
VCM_ROLE: GATE_REVIEWER_ROLE,
|
|
@@ -246,6 +248,7 @@ export function createSessionService(deps) {
|
|
|
246
248
|
cwd: startCommand.cwd,
|
|
247
249
|
env: {
|
|
248
250
|
VCM_API_URL: deps.apiUrl,
|
|
251
|
+
VCM_BASE_REPO_ROOT: repoRoot,
|
|
249
252
|
VCM_TASK_REPO_ROOT: taskContext.taskRepoRoot,
|
|
250
253
|
VCM_TASK_SLUG: taskContext.taskSlug,
|
|
251
254
|
VCM_ROLE: TRANSLATOR_ROLE,
|
|
@@ -322,6 +325,7 @@ export function createSessionService(deps) {
|
|
|
322
325
|
cwd: startCommand.cwd,
|
|
323
326
|
env: {
|
|
324
327
|
VCM_API_URL: deps.apiUrl,
|
|
328
|
+
VCM_BASE_REPO_ROOT: repoRoot,
|
|
325
329
|
VCM_TASK_REPO_ROOT: taskContext.taskRepoRoot,
|
|
326
330
|
VCM_TASK_SLUG: taskContext.taskSlug,
|
|
327
331
|
VCM_ROLE: HARNESS_ENGINEER_ROLE,
|
|
@@ -426,6 +430,7 @@ export function createSessionService(deps) {
|
|
|
426
430
|
cwd: startCommand.cwd,
|
|
427
431
|
env: {
|
|
428
432
|
VCM_API_URL: deps.apiUrl,
|
|
433
|
+
VCM_BASE_REPO_ROOT: repoRoot,
|
|
429
434
|
VCM_TASK_REPO_ROOT: targetCwd,
|
|
430
435
|
VCM_TASK_SLUG: normalizeProjectScopedRecordForPersistence(session).taskSlug,
|
|
431
436
|
VCM_ROLE: session.role,
|
|
@@ -1438,6 +1443,9 @@ function normalizeClaudePermissionMode(value) {
|
|
|
1438
1443
|
if (value === "bypassPermissions" || value === "dangerously-skip-permissions") {
|
|
1439
1444
|
return "bypassPermissions";
|
|
1440
1445
|
}
|
|
1446
|
+
if (value === "plan") {
|
|
1447
|
+
return "plan";
|
|
1448
|
+
}
|
|
1441
1449
|
return "default";
|
|
1442
1450
|
}
|
|
1443
1451
|
function normalizeClaudeModel(value) {
|
|
@@ -5,8 +5,15 @@ export function renderRootClaudeHarnessRules() {
|
|
|
5
5
|
- Read module-local \`CLAUDE.md\` before editing a subdirectory if one exists.
|
|
6
6
|
- Use \`vcm-route-message\` whenever a VCM role hands off work, asks another role a question, reports a result, reports a blocker, or raises a finding. Follow its write-then-stop rule.
|
|
7
7
|
- Use \`vcm-long-running-validation\` for long-running validation. Follow the background job limits below.
|
|
8
|
+
- Use \`vcm-report-harness-issue\` when you notice a reusable VCM harness problem. Record feedback; do not contact Harness Engineer directly.
|
|
8
9
|
- Project-manager uses \`vcm-gate-review\` at enabled Gate Review trigger points and on VCM Gate Review callbacks.
|
|
9
10
|
|
|
11
|
+
## VCM Harness Scope
|
|
12
|
+
|
|
13
|
+
VCM harness includes root \`CLAUDE.md\`, \`.claude/agents/**\`, \`.claude/skills/**\`, \`.ai/tools/**\`, \`.claude/settings.json\`, VCM managed blocks, generated-context tooling, bootstrap rules, routing rules, validation rules, Gate Review rules, Translator rules, and Harness Engineer rules.
|
|
14
|
+
|
|
15
|
+
If a reusable harness problem is suspected, it is enough to record a concise feedback report with evidence. Harness Engineer decides whether it is real, whether it should be fixed, and which files are in scope.
|
|
16
|
+
|
|
10
17
|
## VCM Background Jobs
|
|
11
18
|
|
|
12
19
|
- Never run the Bash tool with \`run_in_background: true\`. Never detach a process with \`nohup\`, \`setsid\`, \`disown\`, or a trailing \`&\`. VCM denies these calls.
|