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