vibe-coding-master 0.3.32 → 0.4.1
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/README.md +1 -1
- package/dist/backend/api/harness-routes.js +56 -0
- package/dist/backend/api/session-routes.js +5 -0
- package/dist/backend/api/translation-routes.js +4 -0
- package/dist/backend/cli/install-vcm-harness.js +12 -0
- package/dist/backend/server.js +10 -8
- package/dist/backend/services/claude-hook-service.js +42 -2
- package/dist/backend/services/harness-revision.js +32 -0
- package/dist/backend/services/harness-service.js +324 -118
- package/dist/backend/services/session-service.js +385 -13
- package/dist/backend/templates/harness/harness-engineer-agent.js +63 -0
- package/dist/shared/constants.js +11 -1
- package/dist-frontend/assets/index-kJDZAzjD.css +32 -0
- package/dist-frontend/assets/index-uYhgIwNK.js +94 -0
- package/dist-frontend/index.html +2 -2
- package/docs/full-harness-baseline.md +1 -1
- package/docs/product-design.md +1 -1
- package/docs/v0.4-harness-optimization-plan.md +662 -0
- package/docs/{v0.4-custom-workflow-plan.md → v0.5-custom-workflow-plan.md} +15 -11
- package/docs/vcm-cc-best-practices.md +7 -12
- package/package.json +1 -1
- package/dist-frontend/assets/index-BaIbh99h.js +0 -94
- package/dist-frontend/assets/index-BvUHLq4X.css +0 -32
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import { execFile } from "node:child_process";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { promisify } from "node:util";
|
|
5
4
|
import { renderArchitectHarnessRules } from "../templates/harness/architect-agent.js";
|
|
6
5
|
import { renderCoderHarnessRules } from "../templates/harness/coder-agent.js";
|
|
7
6
|
import { renderGateReviewerAgentRules, renderRequestGateReviewTool, renderTranslatorAgentRules, renderVcmGateReviewSkillRules } from "../templates/harness/gate-review.js";
|
|
7
|
+
import { renderHarnessEngineerHarnessRules } from "../templates/harness/harness-engineer-agent.js";
|
|
8
8
|
import { renderRootClaudeHarnessRules } from "../templates/harness/claude-root.js";
|
|
9
9
|
import { renderGitignoreHarnessRules } from "../templates/harness/gitignore.js";
|
|
10
10
|
import { renderProjectManagerHarnessRules } from "../templates/harness/project-manager-agent.js";
|
|
@@ -16,11 +16,10 @@ import { renderVcmLongRunningValidationSkillRules } from "../templates/harness/v
|
|
|
16
16
|
import { renderVcmRouteMessageSkillRules } from "../templates/harness/vcm-route-message-skill.js";
|
|
17
17
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
18
18
|
import { VcmError } from "../errors.js";
|
|
19
|
+
import { bumpHarnessRevision, readHarnessRevisionState } from "./harness-revision.js";
|
|
19
20
|
const execFileAsync = promisify(execFile);
|
|
20
|
-
const BOOTSTRAP_TASK_SLUG = "__vcm-harness-bootstrap__";
|
|
21
|
-
const BOOTSTRAP_RUNTIME_ROLE = "project-manager";
|
|
22
|
-
const BOOTSTRAP_LOG_PATH = ".ai/vcm/bootstrap/bootstrap.log";
|
|
23
21
|
const BOOTSTRAP_SESSION_PATH = ".ai/vcm/bootstrap/session.json";
|
|
22
|
+
const HARNESS_ENGINEER_SESSION_PATH = ".ai/vcm/harness-engineer/session.json";
|
|
24
23
|
export const VCM_HARNESS_VERSION = 1;
|
|
25
24
|
const MANAGED_BLOCK_PATTERN = /<!-- VCM:BEGIN(?:\s+version=(\d+))? -->[\s\S]*?<!-- VCM:END -->/m;
|
|
26
25
|
const HASH_MANAGED_BLOCK_PATTERN = /# VCM:BEGIN(?:\s+version=(\d+))?\n[\s\S]*?# VCM:END/m;
|
|
@@ -119,6 +118,13 @@ const HARNESS_FILES = [
|
|
|
119
118
|
frontmatter: renderAgentFrontmatter("translator", "VCM project translation tool role for conversation translation, file translation, bootstrap, and memory updates."),
|
|
120
119
|
renderRules: renderTranslatorAgentRules
|
|
121
120
|
},
|
|
121
|
+
{
|
|
122
|
+
kind: "agent-harness-engineer",
|
|
123
|
+
path: ".claude/agents/harness-engineer.md",
|
|
124
|
+
title: "Harness Engineer Agent",
|
|
125
|
+
frontmatter: renderAgentFrontmatter("harness-engineer", "VCM project-scoped harness maintenance role for harness diagnosis, diff proposals, and VCM issue drafts."),
|
|
126
|
+
renderRules: renderHarnessEngineerHarnessRules
|
|
127
|
+
},
|
|
122
128
|
{
|
|
123
129
|
kind: "tool-request-gate-review",
|
|
124
130
|
path: ".ai/tools/request-gate-review",
|
|
@@ -162,11 +168,46 @@ export function createHarnessService(deps) {
|
|
|
162
168
|
async getHarnessStatus(repoRoot) {
|
|
163
169
|
const analyses = await analyzeHarnessFiles(deps.fs, repoRoot);
|
|
164
170
|
const legacyChanges = await analyzeLegacyCodexHarnessPaths(deps.fs, repoRoot);
|
|
165
|
-
return renderHarnessStatus(analyses, legacyChanges);
|
|
171
|
+
return renderHarnessStatus(await readHarnessRevisionValue(deps.fs, repoRoot), analyses, legacyChanges);
|
|
172
|
+
},
|
|
173
|
+
async getHarnessFileContent(repoRoot, filePath) {
|
|
174
|
+
return readHarnessFileContent(deps.fs, repoRoot, filePath);
|
|
175
|
+
},
|
|
176
|
+
async updateHarnessFileContent(repoRoot, filePath, content) {
|
|
177
|
+
const definition = getHarnessFileDefinition(filePath);
|
|
178
|
+
if (!isProjectEditableHarnessFile(definition)) {
|
|
179
|
+
throw new VcmError({
|
|
180
|
+
code: "HARNESS_FILE_READONLY",
|
|
181
|
+
message: "This harness file is managed by VCM and cannot be edited directly.",
|
|
182
|
+
statusCode: 409,
|
|
183
|
+
hint: getReadonlyHarnessReason(definition)
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
const absolutePath = resolveHarnessPath(repoRoot, definition.path);
|
|
187
|
+
const currentContent = await deps.fs.pathExists(absolutePath)
|
|
188
|
+
? await deps.fs.readText(absolutePath)
|
|
189
|
+
: "";
|
|
190
|
+
assertManagedBlockUnchanged(definition, currentContent, content);
|
|
191
|
+
const nextContent = ensureTrailingNewline(content);
|
|
192
|
+
await deps.fs.writeText(absolutePath, nextContent);
|
|
193
|
+
if (nextContent !== currentContent) {
|
|
194
|
+
await bumpHarnessRevision(deps.fs, repoRoot, now());
|
|
195
|
+
}
|
|
196
|
+
const file = await readHarnessFileContent(deps.fs, repoRoot, definition.path);
|
|
197
|
+
const analyses = await analyzeHarnessFiles(deps.fs, repoRoot);
|
|
198
|
+
const legacyChanges = await analyzeLegacyCodexHarnessPaths(deps.fs, repoRoot);
|
|
199
|
+
return {
|
|
200
|
+
file,
|
|
201
|
+
status: renderHarnessStatus(await readHarnessRevisionValue(deps.fs, repoRoot), analyses, legacyChanges)
|
|
202
|
+
};
|
|
166
203
|
},
|
|
167
204
|
async applyHarness(repoRoot) {
|
|
168
205
|
if (deps.runFixedInstaller) {
|
|
169
|
-
|
|
206
|
+
const result = await deps.runFixedInstaller(repoRoot);
|
|
207
|
+
if (result.changedFiles.length > 0) {
|
|
208
|
+
await bumpHarnessRevision(deps.fs, repoRoot, now());
|
|
209
|
+
}
|
|
210
|
+
return result;
|
|
170
211
|
}
|
|
171
212
|
const analyses = await analyzeHarnessFiles(deps.fs, repoRoot);
|
|
172
213
|
const changedFiles = [];
|
|
@@ -181,6 +222,9 @@ export function createHarnessService(deps) {
|
|
|
181
222
|
}
|
|
182
223
|
const legacyChanges = await removeLegacyCodexHarnessPaths(deps.fs, repoRoot);
|
|
183
224
|
changedFiles.push(...legacyChanges);
|
|
225
|
+
if (changedFiles.length > 0) {
|
|
226
|
+
await bumpHarnessRevision(deps.fs, repoRoot, now());
|
|
227
|
+
}
|
|
184
228
|
return {
|
|
185
229
|
version: VCM_HARNESS_VERSION,
|
|
186
230
|
changedFiles,
|
|
@@ -260,64 +304,70 @@ export function createHarnessService(deps) {
|
|
|
260
304
|
return getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
261
305
|
},
|
|
262
306
|
async startHarnessBootstrap(repoRoot, input = {}) {
|
|
263
|
-
|
|
307
|
+
const session = await ensureHarnessEngineerForBootstrap(deps, repoRoot, now, input);
|
|
308
|
+
const nextStatus = await getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
309
|
+
return {
|
|
310
|
+
status: {
|
|
311
|
+
...nextStatus,
|
|
312
|
+
session
|
|
313
|
+
},
|
|
314
|
+
session,
|
|
315
|
+
prompt: buildHarnessBootstrapPrompt(repoRoot)
|
|
316
|
+
};
|
|
317
|
+
},
|
|
318
|
+
async restartHarnessBootstrap(repoRoot, input = {}) {
|
|
319
|
+
const session = await restartHarnessEngineerForBootstrap(deps, repoRoot, now, input);
|
|
320
|
+
const nextStatus = await getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
321
|
+
return {
|
|
322
|
+
status: {
|
|
323
|
+
...nextStatus,
|
|
324
|
+
session
|
|
325
|
+
},
|
|
326
|
+
session,
|
|
327
|
+
prompt: buildHarnessBootstrapPrompt(repoRoot)
|
|
328
|
+
};
|
|
329
|
+
},
|
|
330
|
+
async stopHarnessBootstrap(repoRoot) {
|
|
331
|
+
const existing = await getCurrentHarnessEngineerBootstrapSession(deps, repoRoot);
|
|
332
|
+
if (!existing) {
|
|
333
|
+
throw new VcmError({
|
|
334
|
+
code: "HARNESS_BOOTSTRAP_SESSION_MISSING",
|
|
335
|
+
message: "Harness Engineer session has not been started.",
|
|
336
|
+
statusCode: 404
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
await deps.harnessEngineerSessions?.stopProjectHarnessEngineerSession(repoRoot);
|
|
340
|
+
await clearHarnessBootstrapRunState(deps.fs, repoRoot);
|
|
341
|
+
return getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
342
|
+
},
|
|
343
|
+
async runHarnessBootstrap(repoRoot) {
|
|
344
|
+
if (!deps.runtime || !deps.harnessEngineerSessions) {
|
|
264
345
|
throw new VcmError({
|
|
265
346
|
code: "HARNESS_BOOTSTRAP_UNAVAILABLE",
|
|
266
347
|
message: "Harness bootstrap sessions are not available in this VCM runtime.",
|
|
267
348
|
statusCode: 501
|
|
268
349
|
});
|
|
269
350
|
}
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
status: currentStatus,
|
|
274
|
-
session: currentStatus.session,
|
|
275
|
-
prompt: buildHarnessBootstrapPrompt(repoRoot)
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
if (!currentStatus.canStart) {
|
|
351
|
+
const status = await getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
352
|
+
const session = status.session;
|
|
353
|
+
if (!session || session.status !== "running" || !deps.runtime.getSession(session.id)) {
|
|
279
354
|
throw new VcmError({
|
|
280
|
-
code: "
|
|
281
|
-
message: "
|
|
355
|
+
code: "HARNESS_BOOTSTRAP_SESSION_NOT_RUNNING",
|
|
356
|
+
message: "Start the Harness Engineer session before running bootstrap.",
|
|
282
357
|
statusCode: 409
|
|
283
358
|
});
|
|
284
359
|
}
|
|
285
|
-
const config = await deps.projectService.loadConfig(repoRoot);
|
|
286
|
-
const claudeSessionId = randomUUID();
|
|
287
|
-
const command = buildClaudeStartCommand(config.claudeCommand, "default", claudeSessionId);
|
|
288
|
-
const logPath = path.join(repoRoot, BOOTSTRAP_LOG_PATH);
|
|
289
|
-
const runtimeSession = await deps.runtime.createSession({
|
|
290
|
-
taskSlug: BOOTSTRAP_TASK_SLUG,
|
|
291
|
-
role: BOOTSTRAP_RUNTIME_ROLE,
|
|
292
|
-
command: command.command,
|
|
293
|
-
args: command.args,
|
|
294
|
-
cwd: repoRoot,
|
|
295
|
-
env: {
|
|
296
|
-
VCM_API_URL: deps.apiUrl,
|
|
297
|
-
VCM_TASK_REPO_ROOT: repoRoot,
|
|
298
|
-
VCM_HARNESS_BOOTSTRAP: "1",
|
|
299
|
-
VCM_SESSION_ID: claudeSessionId
|
|
300
|
-
},
|
|
301
|
-
cols: input.cols,
|
|
302
|
-
rows: input.rows,
|
|
303
|
-
logPath
|
|
304
|
-
});
|
|
305
|
-
const timestamp = now();
|
|
306
|
-
const session = {
|
|
307
|
-
id: runtimeSession.id,
|
|
308
|
-
claudeSessionId,
|
|
309
|
-
status: runtimeSession.status === "crashed" ? "crashed" : runtimeSession.status === "exited" ? "exited" : "running",
|
|
310
|
-
command: command.display,
|
|
311
|
-
cwd: repoRoot,
|
|
312
|
-
logPath: BOOTSTRAP_LOG_PATH,
|
|
313
|
-
startedAt: runtimeSession.startedAt,
|
|
314
|
-
updatedAt: timestamp,
|
|
315
|
-
lastOutputAt: runtimeSession.lastOutputAt,
|
|
316
|
-
exitCode: runtimeSession.exitCode
|
|
317
|
-
};
|
|
318
|
-
await persistHarnessBootstrapSession(deps.fs, repoRoot, session);
|
|
319
360
|
const prompt = buildHarnessBootstrapPrompt(repoRoot);
|
|
320
|
-
|
|
361
|
+
const timestamp = now();
|
|
362
|
+
await persistHarnessBootstrapRunState(deps.fs, repoRoot, {
|
|
363
|
+
version: 1,
|
|
364
|
+
status: "running",
|
|
365
|
+
sessionId: session.id,
|
|
366
|
+
claudeSessionId: session.claudeSessionId,
|
|
367
|
+
startedAt: timestamp,
|
|
368
|
+
updatedAt: timestamp
|
|
369
|
+
});
|
|
370
|
+
await submitTerminalInput(deps.runtime, session.id, prompt);
|
|
321
371
|
const nextStatus = await getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
322
372
|
return {
|
|
323
373
|
status: {
|
|
@@ -327,6 +377,21 @@ export function createHarnessService(deps) {
|
|
|
327
377
|
session,
|
|
328
378
|
prompt
|
|
329
379
|
};
|
|
380
|
+
},
|
|
381
|
+
async recordHarnessBootstrapHook(repoRoot, input) {
|
|
382
|
+
const state = await loadPersistedHarnessBootstrapRunState(deps.fs, repoRoot);
|
|
383
|
+
if (state?.status !== "running" || !matchesBootstrapRunState(state, input)) {
|
|
384
|
+
return getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
385
|
+
}
|
|
386
|
+
const timestamp = now();
|
|
387
|
+
await persistHarnessBootstrapRunState(deps.fs, repoRoot, {
|
|
388
|
+
...state,
|
|
389
|
+
status: input.eventName === "Stop" ? "complete" : state.status,
|
|
390
|
+
completedAt: input.eventName === "Stop" ? timestamp : state.completedAt,
|
|
391
|
+
updatedAt: timestamp,
|
|
392
|
+
lastHookEvent: input.eventName
|
|
393
|
+
});
|
|
394
|
+
return getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
330
395
|
}
|
|
331
396
|
};
|
|
332
397
|
}
|
|
@@ -346,6 +411,72 @@ function dedupeHarnessChanges(changedFiles) {
|
|
|
346
411
|
}
|
|
347
412
|
return changes;
|
|
348
413
|
}
|
|
414
|
+
async function ensureHarnessEngineerForBootstrap(deps, repoRoot, now, input) {
|
|
415
|
+
if (!deps.harnessEngineerSessions) {
|
|
416
|
+
throw new VcmError({
|
|
417
|
+
code: "HARNESS_BOOTSTRAP_UNAVAILABLE",
|
|
418
|
+
message: "Harness bootstrap sessions are not available in this VCM runtime.",
|
|
419
|
+
statusCode: 501
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
const currentStatus = await getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
423
|
+
if (currentStatus.session?.status === "running") {
|
|
424
|
+
return currentStatus.session;
|
|
425
|
+
}
|
|
426
|
+
if (!currentStatus.canStart) {
|
|
427
|
+
throw new VcmError({
|
|
428
|
+
code: "HARNESS_BOOTSTRAP_NOT_READY",
|
|
429
|
+
message: "Install the fixed VCM harness before starting harness bootstrap.",
|
|
430
|
+
statusCode: 409
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
return toHarnessBootstrapSession(await deps.harnessEngineerSessions.ensureProjectHarnessEngineerSession(repoRoot, input));
|
|
434
|
+
}
|
|
435
|
+
async function restartHarnessEngineerForBootstrap(deps, repoRoot, now, input) {
|
|
436
|
+
if (!deps.harnessEngineerSessions) {
|
|
437
|
+
throw new VcmError({
|
|
438
|
+
code: "HARNESS_BOOTSTRAP_UNAVAILABLE",
|
|
439
|
+
message: "Harness bootstrap sessions are not available in this VCM runtime.",
|
|
440
|
+
statusCode: 501
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
const currentStatus = await getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
444
|
+
if (!currentStatus.canStart) {
|
|
445
|
+
throw new VcmError({
|
|
446
|
+
code: "HARNESS_BOOTSTRAP_NOT_READY",
|
|
447
|
+
message: "Install the fixed VCM harness before starting harness bootstrap.",
|
|
448
|
+
statusCode: 409
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
return toHarnessBootstrapSession(await deps.harnessEngineerSessions.restartProjectHarnessEngineerSession(repoRoot, input));
|
|
452
|
+
}
|
|
453
|
+
async function getCurrentHarnessEngineerBootstrapSession(deps, repoRoot) {
|
|
454
|
+
const session = await deps.harnessEngineerSessions?.getProjectHarnessEngineerSession(repoRoot);
|
|
455
|
+
return session ? toHarnessBootstrapSession(session) : undefined;
|
|
456
|
+
}
|
|
457
|
+
function toHarnessBootstrapSession(session) {
|
|
458
|
+
return {
|
|
459
|
+
id: session.id,
|
|
460
|
+
claudeSessionId: session.claudeSessionId,
|
|
461
|
+
status: toBootstrapSessionStatus(session.status),
|
|
462
|
+
command: session.command,
|
|
463
|
+
permissionMode: session.permissionMode,
|
|
464
|
+
model: session.model,
|
|
465
|
+
effort: session.effort,
|
|
466
|
+
cwd: session.cwd,
|
|
467
|
+
logPath: HARNESS_ENGINEER_SESSION_PATH,
|
|
468
|
+
startedAt: session.startedAt,
|
|
469
|
+
updatedAt: session.updatedAt,
|
|
470
|
+
lastOutputAt: session.lastOutputAt,
|
|
471
|
+
exitCode: session.exitCode
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
function toBootstrapSessionStatus(status) {
|
|
475
|
+
if (status === "running" || status === "crashed" || status === "exited" || status === "resumable") {
|
|
476
|
+
return status;
|
|
477
|
+
}
|
|
478
|
+
return status === "missing" ? "resumable" : "exited";
|
|
479
|
+
}
|
|
349
480
|
function normalizeHarnessGitPath(value) {
|
|
350
481
|
if (!value || value.includes("\0") || path.posix.isAbsolute(value)) {
|
|
351
482
|
throw new VcmError({
|
|
@@ -369,6 +500,104 @@ function normalizeHarnessGitPath(value) {
|
|
|
369
500
|
function shortCommit(commit) {
|
|
370
501
|
return commit.slice(0, 7);
|
|
371
502
|
}
|
|
503
|
+
async function readHarnessFileContent(fs, repoRoot, filePath) {
|
|
504
|
+
const definition = getHarnessFileDefinition(filePath);
|
|
505
|
+
const absolutePath = resolveHarnessPath(repoRoot, definition.path);
|
|
506
|
+
const exists = await fs.pathExists(absolutePath);
|
|
507
|
+
const content = exists ? await fs.readText(absolutePath) : "";
|
|
508
|
+
const editable = isProjectEditableHarnessFile(definition);
|
|
509
|
+
return {
|
|
510
|
+
path: definition.path,
|
|
511
|
+
kind: definition.kind,
|
|
512
|
+
title: definition.title,
|
|
513
|
+
content,
|
|
514
|
+
editable,
|
|
515
|
+
readonlyReason: editable ? undefined : getReadonlyHarnessReason(definition)
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function getHarnessFileDefinition(filePath) {
|
|
519
|
+
const normalizedPath = normalizeHarnessFilePath(filePath);
|
|
520
|
+
const definition = HARNESS_FILES.find((file) => file.path === normalizedPath) ?? getSpecialHarnessFileDefinition(normalizedPath);
|
|
521
|
+
if (!definition) {
|
|
522
|
+
throw new VcmError({
|
|
523
|
+
code: "HARNESS_FILE_UNKNOWN",
|
|
524
|
+
message: "This path is not a known VCM harness file.",
|
|
525
|
+
statusCode: 404,
|
|
526
|
+
hint: normalizedPath
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
return definition;
|
|
530
|
+
}
|
|
531
|
+
function getSpecialHarnessFileDefinition(filePath) {
|
|
532
|
+
if (filePath !== CLAUDE_SETTINGS_PATH) {
|
|
533
|
+
return undefined;
|
|
534
|
+
}
|
|
535
|
+
return {
|
|
536
|
+
kind: "claude-settings",
|
|
537
|
+
path: CLAUDE_SETTINGS_PATH,
|
|
538
|
+
title: "Claude Code Settings",
|
|
539
|
+
ownership: "raw-file",
|
|
540
|
+
renderRules: () => ""
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
function normalizeHarnessFilePath(filePath) {
|
|
544
|
+
if (!filePath || filePath.includes("\0") || path.posix.isAbsolute(filePath)) {
|
|
545
|
+
throw new VcmError({
|
|
546
|
+
code: "HARNESS_FILE_PATH_INVALID",
|
|
547
|
+
message: "Harness file path is invalid.",
|
|
548
|
+
statusCode: 400,
|
|
549
|
+
hint: filePath
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
const normalized = path.posix.normalize(filePath).replace(/^\.\//, "");
|
|
553
|
+
if (normalized === "." || normalized.startsWith("../")) {
|
|
554
|
+
throw new VcmError({
|
|
555
|
+
code: "HARNESS_FILE_PATH_INVALID",
|
|
556
|
+
message: "Harness file path must stay inside the repository.",
|
|
557
|
+
statusCode: 400,
|
|
558
|
+
hint: filePath
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
return normalized;
|
|
562
|
+
}
|
|
563
|
+
function isProjectEditableHarnessFile(definition) {
|
|
564
|
+
return definition.ownership !== "whole-file" && definition.ownership !== "raw-file";
|
|
565
|
+
}
|
|
566
|
+
function getReadonlyHarnessReason(definition) {
|
|
567
|
+
if (definition.path === CLAUDE_SETTINGS_PATH) {
|
|
568
|
+
return "Claude Code hooks are generated by VCM fixed harness install.";
|
|
569
|
+
}
|
|
570
|
+
if (definition.ownership === "whole-file" || definition.ownership === "raw-file") {
|
|
571
|
+
return "This file is VCM-owned whole-file template content. Use fixed harness update instead.";
|
|
572
|
+
}
|
|
573
|
+
return "This harness file is not editable from Harness Studio.";
|
|
574
|
+
}
|
|
575
|
+
function assertManagedBlockUnchanged(definition, currentContent, nextContent) {
|
|
576
|
+
const currentBlock = extractManagedBlock(definition, currentContent);
|
|
577
|
+
const nextBlock = extractManagedBlock(definition, nextContent);
|
|
578
|
+
if (currentBlock && nextBlock !== currentBlock) {
|
|
579
|
+
throw new VcmError({
|
|
580
|
+
code: "HARNESS_MANAGED_BLOCK_PROTECTED",
|
|
581
|
+
message: "VCM fixed managed blocks cannot be edited from Harness Studio.",
|
|
582
|
+
statusCode: 409,
|
|
583
|
+
hint: "Edit project-specific content outside the VCM:BEGIN / VCM:END block, or use fixed harness update."
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
if (!currentBlock && nextBlock) {
|
|
587
|
+
throw new VcmError({
|
|
588
|
+
code: "HARNESS_MANAGED_BLOCK_PROTECTED",
|
|
589
|
+
message: "VCM fixed managed blocks must be installed by VCM.",
|
|
590
|
+
statusCode: 409,
|
|
591
|
+
hint: "Use fixed harness update instead of adding a VCM managed block manually."
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
function extractManagedBlock(definition, content) {
|
|
596
|
+
if (definition.ownership === "whole-file" || definition.ownership === "raw-file") {
|
|
597
|
+
return undefined;
|
|
598
|
+
}
|
|
599
|
+
return content.match(getManagedBlockPattern(definition))?.[0];
|
|
600
|
+
}
|
|
372
601
|
async function analyzeHarnessFiles(fs, repoRoot) {
|
|
373
602
|
const analyses = [];
|
|
374
603
|
for (const definition of HARNESS_FILES) {
|
|
@@ -508,7 +737,7 @@ async function removeLegacyCodexHarnessPaths(fs, repoRoot) {
|
|
|
508
737
|
}
|
|
509
738
|
return changes;
|
|
510
739
|
}
|
|
511
|
-
function renderHarnessStatus(analyses, legacyChanges = []) {
|
|
740
|
+
function renderHarnessStatus(harnessRevision, analyses, legacyChanges = []) {
|
|
512
741
|
const files = analyses.map((analysis) => analysis.status);
|
|
513
742
|
const plannedChanges = analyses
|
|
514
743
|
.map((analysis) => analysis.plannedChange)
|
|
@@ -528,6 +757,7 @@ function renderHarnessStatus(analyses, legacyChanges = []) {
|
|
|
528
757
|
analysis.status.exists));
|
|
529
758
|
return {
|
|
530
759
|
version: VCM_HARNESS_VERSION,
|
|
760
|
+
harnessRevision,
|
|
531
761
|
initialized,
|
|
532
762
|
files,
|
|
533
763
|
needsApply: plannedChanges.length > 0,
|
|
@@ -537,6 +767,9 @@ function renderHarnessStatus(analyses, legacyChanges = []) {
|
|
|
537
767
|
: []
|
|
538
768
|
};
|
|
539
769
|
}
|
|
770
|
+
async function readHarnessRevisionValue(fs, repoRoot) {
|
|
771
|
+
return (await readHarnessRevisionState(fs, repoRoot)).revision;
|
|
772
|
+
}
|
|
540
773
|
function renderManagedBlock(definition, rules) {
|
|
541
774
|
const endSpacing = definition.blankLineBeforeEnd ? "\n\n" : "\n";
|
|
542
775
|
if (definition.commentStyle === "hash") {
|
|
@@ -693,26 +926,28 @@ async function getHarnessBootstrapStatus(deps, repoRoot, now) {
|
|
|
693
926
|
await checkModuleArchitectureDocs(deps.fs, repoRoot, moduleIndex),
|
|
694
927
|
await checkFilledMarkdown(deps.fs, repoRoot, "docs/TESTING.md", "Testing doc", "testing-doc")
|
|
695
928
|
];
|
|
696
|
-
const
|
|
929
|
+
const runState = await loadPersistedHarnessBootstrapRunState(deps.fs, repoRoot);
|
|
930
|
+
const session = await getCurrentHarnessEngineerBootstrapSession(deps, repoRoot);
|
|
697
931
|
const fixedHarnessReady = checks[0]?.status === "ok";
|
|
698
932
|
const projectChecks = checks.slice(1);
|
|
699
933
|
const projectComplete = projectChecks.every((check) => check.status === "ok");
|
|
700
934
|
const projectStarted = projectChecks.some((check) => check.status === "ok" || check.status === "incomplete");
|
|
935
|
+
const runActive = runState?.status === "running" && session?.status === "running";
|
|
701
936
|
const status = !fixedHarnessReady
|
|
702
937
|
? "not_ready"
|
|
703
|
-
:
|
|
938
|
+
: runActive
|
|
704
939
|
? "running"
|
|
705
|
-
: projectComplete
|
|
940
|
+
: runState?.status === "complete" || projectComplete
|
|
706
941
|
? "complete"
|
|
707
942
|
: projectStarted
|
|
708
943
|
? "incomplete"
|
|
709
944
|
: "not_started";
|
|
710
945
|
return {
|
|
711
946
|
status,
|
|
712
|
-
canStart: fixedHarnessReady &&
|
|
947
|
+
canStart: fixedHarnessReady && !runActive,
|
|
713
948
|
checks,
|
|
714
949
|
session,
|
|
715
|
-
warnings: bootstrapWarnings(status, checks, session)
|
|
950
|
+
warnings: bootstrapWarnings(status, checks, session, runState)
|
|
716
951
|
};
|
|
717
952
|
}
|
|
718
953
|
async function checkFixedHarness(fs, repoRoot) {
|
|
@@ -913,68 +1148,47 @@ function isFilledMarkdown(content) {
|
|
|
913
1148
|
.filter((line) => line && !line.startsWith("#"));
|
|
914
1149
|
return meaningfulLines.join("\n").length >= 120;
|
|
915
1150
|
}
|
|
916
|
-
async function
|
|
917
|
-
const persisted = await loadPersistedHarnessBootstrapSession(deps.fs, repoRoot);
|
|
918
|
-
const runtimeSession = deps.runtime
|
|
919
|
-
?.listSessions(BOOTSTRAP_TASK_SLUG)
|
|
920
|
-
.find((session) => session.id === persisted?.id || session.status === "running");
|
|
921
|
-
if (runtimeSession) {
|
|
922
|
-
const session = {
|
|
923
|
-
id: runtimeSession.id,
|
|
924
|
-
claudeSessionId: persisted?.claudeSessionId ?? runtimeSession.id,
|
|
925
|
-
status: runtimeSession.status === "crashed" ? "crashed" : runtimeSession.status === "exited" ? "exited" : "running",
|
|
926
|
-
command: persisted?.command ?? "claude",
|
|
927
|
-
cwd: persisted?.cwd ?? repoRoot,
|
|
928
|
-
logPath: persisted?.logPath ?? BOOTSTRAP_LOG_PATH,
|
|
929
|
-
startedAt: runtimeSession.startedAt,
|
|
930
|
-
updatedAt: now(),
|
|
931
|
-
lastOutputAt: runtimeSession.lastOutputAt,
|
|
932
|
-
exitCode: runtimeSession.exitCode
|
|
933
|
-
};
|
|
934
|
-
await persistHarnessBootstrapSession(deps.fs, repoRoot, session);
|
|
935
|
-
return session;
|
|
936
|
-
}
|
|
937
|
-
if (persisted?.status === "running") {
|
|
938
|
-
return {
|
|
939
|
-
...persisted,
|
|
940
|
-
status: "resumable",
|
|
941
|
-
updatedAt: now()
|
|
942
|
-
};
|
|
943
|
-
}
|
|
944
|
-
return undefined;
|
|
945
|
-
}
|
|
946
|
-
async function loadPersistedHarnessBootstrapSession(fs, repoRoot) {
|
|
1151
|
+
async function loadPersistedHarnessBootstrapRunState(fs, repoRoot) {
|
|
947
1152
|
const payload = await readOptionalJsonObject(fs, repoRoot, BOOTSTRAP_SESSION_PATH);
|
|
948
1153
|
if (!payload) {
|
|
949
1154
|
return undefined;
|
|
950
1155
|
}
|
|
951
|
-
if (
|
|
952
|
-
typeof payload.claudeSessionId !== "string" ||
|
|
953
|
-
typeof payload.command !== "string" ||
|
|
954
|
-
typeof payload.cwd !== "string" ||
|
|
955
|
-
typeof payload.logPath !== "string" ||
|
|
1156
|
+
if (payload.version !== 1 ||
|
|
956
1157
|
typeof payload.updatedAt !== "string") {
|
|
957
1158
|
return undefined;
|
|
958
1159
|
}
|
|
959
1160
|
const status = payload.status;
|
|
960
|
-
if (status !== "running" && status !== "
|
|
1161
|
+
if (status !== "running" && status !== "complete") {
|
|
961
1162
|
return undefined;
|
|
962
1163
|
}
|
|
963
1164
|
return {
|
|
964
|
-
|
|
965
|
-
claudeSessionId: payload.claudeSessionId,
|
|
1165
|
+
version: 1,
|
|
966
1166
|
status,
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
logPath: payload.logPath,
|
|
1167
|
+
sessionId: typeof payload.sessionId === "string" ? payload.sessionId : undefined,
|
|
1168
|
+
claudeSessionId: typeof payload.claudeSessionId === "string" ? payload.claudeSessionId : undefined,
|
|
970
1169
|
startedAt: typeof payload.startedAt === "string" ? payload.startedAt : undefined,
|
|
1170
|
+
completedAt: typeof payload.completedAt === "string" ? payload.completedAt : undefined,
|
|
971
1171
|
updatedAt: payload.updatedAt,
|
|
972
|
-
|
|
973
|
-
exitCode: typeof payload.exitCode === "number" || payload.exitCode === null ? payload.exitCode : undefined
|
|
1172
|
+
lastHookEvent: isHarnessBootstrapHookEvent(payload.lastHookEvent) ? payload.lastHookEvent : undefined
|
|
974
1173
|
};
|
|
975
1174
|
}
|
|
976
|
-
async function
|
|
977
|
-
await fs.writeJsonAtomic(resolveHarnessPath(repoRoot, BOOTSTRAP_SESSION_PATH),
|
|
1175
|
+
async function persistHarnessBootstrapRunState(fs, repoRoot, state) {
|
|
1176
|
+
await fs.writeJsonAtomic(resolveHarnessPath(repoRoot, BOOTSTRAP_SESSION_PATH), state);
|
|
1177
|
+
}
|
|
1178
|
+
async function clearHarnessBootstrapRunState(fs, repoRoot) {
|
|
1179
|
+
await fs.removePath?.(resolveHarnessPath(repoRoot, BOOTSTRAP_SESSION_PATH), { force: true });
|
|
1180
|
+
}
|
|
1181
|
+
function isHarnessBootstrapHookEvent(value) {
|
|
1182
|
+
return value === "Stop" || value === "StopFailure" || value === "UserPromptSubmit" || value === "PostCompact";
|
|
1183
|
+
}
|
|
1184
|
+
function matchesBootstrapRunState(state, input) {
|
|
1185
|
+
if (state.sessionId && input.sessionId && state.sessionId !== input.sessionId) {
|
|
1186
|
+
return false;
|
|
1187
|
+
}
|
|
1188
|
+
if (state.claudeSessionId && input.claudeSessionId && state.claudeSessionId !== input.claudeSessionId) {
|
|
1189
|
+
return false;
|
|
1190
|
+
}
|
|
1191
|
+
return true;
|
|
978
1192
|
}
|
|
979
1193
|
async function readOptionalText(fs, repoRoot, relativePath) {
|
|
980
1194
|
const absolutePath = resolveHarnessPath(repoRoot, relativePath);
|
|
@@ -998,13 +1212,16 @@ async function readOptionalJsonObject(fs, repoRoot, relativePath) {
|
|
|
998
1212
|
return undefined;
|
|
999
1213
|
}
|
|
1000
1214
|
}
|
|
1001
|
-
function bootstrapWarnings(status, checks, session) {
|
|
1215
|
+
function bootstrapWarnings(status, checks, session, runState) {
|
|
1002
1216
|
const warnings = [];
|
|
1003
1217
|
if (status === "not_ready") {
|
|
1004
1218
|
warnings.push("Install or update the fixed VCM harness before running harness bootstrap.");
|
|
1005
1219
|
}
|
|
1006
1220
|
if (session?.status === "resumable") {
|
|
1007
|
-
warnings.push("The
|
|
1221
|
+
warnings.push("The Harness Engineer session is resumable; resume it before running harness bootstrap.");
|
|
1222
|
+
}
|
|
1223
|
+
if (runState?.status === "running" && session?.status !== "running") {
|
|
1224
|
+
warnings.push("The previous bootstrap run did not finish with an active Harness Engineer session; resume the session or run bootstrap again.");
|
|
1008
1225
|
}
|
|
1009
1226
|
const unknownChecks = checks.filter((check) => check.status === "unknown");
|
|
1010
1227
|
if (unknownChecks.length > 0) {
|
|
@@ -1012,17 +1229,6 @@ function bootstrapWarnings(status, checks, session) {
|
|
|
1012
1229
|
}
|
|
1013
1230
|
return warnings;
|
|
1014
1231
|
}
|
|
1015
|
-
function buildClaudeStartCommand(command = "claude", permissionMode = "default", claudeSessionId) {
|
|
1016
|
-
const args = ["--session-id", claudeSessionId];
|
|
1017
|
-
if (permissionMode === "bypassPermissions") {
|
|
1018
|
-
args.push("--permission-mode", "bypassPermissions");
|
|
1019
|
-
}
|
|
1020
|
-
return {
|
|
1021
|
-
command,
|
|
1022
|
-
args,
|
|
1023
|
-
display: `${command} ${args.join(" ")}`
|
|
1024
|
-
};
|
|
1025
|
-
}
|
|
1026
1232
|
function buildHarnessBootstrapPrompt(repoRoot) {
|
|
1027
1233
|
return `Use the vcm-harness-bootstrap skill to finish the VCM harness bootstrap for this repository.
|
|
1028
1234
|
|