vibe-coding-master 0.4.3 → 0.4.5

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.
@@ -174,6 +174,7 @@ export function createHarnessService(deps) {
174
174
  return readHarnessFileContent(deps.fs, repoRoot, filePath);
175
175
  },
176
176
  async updateHarnessFileContent(repoRoot, filePath, content) {
177
+ await assertHarnessWorktreeClean(deps.git, repoRoot);
177
178
  const definition = getHarnessFileDefinition(filePath);
178
179
  if (!isProjectEditableHarnessFile(definition)) {
179
180
  throw new VcmError({
@@ -190,24 +191,34 @@ export function createHarnessService(deps) {
190
191
  assertManagedBlockUnchanged(definition, currentContent, content);
191
192
  const nextContent = ensureTrailingNewline(content);
192
193
  await deps.fs.writeText(absolutePath, nextContent);
194
+ let harnessCommit;
193
195
  if (nextContent !== currentContent) {
194
196
  await bumpHarnessRevision(deps.fs, repoRoot, now());
197
+ harnessCommit = (await commitHarnessVisibleChanges(deps.git, repoRoot, "chore(vcm-harness): update harness file")).harnessCommit;
195
198
  }
196
199
  const file = await readHarnessFileContent(deps.fs, repoRoot, definition.path);
197
200
  const analyses = await analyzeHarnessFiles(deps.fs, repoRoot);
198
201
  const legacyChanges = await analyzeLegacyCodexHarnessPaths(deps.fs, repoRoot);
199
202
  return {
200
203
  file,
201
- status: renderHarnessStatus(await readHarnessRevisionValue(deps.fs, repoRoot), analyses, legacyChanges)
204
+ status: renderHarnessStatus(await readHarnessRevisionValue(deps.fs, repoRoot), analyses, legacyChanges),
205
+ harnessCommit
202
206
  };
203
207
  },
204
208
  async applyHarness(repoRoot) {
209
+ await assertHarnessWorktreeClean(deps.git, repoRoot);
205
210
  if (deps.runFixedInstaller) {
206
211
  const result = await deps.runFixedInstaller(repoRoot);
207
212
  if (result.changedFiles.length > 0) {
208
213
  await bumpHarnessRevision(deps.fs, repoRoot, now());
209
214
  }
210
- return result;
215
+ const committed = await commitHarnessVisibleChanges(deps.git, repoRoot, "chore(vcm-harness): update fixed harness");
216
+ return {
217
+ ...result,
218
+ changedFiles: committed.changedFiles.length > 0 ? committed.changedFiles : result.changedFiles,
219
+ harnessCommit: committed.harnessCommit,
220
+ message: formatHarnessApplyMessage(committed.harnessCommit, result.changedFiles.length > 0)
221
+ };
211
222
  }
212
223
  const analyses = await analyzeHarnessFiles(deps.fs, repoRoot);
213
224
  const changedFiles = [];
@@ -225,106 +236,51 @@ export function createHarnessService(deps) {
225
236
  if (changedFiles.length > 0) {
226
237
  await bumpHarnessRevision(deps.fs, repoRoot, now());
227
238
  }
239
+ const committed = await commitHarnessVisibleChanges(deps.git, repoRoot, "chore(vcm-harness): update fixed harness");
228
240
  return {
229
241
  version: VCM_HARNESS_VERSION,
230
- changedFiles,
242
+ changedFiles: committed.changedFiles.length > 0 ? committed.changedFiles : changedFiles,
243
+ harnessCommit: committed.harnessCommit,
231
244
  message: changedFiles.length === 0
232
245
  ? "VCM Harness is already up to date."
233
- : "VCM Harness updated. Review these files and commit the harness changes before starting long-running work."
246
+ : formatHarnessApplyMessage(committed.harnessCommit, true)
234
247
  };
235
248
  },
236
- async commitAndRebaseTask(repoRoot, input) {
249
+ async getRepositoryDiff(repoRoot, scope = "harness") {
237
250
  if (!deps.git) {
238
251
  throw new VcmError({
239
252
  code: "HARNESS_GIT_UNAVAILABLE",
240
- message: "Git-backed harness sync is not available in this VCM runtime.",
253
+ message: "Git-backed repository diff is not available in this VCM runtime.",
241
254
  statusCode: 501
242
255
  });
243
256
  }
244
- const changedFiles = dedupeHarnessChanges(input.changedFiles);
245
- const changedPaths = changedFiles.map((change) => change.path);
246
- const taskBranch = await deps.git.getCurrentBranch(input.worktreePath);
247
- if (taskBranch !== input.branch) {
248
- throw new VcmError({
249
- code: "HARNESS_TASK_BRANCH_MISMATCH",
250
- message: "The selected task worktree is not on its recorded task branch.",
251
- statusCode: 409,
252
- hint: `Expected ${input.branch}, found ${taskBranch}.`
253
- });
254
- }
255
- const taskStatus = await deps.git.getStatusPorcelain(input.worktreePath);
256
- if (taskStatus.trim()) {
257
- throw new VcmError({
258
- code: "HARNESS_TASK_DIRTY",
259
- message: "The selected task worktree has uncommitted changes.",
260
- statusCode: 409,
261
- hint: "Commit or clean the task worktree before rebasing it onto the harness commit."
262
- });
263
- }
264
- const preExistingStaged = await deps.git.getStagedStatus(repoRoot);
265
- if (preExistingStaged.trim()) {
266
- throw new VcmError({
267
- code: "HARNESS_BASE_STAGED_CHANGES",
268
- message: "The connected repository already has staged changes.",
269
- statusCode: 409,
270
- hint: "Commit, unstage, or clean existing staged changes before using Commit & rebase task."
271
- });
272
- }
273
- const baseBranch = await deps.git.getCurrentBranch(repoRoot);
274
- const baseCommitBefore = await deps.git.getHeadCommit(repoRoot);
275
- let harnessCommit;
276
- let committed = false;
277
- if (changedPaths.length > 0) {
278
- await deps.git.addPaths(repoRoot, changedPaths);
279
- const stagedHarnessChanges = await deps.git.getStagedStatus(repoRoot);
280
- if (stagedHarnessChanges.trim()) {
281
- harnessCommit = await deps.git.commit(repoRoot, "chore: update VCM harness");
282
- committed = true;
283
- }
284
- }
285
- const baseCommitAfter = await deps.git.getHeadCommit(repoRoot);
286
- await deps.git.rebase(input.worktreePath, baseCommitAfter);
287
- return {
288
- taskSlug: input.taskSlug,
289
- branch: input.branch,
290
- worktreePath: input.worktreePath,
291
- baseBranch,
292
- baseCommitBefore,
293
- baseCommitAfter,
294
- harnessCommit,
295
- committed,
296
- rebased: true,
297
- changedFiles,
298
- message: committed
299
- ? `Committed VCM harness update ${shortCommit(baseCommitAfter)} on ${baseBranch} and rebased ${input.branch}.`
300
- : `No new harness commit was needed; rebased ${input.branch} onto ${shortCommit(baseCommitAfter)}.`
301
- };
257
+ return getRepositoryDiffReport(deps.git, repoRoot, scope, now());
302
258
  },
303
- async getBootstrapStatus(repoRoot) {
304
- return getHarnessBootstrapStatus(deps, repoRoot, now);
259
+ async getBootstrapStatus(repoRoot, targetRepoRoot = repoRoot) {
260
+ return getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now);
305
261
  },
306
- async startHarnessBootstrap(repoRoot, input = {}) {
307
- const session = await ensureHarnessEngineerForBootstrap(deps, repoRoot, now, input);
308
- const nextStatus = await getHarnessBootstrapStatus(deps, repoRoot, now);
262
+ async startHarnessBootstrap(repoRoot, targetRepoRoot = repoRoot, input = {}) {
263
+ const session = await ensureHarnessEngineerForBootstrap(deps, repoRoot, targetRepoRoot, now, input);
264
+ const nextStatus = await getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now);
309
265
  return {
310
266
  status: {
311
267
  ...nextStatus,
312
268
  session
313
269
  },
314
270
  session,
315
- prompt: buildHarnessBootstrapPrompt(repoRoot)
271
+ prompt: buildHarnessBootstrapPrompt(repoRoot, targetRepoRoot)
316
272
  };
317
273
  },
318
- async restartHarnessBootstrap(repoRoot, input = {}) {
319
- const session = await restartHarnessEngineerForBootstrap(deps, repoRoot, now, input);
320
- const nextStatus = await getHarnessBootstrapStatus(deps, repoRoot, now);
274
+ async restartHarnessBootstrap(repoRoot, targetRepoRoot = repoRoot, input = {}) {
275
+ const session = await restartHarnessEngineerForBootstrap(deps, repoRoot, targetRepoRoot, now, input);
276
+ const nextStatus = await getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now);
321
277
  return {
322
278
  status: {
323
279
  ...nextStatus,
324
280
  session
325
281
  },
326
282
  session,
327
- prompt: buildHarnessBootstrapPrompt(repoRoot)
283
+ prompt: buildHarnessBootstrapPrompt(repoRoot, targetRepoRoot)
328
284
  };
329
285
  },
330
286
  async stopHarnessBootstrap(repoRoot) {
@@ -338,9 +294,9 @@ export function createHarnessService(deps) {
338
294
  }
339
295
  await deps.harnessEngineerSessions?.stopProjectHarnessEngineerSession(repoRoot);
340
296
  await clearHarnessBootstrapRunState(deps.fs, repoRoot);
341
- return getHarnessBootstrapStatus(deps, repoRoot, now);
297
+ return getHarnessBootstrapStatus(deps, repoRoot, repoRoot, now);
342
298
  },
343
- async runHarnessBootstrap(repoRoot) {
299
+ async runHarnessBootstrap(repoRoot, targetRepoRoot = repoRoot) {
344
300
  if (!deps.runtime || !deps.harnessEngineerSessions) {
345
301
  throw new VcmError({
346
302
  code: "HARNESS_BOOTSTRAP_UNAVAILABLE",
@@ -348,7 +304,8 @@ export function createHarnessService(deps) {
348
304
  statusCode: 501
349
305
  });
350
306
  }
351
- const status = await getHarnessBootstrapStatus(deps, repoRoot, now);
307
+ await assertHarnessWorktreeClean(deps.git, targetRepoRoot);
308
+ const status = await getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now);
352
309
  const session = status.session;
353
310
  if (!session || session.status !== "running" || !deps.runtime.getSession(session.id)) {
354
311
  throw new VcmError({
@@ -357,33 +314,38 @@ export function createHarnessService(deps) {
357
314
  statusCode: 409
358
315
  });
359
316
  }
360
- const prompt = buildHarnessBootstrapPrompt(repoRoot);
317
+ const prompt = buildHarnessBootstrapPrompt(repoRoot, targetRepoRoot);
361
318
  const timestamp = now();
362
319
  await persistHarnessBootstrapRunState(deps.fs, repoRoot, {
363
320
  version: 1,
364
321
  status: "running",
322
+ targetRepoRoot,
365
323
  sessionId: session.id,
366
324
  claudeSessionId: session.claudeSessionId,
367
325
  startedAt: timestamp,
368
326
  updatedAt: timestamp
369
327
  });
370
328
  await submitTerminalInput(deps.runtime, session.id, prompt);
371
- const nextStatus = await getHarnessBootstrapStatus(deps, repoRoot, now);
329
+ const nextStatus = await getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now);
372
330
  return {
373
331
  status: {
374
332
  ...nextStatus,
375
333
  session
376
334
  },
377
335
  session,
378
- prompt
336
+ prompt,
337
+ targetRepoRoot
379
338
  };
380
339
  },
381
340
  async recordHarnessBootstrapHook(repoRoot, input) {
382
341
  const state = await loadPersistedHarnessBootstrapRunState(deps.fs, repoRoot);
383
342
  if (state?.status !== "running" || !matchesBootstrapRunState(state, input)) {
384
- return getHarnessBootstrapStatus(deps, repoRoot, now);
343
+ return getHarnessBootstrapStatus(deps, repoRoot, state?.targetRepoRoot ?? repoRoot, now);
385
344
  }
386
345
  const timestamp = now();
346
+ if (input.eventName === "Stop" && state.targetRepoRoot) {
347
+ await bumpHarnessRevision(deps.fs, state.targetRepoRoot, timestamp);
348
+ }
387
349
  await persistHarnessBootstrapRunState(deps.fs, repoRoot, {
388
350
  ...state,
389
351
  status: input.eventName === "Stop" ? "complete" : state.status,
@@ -391,27 +353,11 @@ export function createHarnessService(deps) {
391
353
  updatedAt: timestamp,
392
354
  lastHookEvent: input.eventName
393
355
  });
394
- return getHarnessBootstrapStatus(deps, repoRoot, now);
356
+ return getHarnessBootstrapStatus(deps, repoRoot, state.targetRepoRoot ?? repoRoot, now);
395
357
  }
396
358
  };
397
359
  }
398
- function dedupeHarnessChanges(changedFiles) {
399
- const seen = new Set();
400
- const changes = [];
401
- for (const change of changedFiles) {
402
- const normalizedPath = normalizeHarnessGitPath(change.path);
403
- if (seen.has(normalizedPath)) {
404
- continue;
405
- }
406
- seen.add(normalizedPath);
407
- changes.push({
408
- ...change,
409
- path: normalizedPath
410
- });
411
- }
412
- return changes;
413
- }
414
- async function ensureHarnessEngineerForBootstrap(deps, repoRoot, now, input) {
360
+ async function ensureHarnessEngineerForBootstrap(deps, repoRoot, targetRepoRoot, now, input) {
415
361
  if (!deps.harnessEngineerSessions) {
416
362
  throw new VcmError({
417
363
  code: "HARNESS_BOOTSTRAP_UNAVAILABLE",
@@ -419,7 +365,7 @@ async function ensureHarnessEngineerForBootstrap(deps, repoRoot, now, input) {
419
365
  statusCode: 501
420
366
  });
421
367
  }
422
- const currentStatus = await getHarnessBootstrapStatus(deps, repoRoot, now);
368
+ const currentStatus = await getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now);
423
369
  if (currentStatus.session?.status === "running") {
424
370
  return currentStatus.session;
425
371
  }
@@ -432,7 +378,7 @@ async function ensureHarnessEngineerForBootstrap(deps, repoRoot, now, input) {
432
378
  }
433
379
  return toHarnessBootstrapSession(await deps.harnessEngineerSessions.ensureProjectHarnessEngineerSession(repoRoot, input));
434
380
  }
435
- async function restartHarnessEngineerForBootstrap(deps, repoRoot, now, input) {
381
+ async function restartHarnessEngineerForBootstrap(deps, repoRoot, targetRepoRoot, now, input) {
436
382
  if (!deps.harnessEngineerSessions) {
437
383
  throw new VcmError({
438
384
  code: "HARNESS_BOOTSTRAP_UNAVAILABLE",
@@ -440,7 +386,7 @@ async function restartHarnessEngineerForBootstrap(deps, repoRoot, now, input) {
440
386
  statusCode: 501
441
387
  });
442
388
  }
443
- const currentStatus = await getHarnessBootstrapStatus(deps, repoRoot, now);
389
+ const currentStatus = await getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now);
444
390
  if (!currentStatus.canStart) {
445
391
  throw new VcmError({
446
392
  code: "HARNESS_BOOTSTRAP_NOT_READY",
@@ -500,6 +446,248 @@ function normalizeHarnessGitPath(value) {
500
446
  function shortCommit(commit) {
501
447
  return commit.slice(0, 7);
502
448
  }
449
+ const REPOSITORY_DIFF_MAX_FILES = 250;
450
+ const REPOSITORY_DIFF_MAX_DIFF_CHARS = 180_000;
451
+ async function getRepositoryDiffReport(git, repoRoot, scope, generatedAt) {
452
+ const commitInfo = await git.getCommitInfo(repoRoot, "HEAD");
453
+ const rawDiff = await git.getCommitDiff(repoRoot, "HEAD");
454
+ const allFiles = parseCommitDiffFiles(rawDiff)
455
+ .filter((file) => scope === "all" || file.category !== "product_code");
456
+ const warnings = [];
457
+ const files = allFiles.slice(0, REPOSITORY_DIFF_MAX_FILES);
458
+ if (allFiles.length > files.length) {
459
+ warnings.push(`Diff file list is truncated to ${REPOSITORY_DIFF_MAX_FILES} files.`);
460
+ }
461
+ const productCodeCount = files.filter((file) => file.category === "product_code").length;
462
+ if (productCodeCount > 0) {
463
+ warnings.push(`${productCodeCount} product code file(s) are present in this commit. Verify they are expected before approving harness work.`);
464
+ }
465
+ return {
466
+ version: 1,
467
+ repoRoot,
468
+ scope,
469
+ generatedAt,
470
+ commit: {
471
+ sha: commitInfo.sha,
472
+ shortSha: commitInfo.sha.slice(0, 12),
473
+ subject: commitInfo.subject,
474
+ committedAt: commitInfo.committedAt
475
+ },
476
+ summary: {
477
+ totalFiles: files.length,
478
+ committedFiles: files.length,
479
+ stagedFiles: 0,
480
+ unstagedFiles: 0,
481
+ untrackedFiles: 0,
482
+ additions: files.reduce((total, file) => total + file.additions, 0),
483
+ deletions: files.reduce((total, file) => total + file.deletions, 0),
484
+ harnessFiles: files.filter((file) => file.category !== "product_code").length,
485
+ productCodeFiles: productCodeCount,
486
+ truncatedFiles: files.filter((file) => file.truncated).length,
487
+ binaryFiles: files.filter((file) => file.binary).length
488
+ },
489
+ files,
490
+ warnings
491
+ };
492
+ }
493
+ function parseCommitDiffFiles(rawDiff) {
494
+ const chunks = rawDiff.split(/(?=^diff --git )/m).filter((chunk) => chunk.startsWith("diff --git "));
495
+ return chunks.map(parseCommitDiffFile);
496
+ }
497
+ function parseCommitDiffFile(chunk) {
498
+ const firstLine = chunk.split("\n", 1)[0] ?? "";
499
+ const match = firstLine.match(/^diff --git a\/(.+) b\/(.+)$/);
500
+ const oldPath = normalizeHarnessGitPath(match?.[1] ?? "unknown");
501
+ const nextPath = normalizeHarnessGitPath(match?.[2] ?? oldPath);
502
+ const status = getCommitDiffFileStatus(chunk);
503
+ const pathForCategory = status === "deleted" ? oldPath : nextPath;
504
+ const category = classifyRepositoryDiffPath(pathForCategory);
505
+ const truncatedDiff = truncateRepositoryDiff(chunk);
506
+ const lineStats = countDiffLines(truncatedDiff.diff);
507
+ return {
508
+ path: pathForCategory,
509
+ oldPath: oldPath === pathForCategory ? undefined : oldPath,
510
+ status,
511
+ stage: "committed",
512
+ category,
513
+ diff: truncatedDiff.diff,
514
+ binary: /Binary files .* differ|GIT binary patch/.test(chunk),
515
+ truncated: truncatedDiff.truncated,
516
+ additions: lineStats.additions,
517
+ deletions: lineStats.deletions
518
+ };
519
+ }
520
+ function getCommitDiffFileStatus(diffChunk) {
521
+ if (/^new file mode /m.test(diffChunk)) {
522
+ return "added";
523
+ }
524
+ if (/^deleted file mode /m.test(diffChunk)) {
525
+ return "deleted";
526
+ }
527
+ if (/^similarity index /m.test(diffChunk) && /^rename from /m.test(diffChunk) && /^rename to /m.test(diffChunk)) {
528
+ return "renamed";
529
+ }
530
+ if (/^copy from /m.test(diffChunk) && /^copy to /m.test(diffChunk)) {
531
+ return "copied";
532
+ }
533
+ return "modified";
534
+ }
535
+ export function parseGitStatusPorcelainV1(rawStatus) {
536
+ const records = rawStatus.split("\0").filter(Boolean);
537
+ const entries = [];
538
+ for (let index = 0; index < records.length; index += 1) {
539
+ const record = records[index] ?? "";
540
+ if (record.length < 4) {
541
+ continue;
542
+ }
543
+ const indexStatus = record[0] ?? " ";
544
+ const workingTreeStatus = record[1] ?? " ";
545
+ const filePath = normalizeHarnessGitPath(record.slice(3));
546
+ let oldPath;
547
+ if (indexStatus === "R" || indexStatus === "C") {
548
+ const oldPathRecord = records[index + 1];
549
+ if (oldPathRecord) {
550
+ oldPath = normalizeHarnessGitPath(oldPathRecord);
551
+ index += 1;
552
+ }
553
+ }
554
+ entries.push({
555
+ path: filePath,
556
+ oldPath,
557
+ indexStatus,
558
+ workingTreeStatus
559
+ });
560
+ }
561
+ return entries;
562
+ }
563
+ export function classifyRepositoryDiffPath(repoRelativePath) {
564
+ const filePath = normalizeHarnessGitPath(repoRelativePath);
565
+ if (filePath === "CLAUDE.md" ||
566
+ filePath === ".gitignore" ||
567
+ filePath === ".ai/vcm-harness-manifest.json" ||
568
+ filePath === ".github/pull_request_template.md" ||
569
+ filePath === ".claude/settings.json" ||
570
+ filePath === ".claude/settings.local.json" ||
571
+ filePath.startsWith(".claude/agents/") ||
572
+ filePath.startsWith(".claude/skills/")) {
573
+ return "fixed_harness";
574
+ }
575
+ if (filePath.startsWith(".ai/tools/") || filePath.startsWith(".github/workflows/")) {
576
+ return "tools_hooks";
577
+ }
578
+ if (filePath.startsWith(".ai/generated/")) {
579
+ return "generated_context";
580
+ }
581
+ if (filePath.startsWith("docs/") ||
582
+ filePath === "ARCHITECTURE.md" ||
583
+ filePath.endsWith("/ARCHITECTURE.md") ||
584
+ filePath === "TESTING.md" ||
585
+ filePath.endsWith("/TESTING.md")) {
586
+ return "project_docs";
587
+ }
588
+ return "product_code";
589
+ }
590
+ function truncateRepositoryDiff(diff) {
591
+ if (diff.length <= REPOSITORY_DIFF_MAX_DIFF_CHARS) {
592
+ return { diff, truncated: false };
593
+ }
594
+ return {
595
+ diff: `${diff.slice(0, REPOSITORY_DIFF_MAX_DIFF_CHARS)}\n# ... diff truncated ...\n`,
596
+ truncated: true
597
+ };
598
+ }
599
+ function countDiffLines(diff) {
600
+ let additions = 0;
601
+ let deletions = 0;
602
+ for (const line of diff.split("\n")) {
603
+ if (line.startsWith("+++") || line.startsWith("---")) {
604
+ continue;
605
+ }
606
+ if (line.startsWith("+")) {
607
+ additions += 1;
608
+ }
609
+ else if (line.startsWith("-")) {
610
+ deletions += 1;
611
+ }
612
+ }
613
+ return { additions, deletions };
614
+ }
615
+ async function assertHarnessWorktreeClean(git, repoRoot) {
616
+ if (!git) {
617
+ return;
618
+ }
619
+ const entries = await getVisibleGitStatusEntries(git, repoRoot);
620
+ if (entries.length === 0) {
621
+ return;
622
+ }
623
+ throw new VcmError({
624
+ code: "HARNESS_WORKTREE_DIRTY",
625
+ message: "The active task worktree has uncommitted Git-visible changes.",
626
+ statusCode: 409,
627
+ hint: `Commit or revert these files before running a harness operation: ${entries.map((entry) => entry.path).slice(0, 12).join(", ")}`
628
+ });
629
+ }
630
+ async function commitHarnessVisibleChanges(git, repoRoot, message) {
631
+ if (!git) {
632
+ return { changedFiles: [] };
633
+ }
634
+ const entries = await getVisibleGitStatusEntries(git, repoRoot);
635
+ if (entries.length === 0) {
636
+ return { changedFiles: [] };
637
+ }
638
+ const unexpected = entries.filter((entry) => classifyRepositoryDiffPath(entry.path) === "product_code");
639
+ if (unexpected.length > 0) {
640
+ throw new VcmError({
641
+ code: "HARNESS_UNEXPECTED_PRODUCT_CHANGES",
642
+ message: "Harness operation produced product-code changes.",
643
+ statusCode: 409,
644
+ hint: `Review, revert, or move these changes before continuing: ${unexpected.map((entry) => entry.path).slice(0, 12).join(", ")}`
645
+ });
646
+ }
647
+ const changedFiles = entries.map(statusEntryToHarnessChange);
648
+ const stagePaths = uniqueStrings(entries.flatMap((entry) => [entry.path, entry.oldPath].filter((value) => Boolean(value))));
649
+ await git.addPaths(repoRoot, stagePaths);
650
+ const harnessCommit = await git.commit(repoRoot, message);
651
+ return { harnessCommit, changedFiles };
652
+ }
653
+ async function getVisibleGitStatusEntries(git, repoRoot) {
654
+ const rawStatus = await git.getStatusPorcelainV1(repoRoot);
655
+ return parseGitStatusPorcelainV1(rawStatus).filter((entry) => !isVcmRuntimePath(entry.path));
656
+ }
657
+ function isVcmRuntimePath(repoRelativePath) {
658
+ const filePath = normalizeHarnessGitPath(repoRelativePath);
659
+ return filePath.startsWith(".ai/vcm/") ||
660
+ filePath.startsWith(".claude/worktrees/") ||
661
+ filePath.startsWith(".ai/tools/__pycache__/") ||
662
+ filePath.endsWith("/__pycache__");
663
+ }
664
+ function statusEntryToHarnessChange(entry) {
665
+ return {
666
+ path: entry.path,
667
+ action: statusEntryToHarnessAction(entry),
668
+ reason: "Committed by VCM harness operation."
669
+ };
670
+ }
671
+ function statusEntryToHarnessAction(entry) {
672
+ if (entry.indexStatus === "?" || entry.indexStatus === "A" || entry.workingTreeStatus === "A") {
673
+ return "create";
674
+ }
675
+ if (entry.indexStatus === "D" || entry.workingTreeStatus === "D") {
676
+ return "delete";
677
+ }
678
+ return "update";
679
+ }
680
+ function uniqueStrings(values) {
681
+ return [...new Set(values)];
682
+ }
683
+ function formatHarnessApplyMessage(harnessCommit, changed) {
684
+ if (!changed) {
685
+ return "VCM Harness is already up to date.";
686
+ }
687
+ return harnessCommit
688
+ ? `VCM Harness updated and committed as ${shortCommit(harnessCommit)}. Review the commit diff before approving.`
689
+ : "VCM Harness updated. Review the latest harness commit before approving.";
690
+ }
503
691
  async function readHarnessFileContent(fs, repoRoot, filePath) {
504
692
  const definition = getHarnessFileDefinition(filePath);
505
693
  const absolutePath = resolveHarnessPath(repoRoot, definition.path);
@@ -915,16 +1103,16 @@ function resolveHarnessPath(repoRoot, relativePath) {
915
1103
  function ensureTrailingNewline(content) {
916
1104
  return content.endsWith("\n") ? content : `${content}\n`;
917
1105
  }
918
- async function getHarnessBootstrapStatus(deps, repoRoot, now) {
919
- const moduleIndex = await readOptionalJsonObject(deps.fs, repoRoot, ".ai/generated/module-index.json");
1106
+ async function getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now) {
1107
+ const moduleIndex = await readOptionalJsonObject(deps.fs, targetRepoRoot, ".ai/generated/module-index.json");
920
1108
  const checks = [
921
- await checkFixedHarness(deps.fs, repoRoot),
922
- await checkProjectContext(deps.fs, repoRoot),
1109
+ await checkFixedHarness(deps.fs, targetRepoRoot),
1110
+ await checkProjectContext(deps.fs, targetRepoRoot),
923
1111
  checkGeneratedJson(moduleIndex, ".ai/generated/module-index.json", "module-index", "Module index"),
924
- checkGeneratedJson(await readOptionalJsonObject(deps.fs, repoRoot, ".ai/generated/public-surface.json"), ".ai/generated/public-surface.json", "public-surface", "Public surface"),
925
- await checkFilledMarkdown(deps.fs, repoRoot, "docs/ARCHITECTURE.md", "Project architecture", "project-architecture"),
926
- await checkModuleArchitectureDocs(deps.fs, repoRoot, moduleIndex),
927
- await checkFilledMarkdown(deps.fs, repoRoot, "docs/TESTING.md", "Testing doc", "testing-doc")
1112
+ checkGeneratedJson(await readOptionalJsonObject(deps.fs, targetRepoRoot, ".ai/generated/public-surface.json"), ".ai/generated/public-surface.json", "public-surface", "Public surface"),
1113
+ await checkFilledMarkdown(deps.fs, targetRepoRoot, "docs/ARCHITECTURE.md", "Project architecture", "project-architecture"),
1114
+ await checkModuleArchitectureDocs(deps.fs, targetRepoRoot, moduleIndex),
1115
+ await checkFilledMarkdown(deps.fs, targetRepoRoot, "docs/TESTING.md", "Testing doc", "testing-doc")
928
1116
  ];
929
1117
  const runState = await loadPersistedHarnessBootstrapRunState(deps.fs, repoRoot);
930
1118
  const session = await getCurrentHarnessEngineerBootstrapSession(deps, repoRoot);
@@ -1229,27 +1417,35 @@ function bootstrapWarnings(status, checks, session, runState) {
1229
1417
  }
1230
1418
  return warnings;
1231
1419
  }
1232
- function buildHarnessBootstrapPrompt(repoRoot) {
1233
- return `Use the vcm-harness-bootstrap skill to finish the VCM harness bootstrap for this repository.
1420
+ function buildHarnessBootstrapPrompt(baseRepoRoot, targetRepoRoot) {
1421
+ return `Use the vcm-harness-bootstrap skill to finish the VCM harness bootstrap for the active task worktree.
1422
+
1423
+ Base repository root:
1424
+ ${baseRepoRoot}
1234
1425
 
1235
- Repository root:
1236
- ${repoRoot}
1426
+ Target task worktree:
1427
+ ${targetRepoRoot}
1237
1428
 
1238
1429
  Required work:
1239
- - Run .ai/tools/generate-module-index when available.
1240
- - Run .ai/tools/generate-public-surface after module-index.json exists.
1241
- - Add or update project-specific Project Context and Project Constraints in CLAUDE.md above the VCM managed block.
1242
- - Fill docs/ARCHITECTURE.md with project-level module overview, responsibilities, relationships, dependency direction, project-wide constraints, and links to module-level architecture docs.
1243
- - Create or update module-level ARCHITECTURE.md files for clear module boundaries listed by module-index.json.
1244
- - Fill docs/TESTING.md with project-native validation levels, commands, validation selection rules, final-validation cleanup, test layout, integration/E2E case lists, generated-context freshness checks, and known testing gaps.
1430
+ - Work only inside the target task worktree.
1431
+ - Run .ai/tools/generate-module-index from the target task worktree when available.
1432
+ - Run .ai/tools/generate-public-surface from the target task worktree after module-index.json exists.
1433
+ - Add or update project-specific Project Context and Project Constraints in target CLAUDE.md above the VCM managed block.
1434
+ - Fill target docs/ARCHITECTURE.md with project-level module overview, responsibilities, relationships, dependency direction, project-wide constraints, and links to module-level architecture docs.
1435
+ - Create or update target module-level ARCHITECTURE.md files for clear module boundaries listed by module-index.json.
1436
+ - Fill target docs/TESTING.md with project-native validation levels, commands, validation selection rules, final-validation cleanup, test layout, integration/E2E case lists, generated-context freshness checks, and known testing gaps.
1437
+ - Review git status and git diff in the target task worktree.
1438
+ - Stage only allowed bootstrap harness changes and create a commit in the target task worktree.
1245
1439
 
1246
1440
  Boundaries:
1441
+ - Do not write to the base repository root.
1247
1442
  - Do not edit product source, product tests, package manifests, lockfiles, deployment config, secrets, or VCM managed blocks.
1248
1443
  - Preserve user-authored content.
1249
1444
  - Do not create new validation wrapper tools.
1445
+ - VCM will not create the bootstrap commit for you.
1250
1446
 
1251
1447
  Final response:
1252
- Summarize files reviewed, files updated, generated artifacts, verified claims, inferred claims, unknowns, confirmation-needed items, and suggested validation commands.`;
1448
+ Summarize files reviewed, files updated, generated artifacts, commit hash, final git status, verified claims, inferred claims, unknowns, confirmation-needed items, and suggested validation commands.`;
1253
1449
  }
1254
1450
  export function createScriptFixedHarnessInstaller(scriptPath = path.resolve(process.cwd(), "scripts/install-vcm-harness.mjs")) {
1255
1451
  return async (repoRoot) => {