vibe-coding-master 0.4.2 → 0.4.4
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 +314 -120
- package/dist/backend/services/task-service.js +33 -20
- package/dist-frontend/assets/{index-kJDZAzjD.css → index-12kW1cl6.css} +1 -1
- package/dist-frontend/assets/index-Bck3Mfz8.js +94 -0
- package/dist-frontend/index.html +2 -2
- package/docs/product-design.md +2 -2
- package/docs/v0.4-harness-optimization-plan.md +4 -3
- package/package.json +1 -1
- package/scripts/clean-build.mjs +1 -1
- package/scripts/harness-tools/generate-public-surface +1 -1
- package/dist-frontend/assets/index-uYhgIwNK.js +0 -94
- package/scripts/harness-tools/__pycache__/generate-module-indexcpython-314.pyc +0 -0
- package/scripts/harness-tools/__pycache__/generate-public-surfacecpython-314.pyc +0 -0
|
@@ -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,39 @@ 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
|
+
await commitHarnessVisibleChanges(deps.git, state.targetRepoRoot, "chore(vcm-harness): bootstrap project context");
|
|
349
|
+
}
|
|
387
350
|
await persistHarnessBootstrapRunState(deps.fs, repoRoot, {
|
|
388
351
|
...state,
|
|
389
352
|
status: input.eventName === "Stop" ? "complete" : state.status,
|
|
@@ -391,27 +354,11 @@ export function createHarnessService(deps) {
|
|
|
391
354
|
updatedAt: timestamp,
|
|
392
355
|
lastHookEvent: input.eventName
|
|
393
356
|
});
|
|
394
|
-
return getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
357
|
+
return getHarnessBootstrapStatus(deps, repoRoot, state.targetRepoRoot ?? repoRoot, now);
|
|
395
358
|
}
|
|
396
359
|
};
|
|
397
360
|
}
|
|
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) {
|
|
361
|
+
async function ensureHarnessEngineerForBootstrap(deps, repoRoot, targetRepoRoot, now, input) {
|
|
415
362
|
if (!deps.harnessEngineerSessions) {
|
|
416
363
|
throw new VcmError({
|
|
417
364
|
code: "HARNESS_BOOTSTRAP_UNAVAILABLE",
|
|
@@ -419,7 +366,7 @@ async function ensureHarnessEngineerForBootstrap(deps, repoRoot, now, input) {
|
|
|
419
366
|
statusCode: 501
|
|
420
367
|
});
|
|
421
368
|
}
|
|
422
|
-
const currentStatus = await getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
369
|
+
const currentStatus = await getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now);
|
|
423
370
|
if (currentStatus.session?.status === "running") {
|
|
424
371
|
return currentStatus.session;
|
|
425
372
|
}
|
|
@@ -432,7 +379,7 @@ async function ensureHarnessEngineerForBootstrap(deps, repoRoot, now, input) {
|
|
|
432
379
|
}
|
|
433
380
|
return toHarnessBootstrapSession(await deps.harnessEngineerSessions.ensureProjectHarnessEngineerSession(repoRoot, input));
|
|
434
381
|
}
|
|
435
|
-
async function restartHarnessEngineerForBootstrap(deps, repoRoot, now, input) {
|
|
382
|
+
async function restartHarnessEngineerForBootstrap(deps, repoRoot, targetRepoRoot, now, input) {
|
|
436
383
|
if (!deps.harnessEngineerSessions) {
|
|
437
384
|
throw new VcmError({
|
|
438
385
|
code: "HARNESS_BOOTSTRAP_UNAVAILABLE",
|
|
@@ -440,7 +387,7 @@ async function restartHarnessEngineerForBootstrap(deps, repoRoot, now, input) {
|
|
|
440
387
|
statusCode: 501
|
|
441
388
|
});
|
|
442
389
|
}
|
|
443
|
-
const currentStatus = await getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
390
|
+
const currentStatus = await getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now);
|
|
444
391
|
if (!currentStatus.canStart) {
|
|
445
392
|
throw new VcmError({
|
|
446
393
|
code: "HARNESS_BOOTSTRAP_NOT_READY",
|
|
@@ -500,6 +447,248 @@ function normalizeHarnessGitPath(value) {
|
|
|
500
447
|
function shortCommit(commit) {
|
|
501
448
|
return commit.slice(0, 7);
|
|
502
449
|
}
|
|
450
|
+
const REPOSITORY_DIFF_MAX_FILES = 250;
|
|
451
|
+
const REPOSITORY_DIFF_MAX_DIFF_CHARS = 180_000;
|
|
452
|
+
async function getRepositoryDiffReport(git, repoRoot, scope, generatedAt) {
|
|
453
|
+
const commitInfo = await git.getCommitInfo(repoRoot, "HEAD");
|
|
454
|
+
const rawDiff = await git.getCommitDiff(repoRoot, "HEAD");
|
|
455
|
+
const allFiles = parseCommitDiffFiles(rawDiff)
|
|
456
|
+
.filter((file) => scope === "all" || file.category !== "product_code");
|
|
457
|
+
const warnings = [];
|
|
458
|
+
const files = allFiles.slice(0, REPOSITORY_DIFF_MAX_FILES);
|
|
459
|
+
if (allFiles.length > files.length) {
|
|
460
|
+
warnings.push(`Diff file list is truncated to ${REPOSITORY_DIFF_MAX_FILES} files.`);
|
|
461
|
+
}
|
|
462
|
+
const productCodeCount = files.filter((file) => file.category === "product_code").length;
|
|
463
|
+
if (productCodeCount > 0) {
|
|
464
|
+
warnings.push(`${productCodeCount} product code file(s) are present in this commit. Verify they are expected before approving harness work.`);
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
version: 1,
|
|
468
|
+
repoRoot,
|
|
469
|
+
scope,
|
|
470
|
+
generatedAt,
|
|
471
|
+
commit: {
|
|
472
|
+
sha: commitInfo.sha,
|
|
473
|
+
shortSha: commitInfo.sha.slice(0, 12),
|
|
474
|
+
subject: commitInfo.subject,
|
|
475
|
+
committedAt: commitInfo.committedAt
|
|
476
|
+
},
|
|
477
|
+
summary: {
|
|
478
|
+
totalFiles: files.length,
|
|
479
|
+
committedFiles: files.length,
|
|
480
|
+
stagedFiles: 0,
|
|
481
|
+
unstagedFiles: 0,
|
|
482
|
+
untrackedFiles: 0,
|
|
483
|
+
additions: files.reduce((total, file) => total + file.additions, 0),
|
|
484
|
+
deletions: files.reduce((total, file) => total + file.deletions, 0),
|
|
485
|
+
harnessFiles: files.filter((file) => file.category !== "product_code").length,
|
|
486
|
+
productCodeFiles: productCodeCount,
|
|
487
|
+
truncatedFiles: files.filter((file) => file.truncated).length,
|
|
488
|
+
binaryFiles: files.filter((file) => file.binary).length
|
|
489
|
+
},
|
|
490
|
+
files,
|
|
491
|
+
warnings
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function parseCommitDiffFiles(rawDiff) {
|
|
495
|
+
const chunks = rawDiff.split(/(?=^diff --git )/m).filter((chunk) => chunk.startsWith("diff --git "));
|
|
496
|
+
return chunks.map(parseCommitDiffFile);
|
|
497
|
+
}
|
|
498
|
+
function parseCommitDiffFile(chunk) {
|
|
499
|
+
const firstLine = chunk.split("\n", 1)[0] ?? "";
|
|
500
|
+
const match = firstLine.match(/^diff --git a\/(.+) b\/(.+)$/);
|
|
501
|
+
const oldPath = normalizeHarnessGitPath(match?.[1] ?? "unknown");
|
|
502
|
+
const nextPath = normalizeHarnessGitPath(match?.[2] ?? oldPath);
|
|
503
|
+
const status = getCommitDiffFileStatus(chunk);
|
|
504
|
+
const pathForCategory = status === "deleted" ? oldPath : nextPath;
|
|
505
|
+
const category = classifyRepositoryDiffPath(pathForCategory);
|
|
506
|
+
const truncatedDiff = truncateRepositoryDiff(chunk);
|
|
507
|
+
const lineStats = countDiffLines(truncatedDiff.diff);
|
|
508
|
+
return {
|
|
509
|
+
path: pathForCategory,
|
|
510
|
+
oldPath: oldPath === pathForCategory ? undefined : oldPath,
|
|
511
|
+
status,
|
|
512
|
+
stage: "committed",
|
|
513
|
+
category,
|
|
514
|
+
diff: truncatedDiff.diff,
|
|
515
|
+
binary: /Binary files .* differ|GIT binary patch/.test(chunk),
|
|
516
|
+
truncated: truncatedDiff.truncated,
|
|
517
|
+
additions: lineStats.additions,
|
|
518
|
+
deletions: lineStats.deletions
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
function getCommitDiffFileStatus(diffChunk) {
|
|
522
|
+
if (/^new file mode /m.test(diffChunk)) {
|
|
523
|
+
return "added";
|
|
524
|
+
}
|
|
525
|
+
if (/^deleted file mode /m.test(diffChunk)) {
|
|
526
|
+
return "deleted";
|
|
527
|
+
}
|
|
528
|
+
if (/^similarity index /m.test(diffChunk) && /^rename from /m.test(diffChunk) && /^rename to /m.test(diffChunk)) {
|
|
529
|
+
return "renamed";
|
|
530
|
+
}
|
|
531
|
+
if (/^copy from /m.test(diffChunk) && /^copy to /m.test(diffChunk)) {
|
|
532
|
+
return "copied";
|
|
533
|
+
}
|
|
534
|
+
return "modified";
|
|
535
|
+
}
|
|
536
|
+
export function parseGitStatusPorcelainV1(rawStatus) {
|
|
537
|
+
const records = rawStatus.split("\0").filter(Boolean);
|
|
538
|
+
const entries = [];
|
|
539
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
540
|
+
const record = records[index] ?? "";
|
|
541
|
+
if (record.length < 4) {
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
const indexStatus = record[0] ?? " ";
|
|
545
|
+
const workingTreeStatus = record[1] ?? " ";
|
|
546
|
+
const filePath = normalizeHarnessGitPath(record.slice(3));
|
|
547
|
+
let oldPath;
|
|
548
|
+
if (indexStatus === "R" || indexStatus === "C") {
|
|
549
|
+
const oldPathRecord = records[index + 1];
|
|
550
|
+
if (oldPathRecord) {
|
|
551
|
+
oldPath = normalizeHarnessGitPath(oldPathRecord);
|
|
552
|
+
index += 1;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
entries.push({
|
|
556
|
+
path: filePath,
|
|
557
|
+
oldPath,
|
|
558
|
+
indexStatus,
|
|
559
|
+
workingTreeStatus
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
return entries;
|
|
563
|
+
}
|
|
564
|
+
export function classifyRepositoryDiffPath(repoRelativePath) {
|
|
565
|
+
const filePath = normalizeHarnessGitPath(repoRelativePath);
|
|
566
|
+
if (filePath === "CLAUDE.md" ||
|
|
567
|
+
filePath === ".gitignore" ||
|
|
568
|
+
filePath === ".ai/vcm-harness-manifest.json" ||
|
|
569
|
+
filePath === ".github/pull_request_template.md" ||
|
|
570
|
+
filePath === ".claude/settings.json" ||
|
|
571
|
+
filePath === ".claude/settings.local.json" ||
|
|
572
|
+
filePath.startsWith(".claude/agents/") ||
|
|
573
|
+
filePath.startsWith(".claude/skills/")) {
|
|
574
|
+
return "fixed_harness";
|
|
575
|
+
}
|
|
576
|
+
if (filePath.startsWith(".ai/tools/") || filePath.startsWith(".github/workflows/")) {
|
|
577
|
+
return "tools_hooks";
|
|
578
|
+
}
|
|
579
|
+
if (filePath.startsWith(".ai/generated/")) {
|
|
580
|
+
return "generated_context";
|
|
581
|
+
}
|
|
582
|
+
if (filePath.startsWith("docs/") ||
|
|
583
|
+
filePath === "ARCHITECTURE.md" ||
|
|
584
|
+
filePath.endsWith("/ARCHITECTURE.md") ||
|
|
585
|
+
filePath === "TESTING.md" ||
|
|
586
|
+
filePath.endsWith("/TESTING.md")) {
|
|
587
|
+
return "project_docs";
|
|
588
|
+
}
|
|
589
|
+
return "product_code";
|
|
590
|
+
}
|
|
591
|
+
function truncateRepositoryDiff(diff) {
|
|
592
|
+
if (diff.length <= REPOSITORY_DIFF_MAX_DIFF_CHARS) {
|
|
593
|
+
return { diff, truncated: false };
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
diff: `${diff.slice(0, REPOSITORY_DIFF_MAX_DIFF_CHARS)}\n# ... diff truncated ...\n`,
|
|
597
|
+
truncated: true
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
function countDiffLines(diff) {
|
|
601
|
+
let additions = 0;
|
|
602
|
+
let deletions = 0;
|
|
603
|
+
for (const line of diff.split("\n")) {
|
|
604
|
+
if (line.startsWith("+++") || line.startsWith("---")) {
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
if (line.startsWith("+")) {
|
|
608
|
+
additions += 1;
|
|
609
|
+
}
|
|
610
|
+
else if (line.startsWith("-")) {
|
|
611
|
+
deletions += 1;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return { additions, deletions };
|
|
615
|
+
}
|
|
616
|
+
async function assertHarnessWorktreeClean(git, repoRoot) {
|
|
617
|
+
if (!git) {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const entries = await getVisibleGitStatusEntries(git, repoRoot);
|
|
621
|
+
if (entries.length === 0) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
throw new VcmError({
|
|
625
|
+
code: "HARNESS_WORKTREE_DIRTY",
|
|
626
|
+
message: "The active task worktree has uncommitted Git-visible changes.",
|
|
627
|
+
statusCode: 409,
|
|
628
|
+
hint: `Commit or revert these files before running a harness operation: ${entries.map((entry) => entry.path).slice(0, 12).join(", ")}`
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
async function commitHarnessVisibleChanges(git, repoRoot, message) {
|
|
632
|
+
if (!git) {
|
|
633
|
+
return { changedFiles: [] };
|
|
634
|
+
}
|
|
635
|
+
const entries = await getVisibleGitStatusEntries(git, repoRoot);
|
|
636
|
+
if (entries.length === 0) {
|
|
637
|
+
return { changedFiles: [] };
|
|
638
|
+
}
|
|
639
|
+
const unexpected = entries.filter((entry) => classifyRepositoryDiffPath(entry.path) === "product_code");
|
|
640
|
+
if (unexpected.length > 0) {
|
|
641
|
+
throw new VcmError({
|
|
642
|
+
code: "HARNESS_UNEXPECTED_PRODUCT_CHANGES",
|
|
643
|
+
message: "Harness operation produced product-code changes.",
|
|
644
|
+
statusCode: 409,
|
|
645
|
+
hint: `Review, revert, or move these changes before continuing: ${unexpected.map((entry) => entry.path).slice(0, 12).join(", ")}`
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
const changedFiles = entries.map(statusEntryToHarnessChange);
|
|
649
|
+
const stagePaths = uniqueStrings(entries.flatMap((entry) => [entry.path, entry.oldPath].filter((value) => Boolean(value))));
|
|
650
|
+
await git.addPaths(repoRoot, stagePaths);
|
|
651
|
+
const harnessCommit = await git.commit(repoRoot, message);
|
|
652
|
+
return { harnessCommit, changedFiles };
|
|
653
|
+
}
|
|
654
|
+
async function getVisibleGitStatusEntries(git, repoRoot) {
|
|
655
|
+
const rawStatus = await git.getStatusPorcelainV1(repoRoot);
|
|
656
|
+
return parseGitStatusPorcelainV1(rawStatus).filter((entry) => !isVcmRuntimePath(entry.path));
|
|
657
|
+
}
|
|
658
|
+
function isVcmRuntimePath(repoRelativePath) {
|
|
659
|
+
const filePath = normalizeHarnessGitPath(repoRelativePath);
|
|
660
|
+
return filePath.startsWith(".ai/vcm/") ||
|
|
661
|
+
filePath.startsWith(".claude/worktrees/") ||
|
|
662
|
+
filePath.startsWith(".ai/tools/__pycache__/") ||
|
|
663
|
+
filePath.endsWith("/__pycache__");
|
|
664
|
+
}
|
|
665
|
+
function statusEntryToHarnessChange(entry) {
|
|
666
|
+
return {
|
|
667
|
+
path: entry.path,
|
|
668
|
+
action: statusEntryToHarnessAction(entry),
|
|
669
|
+
reason: "Committed by VCM harness operation."
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
function statusEntryToHarnessAction(entry) {
|
|
673
|
+
if (entry.indexStatus === "?" || entry.indexStatus === "A" || entry.workingTreeStatus === "A") {
|
|
674
|
+
return "create";
|
|
675
|
+
}
|
|
676
|
+
if (entry.indexStatus === "D" || entry.workingTreeStatus === "D") {
|
|
677
|
+
return "delete";
|
|
678
|
+
}
|
|
679
|
+
return "update";
|
|
680
|
+
}
|
|
681
|
+
function uniqueStrings(values) {
|
|
682
|
+
return [...new Set(values)];
|
|
683
|
+
}
|
|
684
|
+
function formatHarnessApplyMessage(harnessCommit, changed) {
|
|
685
|
+
if (!changed) {
|
|
686
|
+
return "VCM Harness is already up to date.";
|
|
687
|
+
}
|
|
688
|
+
return harnessCommit
|
|
689
|
+
? `VCM Harness updated and committed as ${shortCommit(harnessCommit)}. Review the commit diff before approving.`
|
|
690
|
+
: "VCM Harness updated. Review the latest harness commit before approving.";
|
|
691
|
+
}
|
|
503
692
|
async function readHarnessFileContent(fs, repoRoot, filePath) {
|
|
504
693
|
const definition = getHarnessFileDefinition(filePath);
|
|
505
694
|
const absolutePath = resolveHarnessPath(repoRoot, definition.path);
|
|
@@ -915,16 +1104,16 @@ function resolveHarnessPath(repoRoot, relativePath) {
|
|
|
915
1104
|
function ensureTrailingNewline(content) {
|
|
916
1105
|
return content.endsWith("\n") ? content : `${content}\n`;
|
|
917
1106
|
}
|
|
918
|
-
async function getHarnessBootstrapStatus(deps, repoRoot, now) {
|
|
919
|
-
const moduleIndex = await readOptionalJsonObject(deps.fs,
|
|
1107
|
+
async function getHarnessBootstrapStatus(deps, repoRoot, targetRepoRoot, now) {
|
|
1108
|
+
const moduleIndex = await readOptionalJsonObject(deps.fs, targetRepoRoot, ".ai/generated/module-index.json");
|
|
920
1109
|
const checks = [
|
|
921
|
-
await checkFixedHarness(deps.fs,
|
|
922
|
-
await checkProjectContext(deps.fs,
|
|
1110
|
+
await checkFixedHarness(deps.fs, targetRepoRoot),
|
|
1111
|
+
await checkProjectContext(deps.fs, targetRepoRoot),
|
|
923
1112
|
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,
|
|
1113
|
+
checkGeneratedJson(await readOptionalJsonObject(deps.fs, targetRepoRoot, ".ai/generated/public-surface.json"), ".ai/generated/public-surface.json", "public-surface", "Public surface"),
|
|
1114
|
+
await checkFilledMarkdown(deps.fs, targetRepoRoot, "docs/ARCHITECTURE.md", "Project architecture", "project-architecture"),
|
|
1115
|
+
await checkModuleArchitectureDocs(deps.fs, targetRepoRoot, moduleIndex),
|
|
1116
|
+
await checkFilledMarkdown(deps.fs, targetRepoRoot, "docs/TESTING.md", "Testing doc", "testing-doc")
|
|
928
1117
|
];
|
|
929
1118
|
const runState = await loadPersistedHarnessBootstrapRunState(deps.fs, repoRoot);
|
|
930
1119
|
const session = await getCurrentHarnessEngineerBootstrapSession(deps, repoRoot);
|
|
@@ -1229,21 +1418,26 @@ function bootstrapWarnings(status, checks, session, runState) {
|
|
|
1229
1418
|
}
|
|
1230
1419
|
return warnings;
|
|
1231
1420
|
}
|
|
1232
|
-
function buildHarnessBootstrapPrompt(
|
|
1233
|
-
return `Use the vcm-harness-bootstrap skill to finish the VCM harness bootstrap for
|
|
1421
|
+
function buildHarnessBootstrapPrompt(baseRepoRoot, targetRepoRoot) {
|
|
1422
|
+
return `Use the vcm-harness-bootstrap skill to finish the VCM harness bootstrap for the active task worktree.
|
|
1423
|
+
|
|
1424
|
+
Base repository root:
|
|
1425
|
+
${baseRepoRoot}
|
|
1234
1426
|
|
|
1235
|
-
|
|
1236
|
-
${
|
|
1427
|
+
Target task worktree:
|
|
1428
|
+
${targetRepoRoot}
|
|
1237
1429
|
|
|
1238
1430
|
Required work:
|
|
1239
|
-
-
|
|
1240
|
-
- Run .ai/tools/generate-
|
|
1241
|
-
-
|
|
1242
|
-
-
|
|
1243
|
-
-
|
|
1244
|
-
-
|
|
1431
|
+
- Work only inside the target task worktree.
|
|
1432
|
+
- Run .ai/tools/generate-module-index from the target task worktree when available.
|
|
1433
|
+
- Run .ai/tools/generate-public-surface from the target task worktree after module-index.json exists.
|
|
1434
|
+
- Add or update project-specific Project Context and Project Constraints in target CLAUDE.md above the VCM managed block.
|
|
1435
|
+
- Fill target docs/ARCHITECTURE.md with project-level module overview, responsibilities, relationships, dependency direction, project-wide constraints, and links to module-level architecture docs.
|
|
1436
|
+
- Create or update target module-level ARCHITECTURE.md files for clear module boundaries listed by module-index.json.
|
|
1437
|
+
- 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.
|
|
1245
1438
|
|
|
1246
1439
|
Boundaries:
|
|
1440
|
+
- Do not write to the base repository root.
|
|
1247
1441
|
- Do not edit product source, product tests, package manifests, lockfiles, deployment config, secrets, or VCM managed blocks.
|
|
1248
1442
|
- Preserve user-authored content.
|
|
1249
1443
|
- Do not create new validation wrapper tools.
|