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.
- package/dist/backend/adapters/git-adapter.js +45 -18
- package/dist/backend/api/harness-routes.js +38 -29
- package/dist/backend/services/harness-service.js +317 -121
- package/dist/backend/services/session-service.js +34 -24
- package/dist/backend/services/task-service.js +33 -20
- package/dist/backend/services/translation-service.js +1 -0
- package/dist/backend/services/translation-worker-service.js +30 -3
- package/dist/backend/templates/harness/harness-engineer-agent.js +2 -0
- package/dist/backend/templates/harness/vcm-harness-bootstrap-skill.js +11 -1
- package/dist-frontend/assets/index-Cvv4cf0U.js +95 -0
- package/dist-frontend/assets/{index-kJDZAzjD.css → index-M9t0gmj2.css} +1 -1
- package/dist-frontend/index.html +2 -2
- package/docs/product-design.md +5 -2
- package/docs/v0.4-harness-optimization-plan.md +4 -3
- package/docs/vcm-cc-best-practices.md +3 -2
- package/package.json +1 -1
- package/scripts/harness-tools/generate-public-surface +1 -1
- package/dist-frontend/assets/index-uYhgIwNK.js +0 -94
|
@@ -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
|
-
|
|
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
|
-
:
|
|
246
|
+
: formatHarnessApplyMessage(committed.harnessCommit, true)
|
|
234
247
|
};
|
|
235
248
|
},
|
|
236
|
-
async
|
|
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
|
|
253
|
+
message: "Git-backed repository diff is not available in this VCM runtime.",
|
|
241
254
|
statusCode: 501
|
|
242
255
|
});
|
|
243
256
|
}
|
|
244
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
922
|
-
await checkProjectContext(deps.fs,
|
|
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,
|
|
925
|
-
await checkFilledMarkdown(deps.fs,
|
|
926
|
-
await checkModuleArchitectureDocs(deps.fs,
|
|
927
|
-
await checkFilledMarkdown(deps.fs,
|
|
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(
|
|
1233
|
-
return `Use the vcm-harness-bootstrap skill to finish the VCM harness bootstrap for
|
|
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
|
-
|
|
1236
|
-
${
|
|
1426
|
+
Target task worktree:
|
|
1427
|
+
${targetRepoRoot}
|
|
1237
1428
|
|
|
1238
1429
|
Required work:
|
|
1239
|
-
-
|
|
1240
|
-
- Run .ai/tools/generate-
|
|
1241
|
-
-
|
|
1242
|
-
-
|
|
1243
|
-
-
|
|
1244
|
-
-
|
|
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) => {
|