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 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 === "bypassPermissions") {
33
- args.push("--permission-mode", "bypassPermissions");
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/",
@@ -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.