vibe-coding-master 0.4.19 → 0.4.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/backend/adapters/git-adapter.js +15 -0
  2. package/dist/backend/api/harness-routes.js +17 -0
  3. package/dist/backend/api/task-routes.js +2 -2
  4. package/dist/backend/app-version.js +12 -0
  5. package/dist/backend/cli/install-vcm-harness.js +3 -2
  6. package/dist/backend/gateway/gateway-service.js +2 -5
  7. package/dist/backend/server.js +6 -2
  8. package/dist/backend/services/app-settings-service.js +2 -0
  9. package/dist/backend/services/claude-hook-service.js +188 -36
  10. package/dist/backend/services/diagnostics-service.js +2 -13
  11. package/dist/backend/services/harness-feedback-service.js +149 -2
  12. package/dist/backend/services/harness-service.js +159 -22
  13. package/dist/backend/services/round-service.js +58 -0
  14. package/dist/backend/services/session-service.js +6 -301
  15. package/dist/backend/templates/harness/architect-agent.js +37 -3
  16. package/dist/backend/templates/harness/coder-agent.js +6 -1
  17. package/dist/backend/templates/harness/harness-engineer-agent.js +31 -0
  18. package/dist/backend/templates/harness/project-manager-agent.js +7 -0
  19. package/dist/backend/templates/harness/reviewer-agent.js +10 -0
  20. package/dist/main.js +2 -11
  21. package/dist-frontend/assets/index-B3sODrcw.css +32 -0
  22. package/dist-frontend/assets/index-_aOTGZCq.js +96 -0
  23. package/dist-frontend/index.html +2 -2
  24. package/docs/full-harness-baseline.md +1 -2
  25. package/docs/gate-review-gates.md +8 -17
  26. package/docs/product-design.md +5 -6
  27. package/docs/v0.4-harness-optimization-plan.md +3 -2
  28. package/package.json +1 -1
  29. package/dist-frontend/assets/index-BrY-xd6U.js +0 -95
  30. package/dist-frontend/assets/index-D1LTJ-sY.css +0 -32
@@ -1,4 +1,6 @@
1
+ import { createHash } from "node:crypto";
1
2
  import path from "node:path";
3
+ import { checkMarkdownArtifact } from "../../shared/validation/artifact-check.js";
2
4
  import { resolveRepoPath, toRepoRelativePath } from "../adapters/filesystem.js";
3
5
  import { VcmError } from "../errors.js";
4
6
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
@@ -6,6 +8,7 @@ const FEEDBACK_ROOT = ".ai/vcm/harness-feedback";
6
8
  const PENDING_DIR = `${FEEDBACK_ROOT}/pending`;
7
9
  const ACTIVE_DIR = `${FEEDBACK_ROOT}/active`;
8
10
  const COMPLETED_DIR = `${FEEDBACK_ROOT}/completed`;
11
+ const TASK_RETROSPECTIVE_DIR = `${FEEDBACK_ROOT}/task-retrospectives`;
9
12
  const STATE_PATH = `${FEEDBACK_ROOT}/state.json`;
10
13
  export function createHarnessFeedbackService(deps) {
11
14
  const now = deps.now ?? (() => new Date().toISOString());
@@ -13,6 +16,80 @@ export function createHarnessFeedbackService(deps) {
13
16
  await maybeDispatchNext(repoRoot, activeTaskSlug);
14
17
  return buildStateReport(repoRoot);
15
18
  }
19
+ async function startTaskRetrospective(repoRoot, input) {
20
+ const taskSlug = input.taskSlug.trim();
21
+ if (!taskSlug) {
22
+ throw new VcmError({
23
+ code: "HARNESS_TASK_REQUIRED",
24
+ message: "Select an active task before reviewing task harness.",
25
+ statusCode: 409
26
+ });
27
+ }
28
+ const state = await loadStoredState(repoRoot);
29
+ if (state) {
30
+ throw new VcmError({
31
+ code: "HARNESS_FEEDBACK_ACTIVE",
32
+ message: "Harness feedback is already active.",
33
+ statusCode: 409,
34
+ hint: "Review, approve, comment, or reject the current Harness feedback before starting Task Harness Retrospective."
35
+ });
36
+ }
37
+ const existingMarker = await loadTaskRetrospectiveMarker(repoRoot, taskSlug);
38
+ if (existingMarker) {
39
+ throw new VcmError({
40
+ code: "TASK_HARNESS_RETROSPECTIVE_EXISTS",
41
+ message: `Task Harness Retrospective has already been triggered for task: ${taskSlug}`,
42
+ statusCode: 409,
43
+ hint: "Review the existing Harness feedback item instead of starting another retrospective for the same task."
44
+ });
45
+ }
46
+ const finalAcceptancePath = path.posix.join(input.handoffDir, "final-acceptance.md");
47
+ const finalAcceptanceAbsolutePath = resolveRepoPath(input.taskRepoRoot, finalAcceptancePath);
48
+ const finalAcceptanceContent = await readAbsoluteOptionalText(finalAcceptanceAbsolutePath);
49
+ const finalAcceptanceCheck = checkMarkdownArtifact("final-acceptance", finalAcceptancePath, finalAcceptanceContent ?? null);
50
+ if (finalAcceptanceCheck.status !== "ok" || !finalAcceptanceContent) {
51
+ throw new VcmError({
52
+ code: "TASK_FINAL_ACCEPTANCE_NOT_READY",
53
+ message: "Task final acceptance is not complete yet.",
54
+ statusCode: 409,
55
+ hint: `${finalAcceptancePath} must pass the final-acceptance artifact check before Task Harness Retrospective can start.`
56
+ });
57
+ }
58
+ const session = await ensureIdleHarnessEngineer(repoRoot, taskSlug);
59
+ const timestamp = now();
60
+ const finalAcceptanceHash = `sha256:${sha256(finalAcceptanceContent)}`;
61
+ const id = sanitizeFeedbackId(`${timestamp}-task-retrospective-${taskSlug}`);
62
+ const feedbackPath = `${ACTIVE_DIR}/${id}/feedback.md`;
63
+ const analysisPath = `${ACTIVE_DIR}/${id}/analysis.md`;
64
+ const applyReportPath = `${ACTIVE_DIR}/${id}/apply-report.md`;
65
+ const active = {
66
+ id,
67
+ title: `Task Harness Retrospective: ${taskSlug}`,
68
+ path: feedbackPath,
69
+ source: "task-retrospective",
70
+ taskSlug,
71
+ summary: "Review the completed task workflow for reusable harness problems.",
72
+ trigger: input.trigger,
73
+ finalAcceptanceHash,
74
+ feedbackPath,
75
+ analysisPath,
76
+ applyReportPath,
77
+ startedAt: timestamp,
78
+ updatedAt: timestamp,
79
+ lastPromptAt: timestamp
80
+ };
81
+ const nextState = {
82
+ version: 1,
83
+ status: "analyzing",
84
+ active
85
+ };
86
+ await deps.fs.ensureDir(resolveRepoPath(repoRoot, path.posix.dirname(feedbackPath)));
87
+ await deps.fs.writeText(resolveRepoPath(repoRoot, feedbackPath), renderTaskRetrospectiveFeedback(active, finalAcceptancePath));
88
+ await persistStoredState(repoRoot, nextState);
89
+ await persistTaskRetrospectiveMarker(repoRoot, active, "analyzing");
90
+ await submitTerminalInput(deps.runtime, session.id, buildTaskRetrospectivePrompt(repoRoot, active));
91
+ return buildStateReport(repoRoot);
92
+ }
16
93
  async function decide(repoRoot, input) {
17
94
  const state = await loadStoredState(repoRoot);
18
95
  if (!state || state.status !== "awaiting_user_approval") {
@@ -48,6 +125,7 @@ export function createHarnessFeedbackService(deps) {
48
125
  }
49
126
  };
50
127
  await persistStoredState(repoRoot, nextState);
128
+ await persistTaskRetrospectiveMarker(repoRoot, nextState.active, "analyzing");
51
129
  await submitTerminalInput(deps.runtime, session.id, buildFeedbackCommentPrompt(repoRoot, nextState.active, input.comment ?? ""));
52
130
  return buildStateReport(repoRoot);
53
131
  }
@@ -63,6 +141,7 @@ export function createHarnessFeedbackService(deps) {
63
141
  }
64
142
  };
65
143
  await persistStoredState(repoRoot, nextState);
144
+ await persistTaskRetrospectiveMarker(repoRoot, nextState.active, "applying");
66
145
  await submitTerminalInput(deps.runtime, session.id, buildFeedbackApplyPrompt(repoRoot, nextState.active, input.comment ?? ""));
67
146
  return buildStateReport(repoRoot);
68
147
  }
@@ -76,14 +155,16 @@ export function createHarnessFeedbackService(deps) {
76
155
  }
77
156
  const timestamp = now();
78
157
  if (state.status === "analyzing") {
79
- await persistStoredState(repoRoot, {
158
+ const nextState = {
80
159
  ...state,
81
160
  status: "awaiting_user_approval",
82
161
  active: {
83
162
  ...state.active,
84
163
  updatedAt: timestamp
85
164
  }
86
- });
165
+ };
166
+ await persistStoredState(repoRoot, nextState);
167
+ await persistTaskRetrospectiveMarker(repoRoot, nextState.active, "awaiting_user_approval", timestamp);
87
168
  return;
88
169
  }
89
170
  if (state.status === "applying") {
@@ -132,6 +213,7 @@ export function createHarnessFeedbackService(deps) {
132
213
  const active = {
133
214
  ...next,
134
215
  feedbackPath: next.path,
216
+ source: next.source ?? "role-feedback",
135
217
  analysisPath,
136
218
  applyReportPath,
137
219
  startedAt: timestamp,
@@ -209,12 +291,15 @@ export function createHarnessFeedbackService(deps) {
209
291
  id: state.active.id,
210
292
  title: state.active.title,
211
293
  path: state.active.path,
294
+ source: state.active.source,
212
295
  reporterRole: state.active.reporterRole,
213
296
  taskSlug: state.active.taskSlug,
214
297
  summary: state.active.summary,
215
298
  status: state.status,
216
299
  startedAt: state.active.startedAt,
217
300
  updatedAt: state.active.updatedAt,
301
+ trigger: state.active.trigger,
302
+ finalAcceptanceHash: state.active.finalAcceptanceHash,
218
303
  feedbackContent,
219
304
  analysisPath: state.active.analysisPath,
220
305
  analysisContent,
@@ -249,6 +334,7 @@ export function createHarnessFeedbackService(deps) {
249
334
  id,
250
335
  title: compactLine(title),
251
336
  path: relativePath,
337
+ source: "role-feedback",
252
338
  reporterRole: metadata["reporter role"] ?? metadata.reporter,
253
339
  taskSlug: metadata["task slug"] ?? metadata.task,
254
340
  summary: metadata.summary
@@ -278,6 +364,16 @@ export function createHarnessFeedbackService(deps) {
278
364
  "</HARNESS_FEEDBACK>"
279
365
  ].join("\n");
280
366
  }
367
+ function buildTaskRetrospectivePrompt(repoRoot, active) {
368
+ return [
369
+ "[VCM Task Harness Retrospective]",
370
+ "",
371
+ "Review the completed task from the current active task worktree.",
372
+ "",
373
+ `Write the analysis to Result Path: ${resolveRepoPath(repoRoot, active.analysisPath)}`,
374
+ "End your turn after writing the result."
375
+ ].join("\n");
376
+ }
281
377
  function buildFeedbackCommentPrompt(repoRoot, active, comment) {
282
378
  return [
283
379
  "[VCM Harness Feedback Revision]",
@@ -345,6 +441,7 @@ export function createHarnessFeedbackService(deps) {
345
441
  comment,
346
442
  completedAt: now()
347
443
  });
444
+ await persistTaskRetrospectiveMarker(repoRoot, state.active, outcome === "rejected" ? "rejected" : "completed");
348
445
  await deps.fs.removePath?.(resolveRepoPath(repoRoot, state.active.feedbackPath), { force: true });
349
446
  await deps.fs.removePath?.(resolveRepoPath(repoRoot, path.posix.dirname(state.active.analysisPath)), { recursive: true, force: true });
350
447
  }
@@ -357,6 +454,7 @@ export function createHarnessFeedbackService(deps) {
357
454
  if (state?.version !== 1 || !state.active?.id || !state.status) {
358
455
  return undefined;
359
456
  }
457
+ state.active.source = state.active.source ?? "role-feedback";
360
458
  return state;
361
459
  }
362
460
  async function persistStoredState(repoRoot, state) {
@@ -372,8 +470,38 @@ export function createHarnessFeedbackService(deps) {
372
470
  }
373
471
  return deps.fs.readText(absolutePath);
374
472
  }
473
+ async function readAbsoluteOptionalText(absolutePath) {
474
+ if (!(await deps.fs.pathExists(absolutePath))) {
475
+ return undefined;
476
+ }
477
+ return deps.fs.readText(absolutePath);
478
+ }
479
+ async function loadTaskRetrospectiveMarker(repoRoot, taskSlug) {
480
+ const markerPath = resolveRepoPath(repoRoot, getTaskRetrospectiveMarkerPath(taskSlug));
481
+ if (!(await deps.fs.pathExists(markerPath))) {
482
+ return undefined;
483
+ }
484
+ return deps.fs.readJson(markerPath);
485
+ }
486
+ async function persistTaskRetrospectiveMarker(repoRoot, active, status, timestamp = now()) {
487
+ if (active.source !== "task-retrospective" || !active.taskSlug) {
488
+ return;
489
+ }
490
+ await deps.fs.writeJsonAtomic(resolveRepoPath(repoRoot, getTaskRetrospectiveMarkerPath(active.taskSlug)), {
491
+ version: 1,
492
+ taskSlug: active.taskSlug,
493
+ activeId: active.id,
494
+ trigger: active.trigger ?? "manual",
495
+ status,
496
+ finalAcceptanceHash: active.finalAcceptanceHash,
497
+ createdAt: active.startedAt,
498
+ updatedAt: timestamp,
499
+ ...(status === "completed" || status === "rejected" ? { completedAt: timestamp } : {})
500
+ });
501
+ }
375
502
  return {
376
503
  getState,
504
+ startTaskRetrospective,
377
505
  decide,
378
506
  recordHarnessEngineerHook,
379
507
  assertHarnessEngineerAvailable
@@ -397,10 +525,29 @@ function firstHeading(content) {
397
525
  function compactLine(value) {
398
526
  return value.replace(/\s+/g, " ").trim().slice(0, 160);
399
527
  }
528
+ function renderTaskRetrospectiveFeedback(active, finalAcceptancePath) {
529
+ return [
530
+ `# ${active.title}`,
531
+ "",
532
+ `Source: ${active.source}`,
533
+ `Task slug: ${active.taskSlug ?? ""}`,
534
+ `Trigger: ${active.trigger ?? "manual"}`,
535
+ `Final acceptance: ${finalAcceptancePath}`,
536
+ `Final acceptance hash: ${active.finalAcceptanceHash ?? ""}`,
537
+ "",
538
+ "Summary: Review the completed task workflow for reusable harness problems."
539
+ ].join("\n");
540
+ }
541
+ function getTaskRetrospectiveMarkerPath(taskSlug) {
542
+ return `${TASK_RETROSPECTIVE_DIR}/${sanitizeFeedbackId(taskSlug)}.json`;
543
+ }
400
544
  function sanitizeFeedbackId(value) {
401
545
  const sanitized = value.replace(/[^A-Za-z0-9._-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
402
546
  return sanitized || "feedback";
403
547
  }
548
+ function sha256(content) {
549
+ return createHash("sha256").update(content).digest("hex");
550
+ }
404
551
  export function getHarnessFeedbackRelativePath(repoRoot, absolutePath) {
405
552
  return toRepoRelativePath(repoRoot, absolutePath);
406
553
  }
@@ -21,6 +21,7 @@ import { bumpHarnessRevision, readHarnessRevisionState } from "./harness-revisio
21
21
  const execFileAsync = promisify(execFile);
22
22
  const BOOTSTRAP_SESSION_PATH = ".ai/vcm/bootstrap/session.json";
23
23
  const HARNESS_ENGINEER_SESSION_PATH = ".ai/vcm/harness-engineer/session.json";
24
+ const MANIFEST_PATH = ".ai/vcm-harness-manifest.json";
24
25
  export const VCM_HARNESS_VERSION = 1;
25
26
  const MANAGED_BLOCK_PATTERN = /<!-- VCM:BEGIN(?:\s+version=(\d+))? -->[\s\S]*?<!-- VCM:END -->/m;
26
27
  const HASH_MANAGED_BLOCK_PATTERN = /# VCM:BEGIN(?:\s+version=(\d+))?\n[\s\S]*?# VCM:END/m;
@@ -173,11 +174,15 @@ const HARNESS_FILES = [
173
174
  ];
174
175
  export function createHarnessService(deps) {
175
176
  const now = deps.now ?? (() => new Date().toISOString());
177
+ const vcmVersion = deps.vcmVersion ?? "unknown";
176
178
  return {
177
179
  async getHarnessStatus(repoRoot) {
178
180
  const analyses = await analyzeHarnessFiles(deps.fs, repoRoot);
179
181
  const legacyChanges = await analyzeLegacyCodexHarnessPaths(deps.fs, repoRoot);
180
- return renderHarnessStatus(await readHarnessRevisionValue(deps.fs, repoRoot), analyses, legacyChanges);
182
+ const manifestChange = deps.runFixedInstaller
183
+ ? await analyzeHarnessManifest(deps.fs, repoRoot, vcmVersion)
184
+ : undefined;
185
+ return renderHarnessStatus(await readHarnessRevisionValue(deps.fs, repoRoot), analyses, legacyChanges, manifestChange);
181
186
  },
182
187
  async getHarnessFileContent(repoRoot, filePath) {
183
188
  return readHarnessFileContent(deps.fs, repoRoot, filePath);
@@ -208,9 +213,12 @@ export function createHarnessService(deps) {
208
213
  const file = await readHarnessFileContent(deps.fs, repoRoot, definition.path);
209
214
  const analyses = await analyzeHarnessFiles(deps.fs, repoRoot);
210
215
  const legacyChanges = await analyzeLegacyCodexHarnessPaths(deps.fs, repoRoot);
216
+ const manifestChange = deps.runFixedInstaller
217
+ ? await analyzeHarnessManifest(deps.fs, repoRoot, vcmVersion)
218
+ : undefined;
211
219
  return {
212
220
  file,
213
- status: renderHarnessStatus(await readHarnessRevisionValue(deps.fs, repoRoot), analyses, legacyChanges),
221
+ status: renderHarnessStatus(await readHarnessRevisionValue(deps.fs, repoRoot), analyses, legacyChanges, manifestChange),
214
222
  harnessCommit
215
223
  };
216
224
  },
@@ -258,12 +266,15 @@ export function createHarnessService(deps) {
258
266
  async getRepositoryDiff(repoRoot, input = {}) {
259
267
  return getRepositoryDiffReport(requireRepositoryDiffGit(deps.git), repoRoot, input, now());
260
268
  },
269
+ async mergeRepositoryDiffToCurrentBranch(baseRepoRoot, input) {
270
+ return mergeRepositoryDiffToCurrentBranch(requireRepositoryMergeGit(deps.git), baseRepoRoot, input, now());
271
+ },
261
272
  async getBootstrapStatus(repoRoot, targetRepoRoot = repoRoot) {
262
- return getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now);
273
+ return getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now, vcmVersion);
263
274
  },
264
275
  async startHarnessBootstrap(repoRoot, targetRepoRoot = repoRoot, input = {}) {
265
- const session = await ensureHarnessEngineerForBootstrap(deps, repoRoot, targetRepoRoot, now, input);
266
- const nextStatus = await getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now);
276
+ const session = await ensureHarnessEngineerForBootstrap(deps, repoRoot, targetRepoRoot, now, vcmVersion, input);
277
+ const nextStatus = await getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now, vcmVersion);
267
278
  return {
268
279
  status: {
269
280
  ...nextStatus,
@@ -274,8 +285,8 @@ export function createHarnessService(deps) {
274
285
  };
275
286
  },
276
287
  async restartHarnessBootstrap(repoRoot, targetRepoRoot = repoRoot, input = {}) {
277
- const session = await restartHarnessEngineerForBootstrap(deps, repoRoot, targetRepoRoot, now, input);
278
- const nextStatus = await getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now);
288
+ const session = await restartHarnessEngineerForBootstrap(deps, repoRoot, targetRepoRoot, now, vcmVersion, input);
289
+ const nextStatus = await getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now, vcmVersion);
279
290
  return {
280
291
  status: {
281
292
  ...nextStatus,
@@ -296,7 +307,7 @@ export function createHarnessService(deps) {
296
307
  }
297
308
  await deps.harnessEngineerSessions?.stopProjectHarnessEngineerSession(repoRoot);
298
309
  await clearHarnessBootstrapRunState(deps.fs, repoRoot);
299
- return getHarnessBootstrapStatus(deps, repoRoot, repoRoot, now);
310
+ return getHarnessBootstrapStatus(deps, repoRoot, repoRoot, now, vcmVersion);
300
311
  },
301
312
  async runHarnessBootstrap(repoRoot, targetRepoRoot = repoRoot) {
302
313
  if (!deps.runtime || !deps.harnessEngineerSessions) {
@@ -307,7 +318,7 @@ export function createHarnessService(deps) {
307
318
  });
308
319
  }
309
320
  await assertHarnessWorktreeClean(deps.git, targetRepoRoot);
310
- const status = await getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now);
321
+ const status = await getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now, vcmVersion);
311
322
  const session = status.session;
312
323
  if (!session || session.status !== "running" || !deps.runtime.getSession(session.id)) {
313
324
  throw new VcmError({
@@ -328,7 +339,7 @@ export function createHarnessService(deps) {
328
339
  updatedAt: timestamp
329
340
  });
330
341
  await submitTerminalInput(deps.runtime, session.id, prompt);
331
- const nextStatus = await getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now);
342
+ const nextStatus = await getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now, vcmVersion);
332
343
  return {
333
344
  status: {
334
345
  ...nextStatus,
@@ -342,7 +353,7 @@ export function createHarnessService(deps) {
342
353
  async recordHarnessBootstrapHook(repoRoot, input) {
343
354
  const state = await loadPersistedHarnessBootstrapRunState(deps.fs, repoRoot);
344
355
  if (state?.status !== "running" || !matchesBootstrapRunState(state, input)) {
345
- return getHarnessBootstrapStatus(deps, repoRoot, state?.targetRepoRoot ?? repoRoot, now);
356
+ return getHarnessBootstrapStatus(deps, repoRoot, state?.targetRepoRoot ?? repoRoot, now, vcmVersion);
346
357
  }
347
358
  const timestamp = now();
348
359
  if (input.eventName === "Stop" && state.targetRepoRoot) {
@@ -355,11 +366,11 @@ export function createHarnessService(deps) {
355
366
  updatedAt: timestamp,
356
367
  lastHookEvent: input.eventName
357
368
  });
358
- return getHarnessBootstrapStatus(deps, repoRoot, state.targetRepoRoot ?? repoRoot, now);
369
+ return getHarnessBootstrapStatus(deps, repoRoot, state.targetRepoRoot ?? repoRoot, now, vcmVersion);
359
370
  }
360
371
  };
361
372
  }
362
- async function ensureHarnessEngineerForBootstrap(deps, repoRoot, targetRepoRoot, now, input) {
373
+ async function ensureHarnessEngineerForBootstrap(deps, repoRoot, targetRepoRoot, now, vcmVersion, input) {
363
374
  if (!deps.harnessEngineerSessions) {
364
375
  throw new VcmError({
365
376
  code: "HARNESS_BOOTSTRAP_UNAVAILABLE",
@@ -367,7 +378,7 @@ async function ensureHarnessEngineerForBootstrap(deps, repoRoot, targetRepoRoot,
367
378
  statusCode: 501
368
379
  });
369
380
  }
370
- const currentStatus = await getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now);
381
+ const currentStatus = await getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now, vcmVersion);
371
382
  if (currentStatus.session?.status === "running") {
372
383
  return currentStatus.session;
373
384
  }
@@ -380,7 +391,7 @@ async function ensureHarnessEngineerForBootstrap(deps, repoRoot, targetRepoRoot,
380
391
  }
381
392
  return toHarnessBootstrapSession(await deps.harnessEngineerSessions.ensureProjectHarnessEngineerSession(repoRoot, input));
382
393
  }
383
- async function restartHarnessEngineerForBootstrap(deps, repoRoot, targetRepoRoot, now, input) {
394
+ async function restartHarnessEngineerForBootstrap(deps, repoRoot, targetRepoRoot, now, vcmVersion, input) {
384
395
  if (!deps.harnessEngineerSessions) {
385
396
  throw new VcmError({
386
397
  code: "HARNESS_BOOTSTRAP_UNAVAILABLE",
@@ -388,7 +399,7 @@ async function restartHarnessEngineerForBootstrap(deps, repoRoot, targetRepoRoot
388
399
  statusCode: 501
389
400
  });
390
401
  }
391
- const currentStatus = await getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now);
402
+ const currentStatus = await getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now, vcmVersion);
392
403
  if (!currentStatus.canStart) {
393
404
  throw new VcmError({
394
405
  code: "HARNESS_BOOTSTRAP_NOT_READY",
@@ -454,6 +465,7 @@ function requireRepositoryDiffGit(git) {
454
465
  if (!git?.getCommitDiff ||
455
466
  !git.getCommitInfo ||
456
467
  !git.getCommitList ||
468
+ !git.getCurrentBranch ||
457
469
  !git.getHeadCommit ||
458
470
  !git.getMergeBase) {
459
471
  throw new VcmError({
@@ -464,7 +476,22 @@ function requireRepositoryDiffGit(git) {
464
476
  }
465
477
  return git;
466
478
  }
479
+ function requireRepositoryMergeGit(git) {
480
+ if (!git?.branchExists ||
481
+ !git.getCurrentBranch ||
482
+ !git.getHeadCommit ||
483
+ !git.mergeBranchFastForward) {
484
+ throw new VcmError({
485
+ code: "HARNESS_GIT_UNAVAILABLE",
486
+ message: "Git-backed repository merge is not available in this VCM runtime.",
487
+ statusCode: 501
488
+ });
489
+ }
490
+ return git;
491
+ }
467
492
  async function getRepositoryDiffReport(git, repoRoot, input, generatedAt) {
493
+ const sourceBranch = await git.getCurrentBranch(repoRoot);
494
+ const targetBranch = input.baseRepoRoot ? await git.getCurrentBranch(input.baseRepoRoot) : sourceBranch;
468
495
  const baseRef = input.baseRepoRoot
469
496
  ? await git.getHeadCommit(input.baseRepoRoot)
470
497
  : "HEAD^";
@@ -476,6 +503,8 @@ async function getRepositoryDiffReport(git, repoRoot, input, generatedAt) {
476
503
  return {
477
504
  version: 1,
478
505
  repoRoot,
506
+ sourceBranch,
507
+ targetBranch,
479
508
  generatedAt,
480
509
  commits,
481
510
  summary: createEmptyRepositoryDiffSummary(),
@@ -497,6 +526,8 @@ async function getRepositoryDiffReport(git, repoRoot, input, generatedAt) {
497
526
  return {
498
527
  version: 1,
499
528
  repoRoot,
529
+ sourceBranch,
530
+ targetBranch,
500
531
  generatedAt,
501
532
  commits,
502
533
  commit: {
@@ -522,6 +553,71 @@ async function getRepositoryDiffReport(git, repoRoot, input, generatedAt) {
522
553
  warnings
523
554
  };
524
555
  }
556
+ async function mergeRepositoryDiffToCurrentBranch(git, baseRepoRoot, input, mergedAt) {
557
+ const taskBranch = input.taskBranch.trim();
558
+ if (!taskBranch) {
559
+ throw new VcmError({
560
+ code: "HARNESS_MERGE_SOURCE_BRANCH_MISSING",
561
+ message: "Task branch is missing.",
562
+ statusCode: 400
563
+ });
564
+ }
565
+ if (!(await git.branchExists(baseRepoRoot, taskBranch))) {
566
+ throw new VcmError({
567
+ code: "HARNESS_MERGE_SOURCE_BRANCH_MISSING",
568
+ message: `Task branch does not exist locally: ${taskBranch}`,
569
+ statusCode: 409,
570
+ hint: "Create or restore the task branch before merging it into the connected repository branch."
571
+ });
572
+ }
573
+ const targetBranch = await git.getCurrentBranch(baseRepoRoot);
574
+ if (targetBranch === "detached") {
575
+ throw new VcmError({
576
+ code: "HARNESS_MERGE_BASE_DETACHED",
577
+ message: "The connected repository is in detached HEAD state.",
578
+ statusCode: 409,
579
+ hint: "Checkout the intended target branch in the connected repository before merging the task branch."
580
+ });
581
+ }
582
+ if (taskBranch === targetBranch) {
583
+ throw new VcmError({
584
+ code: "HARNESS_MERGE_BRANCH_INVALID",
585
+ message: "Task branch is already the connected repository branch.",
586
+ statusCode: 409,
587
+ hint: `Source and target are both ${targetBranch}.`
588
+ });
589
+ }
590
+ await assertVisibleWorktreeClean(git, input.taskRepoRoot, "The active task worktree", "HARNESS_MERGE_TASK_DIRTY");
591
+ await assertVisibleWorktreeClean(git, baseRepoRoot, "The connected repository", "HARNESS_MERGE_BASE_DIRTY");
592
+ const beforeSha = await git.getHeadCommit(baseRepoRoot);
593
+ const mergeResult = await git.mergeBranchFastForward(baseRepoRoot, taskBranch);
594
+ const afterSha = await git.getHeadCommit(baseRepoRoot);
595
+ return {
596
+ version: 1,
597
+ baseRepoRoot,
598
+ taskRepoRoot: input.taskRepoRoot,
599
+ sourceBranch: taskBranch,
600
+ targetBranch,
601
+ beforeSha,
602
+ afterSha,
603
+ changed: beforeSha !== afterSha,
604
+ stdout: mergeResult.stdout,
605
+ stderr: mergeResult.stderr,
606
+ mergedAt
607
+ };
608
+ }
609
+ async function assertVisibleWorktreeClean(git, repoRoot, label, code) {
610
+ const entries = await getVisibleGitStatusEntries(git, repoRoot);
611
+ if (entries.length === 0) {
612
+ return;
613
+ }
614
+ throw new VcmError({
615
+ code,
616
+ message: `${label} has uncommitted Git-visible changes.`,
617
+ statusCode: 409,
618
+ hint: `Commit or revert these files before merging to main: ${entries.map((entry) => entry.path).slice(0, 12).join(", ")}`
619
+ });
620
+ }
525
621
  function toRepositoryDiffCommit(commit) {
526
622
  return {
527
623
  sha: commit.sha,
@@ -987,12 +1083,13 @@ async function removeLegacyCodexHarnessPaths(fs, repoRoot) {
987
1083
  }
988
1084
  return changes;
989
1085
  }
990
- function renderHarnessStatus(harnessRevision, analyses, legacyChanges = []) {
1086
+ function renderHarnessStatus(harnessRevision, analyses, legacyChanges = [], manifestChange) {
991
1087
  const files = analyses.map((analysis) => analysis.status);
992
1088
  const plannedChanges = analyses
993
1089
  .map((analysis) => analysis.plannedChange)
994
1090
  .filter((change) => Boolean(change))
995
- .concat(legacyChanges);
1091
+ .concat(legacyChanges)
1092
+ .concat(manifestChange ? [manifestChange] : []);
996
1093
  // Derive `initialized`: the VCM harness is considered installed when at least one
997
1094
  // VCM-exclusive marker is present (per analysis):
998
1095
  // - status.hasManagedBlock === true (a managed block already lives in the file), OR
@@ -1165,10 +1262,40 @@ function resolveHarnessPath(repoRoot, relativePath) {
1165
1262
  function ensureTrailingNewline(content) {
1166
1263
  return content.endsWith("\n") ? content : `${content}\n`;
1167
1264
  }
1168
- async function getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now) {
1265
+ async function analyzeHarnessManifest(fs, repoRoot, vcmVersion) {
1266
+ if (!vcmVersion || vcmVersion === "unknown") {
1267
+ return undefined;
1268
+ }
1269
+ const manifestPath = resolveHarnessPath(repoRoot, MANIFEST_PATH);
1270
+ if (!await fs.pathExists(manifestPath)) {
1271
+ return {
1272
+ path: MANIFEST_PATH,
1273
+ action: "create",
1274
+ reason: `VCM fixed harness manifest is missing; current VCM version is ${vcmVersion}.`
1275
+ };
1276
+ }
1277
+ const manifest = await readOptionalJsonObject(fs, repoRoot, MANIFEST_PATH);
1278
+ const installedVersion = typeof manifest?.harnessVersion === "string" ? manifest.harnessVersion : undefined;
1279
+ if (!manifest || manifest.manager !== "vcm" || manifest.schemaVersion !== 1) {
1280
+ return {
1281
+ path: MANIFEST_PATH,
1282
+ action: "update",
1283
+ reason: "VCM fixed harness manifest metadata is invalid."
1284
+ };
1285
+ }
1286
+ if (installedVersion !== vcmVersion) {
1287
+ return {
1288
+ path: MANIFEST_PATH,
1289
+ action: "update",
1290
+ reason: `VCM fixed harness manifest version is ${installedVersion ?? "missing"}; current VCM version is ${vcmVersion}.`
1291
+ };
1292
+ }
1293
+ return undefined;
1294
+ }
1295
+ async function getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now, vcmVersion) {
1169
1296
  const moduleIndex = await readOptionalJsonObject(deps.fs, targetRepoRoot, ".ai/generated/module-index.json");
1170
1297
  const checks = [
1171
- await checkFixedHarness(deps.fs, targetRepoRoot),
1298
+ await checkFixedHarness(deps.fs, targetRepoRoot, vcmVersion),
1172
1299
  await checkProjectContext(deps.fs, targetRepoRoot),
1173
1300
  checkGeneratedJson(moduleIndex, ".ai/generated/module-index.json", "module-index", "Module index"),
1174
1301
  checkGeneratedJson(await readOptionalJsonObject(deps.fs, targetRepoRoot, ".ai/generated/public-surface.json"), ".ai/generated/public-surface.json", "public-surface", "Public surface"),
@@ -1200,10 +1327,10 @@ async function getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now) {
1200
1327
  warnings: bootstrapWarnings(status, checks, session, runState)
1201
1328
  };
1202
1329
  }
1203
- async function checkFixedHarness(fs, repoRoot) {
1330
+ async function checkFixedHarness(fs, repoRoot, vcmVersion) {
1204
1331
  const requiredPaths = [
1205
1332
  "CLAUDE.md",
1206
- ".ai/vcm-harness-manifest.json",
1333
+ MANIFEST_PATH,
1207
1334
  ".claude/skills/vcm-harness-bootstrap/SKILL.md",
1208
1335
  ".ai/tools/generate-module-index",
1209
1336
  ".ai/tools/generate-public-surface"
@@ -1232,6 +1359,16 @@ async function checkFixedHarness(fs, repoRoot) {
1232
1359
  detail: "CLAUDE.md is missing the VCM managed block."
1233
1360
  };
1234
1361
  }
1362
+ const manifestChange = await analyzeHarnessManifest(fs, repoRoot, vcmVersion);
1363
+ if (manifestChange) {
1364
+ return {
1365
+ key: "fixed-harness",
1366
+ label: "Fixed harness",
1367
+ status: "incomplete",
1368
+ path: MANIFEST_PATH,
1369
+ detail: manifestChange.reason
1370
+ };
1371
+ }
1235
1372
  return {
1236
1373
  key: "fixed-harness",
1237
1374
  label: "Fixed harness",