vibe-coding-master 0.3.32 → 0.4.0
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/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 +2 -0
- package/dist/backend/services/claude-hook-service.js +37 -2
- package/dist/backend/services/harness-revision.js +32 -0
- package/dist/backend/services/harness-service.js +314 -53
- 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-CMuJhDdX.js +94 -0
- package/dist-frontend/assets/index-kJDZAzjD.css +32 -0
- package/dist-frontend/index.html +2 -2
- 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/package.json +1 -1
- package/dist-frontend/assets/index-BaIbh99h.js +0 -94
- package/dist-frontend/assets/index-BvUHLq4X.css +0 -32
|
@@ -5,6 +5,7 @@ import { promisify } from "node:util";
|
|
|
5
5
|
import { renderArchitectHarnessRules } from "../templates/harness/architect-agent.js";
|
|
6
6
|
import { renderCoderHarnessRules } from "../templates/harness/coder-agent.js";
|
|
7
7
|
import { renderGateReviewerAgentRules, renderRequestGateReviewTool, renderTranslatorAgentRules, renderVcmGateReviewSkillRules } from "../templates/harness/gate-review.js";
|
|
8
|
+
import { renderHarnessEngineerHarnessRules } from "../templates/harness/harness-engineer-agent.js";
|
|
8
9
|
import { renderRootClaudeHarnessRules } from "../templates/harness/claude-root.js";
|
|
9
10
|
import { renderGitignoreHarnessRules } from "../templates/harness/gitignore.js";
|
|
10
11
|
import { renderProjectManagerHarnessRules } from "../templates/harness/project-manager-agent.js";
|
|
@@ -16,6 +17,7 @@ import { renderVcmLongRunningValidationSkillRules } from "../templates/harness/v
|
|
|
16
17
|
import { renderVcmRouteMessageSkillRules } from "../templates/harness/vcm-route-message-skill.js";
|
|
17
18
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
18
19
|
import { VcmError } from "../errors.js";
|
|
20
|
+
import { bumpHarnessRevision, readHarnessRevisionState } from "./harness-revision.js";
|
|
19
21
|
const execFileAsync = promisify(execFile);
|
|
20
22
|
const BOOTSTRAP_TASK_SLUG = "__vcm-harness-bootstrap__";
|
|
21
23
|
const BOOTSTRAP_RUNTIME_ROLE = "project-manager";
|
|
@@ -119,6 +121,13 @@ const HARNESS_FILES = [
|
|
|
119
121
|
frontmatter: renderAgentFrontmatter("translator", "VCM project translation tool role for conversation translation, file translation, bootstrap, and memory updates."),
|
|
120
122
|
renderRules: renderTranslatorAgentRules
|
|
121
123
|
},
|
|
124
|
+
{
|
|
125
|
+
kind: "agent-harness-engineer",
|
|
126
|
+
path: ".claude/agents/harness-engineer.md",
|
|
127
|
+
title: "Harness Engineer Agent",
|
|
128
|
+
frontmatter: renderAgentFrontmatter("harness-engineer", "VCM project-scoped harness maintenance role for harness diagnosis, diff proposals, and VCM issue drafts."),
|
|
129
|
+
renderRules: renderHarnessEngineerHarnessRules
|
|
130
|
+
},
|
|
122
131
|
{
|
|
123
132
|
kind: "tool-request-gate-review",
|
|
124
133
|
path: ".ai/tools/request-gate-review",
|
|
@@ -162,11 +171,46 @@ export function createHarnessService(deps) {
|
|
|
162
171
|
async getHarnessStatus(repoRoot) {
|
|
163
172
|
const analyses = await analyzeHarnessFiles(deps.fs, repoRoot);
|
|
164
173
|
const legacyChanges = await analyzeLegacyCodexHarnessPaths(deps.fs, repoRoot);
|
|
165
|
-
return renderHarnessStatus(analyses, legacyChanges);
|
|
174
|
+
return renderHarnessStatus(await readHarnessRevisionValue(deps.fs, repoRoot), analyses, legacyChanges);
|
|
175
|
+
},
|
|
176
|
+
async getHarnessFileContent(repoRoot, filePath) {
|
|
177
|
+
return readHarnessFileContent(deps.fs, repoRoot, filePath);
|
|
178
|
+
},
|
|
179
|
+
async updateHarnessFileContent(repoRoot, filePath, content) {
|
|
180
|
+
const definition = getHarnessFileDefinition(filePath);
|
|
181
|
+
if (!isProjectEditableHarnessFile(definition)) {
|
|
182
|
+
throw new VcmError({
|
|
183
|
+
code: "HARNESS_FILE_READONLY",
|
|
184
|
+
message: "This harness file is managed by VCM and cannot be edited directly.",
|
|
185
|
+
statusCode: 409,
|
|
186
|
+
hint: getReadonlyHarnessReason(definition)
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
const absolutePath = resolveHarnessPath(repoRoot, definition.path);
|
|
190
|
+
const currentContent = await deps.fs.pathExists(absolutePath)
|
|
191
|
+
? await deps.fs.readText(absolutePath)
|
|
192
|
+
: "";
|
|
193
|
+
assertManagedBlockUnchanged(definition, currentContent, content);
|
|
194
|
+
const nextContent = ensureTrailingNewline(content);
|
|
195
|
+
await deps.fs.writeText(absolutePath, nextContent);
|
|
196
|
+
if (nextContent !== currentContent) {
|
|
197
|
+
await bumpHarnessRevision(deps.fs, repoRoot, now());
|
|
198
|
+
}
|
|
199
|
+
const file = await readHarnessFileContent(deps.fs, repoRoot, definition.path);
|
|
200
|
+
const analyses = await analyzeHarnessFiles(deps.fs, repoRoot);
|
|
201
|
+
const legacyChanges = await analyzeLegacyCodexHarnessPaths(deps.fs, repoRoot);
|
|
202
|
+
return {
|
|
203
|
+
file,
|
|
204
|
+
status: renderHarnessStatus(await readHarnessRevisionValue(deps.fs, repoRoot), analyses, legacyChanges)
|
|
205
|
+
};
|
|
166
206
|
},
|
|
167
207
|
async applyHarness(repoRoot) {
|
|
168
208
|
if (deps.runFixedInstaller) {
|
|
169
|
-
|
|
209
|
+
const result = await deps.runFixedInstaller(repoRoot);
|
|
210
|
+
if (result.changedFiles.length > 0) {
|
|
211
|
+
await bumpHarnessRevision(deps.fs, repoRoot, now());
|
|
212
|
+
}
|
|
213
|
+
return result;
|
|
170
214
|
}
|
|
171
215
|
const analyses = await analyzeHarnessFiles(deps.fs, repoRoot);
|
|
172
216
|
const changedFiles = [];
|
|
@@ -181,6 +225,9 @@ export function createHarnessService(deps) {
|
|
|
181
225
|
}
|
|
182
226
|
const legacyChanges = await removeLegacyCodexHarnessPaths(deps.fs, repoRoot);
|
|
183
227
|
changedFiles.push(...legacyChanges);
|
|
228
|
+
if (changedFiles.length > 0) {
|
|
229
|
+
await bumpHarnessRevision(deps.fs, repoRoot, now());
|
|
230
|
+
}
|
|
184
231
|
return {
|
|
185
232
|
version: VCM_HARNESS_VERSION,
|
|
186
233
|
changedFiles,
|
|
@@ -260,64 +307,71 @@ export function createHarnessService(deps) {
|
|
|
260
307
|
return getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
261
308
|
},
|
|
262
309
|
async startHarnessBootstrap(repoRoot, input = {}) {
|
|
263
|
-
|
|
310
|
+
const session = await launchHarnessBootstrapSession(deps, repoRoot, now, input, { forceFresh: false });
|
|
311
|
+
const nextStatus = await getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
312
|
+
return {
|
|
313
|
+
status: {
|
|
314
|
+
...nextStatus,
|
|
315
|
+
session
|
|
316
|
+
},
|
|
317
|
+
session,
|
|
318
|
+
prompt: buildHarnessBootstrapPrompt(repoRoot)
|
|
319
|
+
};
|
|
320
|
+
},
|
|
321
|
+
async restartHarnessBootstrap(repoRoot, input = {}) {
|
|
322
|
+
const existing = await getCurrentHarnessBootstrapSession(deps, repoRoot, now);
|
|
323
|
+
if (existing && deps.runtime?.getSession(existing.id)) {
|
|
324
|
+
await deps.runtime.stop(existing.id);
|
|
325
|
+
}
|
|
326
|
+
const session = await launchHarnessBootstrapSession(deps, repoRoot, now, input, { forceFresh: true });
|
|
327
|
+
const nextStatus = await getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
328
|
+
return {
|
|
329
|
+
status: {
|
|
330
|
+
...nextStatus,
|
|
331
|
+
session
|
|
332
|
+
},
|
|
333
|
+
session,
|
|
334
|
+
prompt: buildHarnessBootstrapPrompt(repoRoot)
|
|
335
|
+
};
|
|
336
|
+
},
|
|
337
|
+
async stopHarnessBootstrap(repoRoot) {
|
|
338
|
+
const existing = await getCurrentHarnessBootstrapSession(deps, repoRoot, now);
|
|
339
|
+
if (!existing) {
|
|
340
|
+
throw new VcmError({
|
|
341
|
+
code: "HARNESS_BOOTSTRAP_SESSION_MISSING",
|
|
342
|
+
message: "Harness bootstrap terminal has not been started.",
|
|
343
|
+
statusCode: 404
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
if (deps.runtime?.getSession(existing.id)) {
|
|
347
|
+
await deps.runtime.stop(existing.id);
|
|
348
|
+
}
|
|
349
|
+
await persistHarnessBootstrapSession(deps.fs, repoRoot, {
|
|
350
|
+
...existing,
|
|
351
|
+
status: "exited",
|
|
352
|
+
updatedAt: now()
|
|
353
|
+
});
|
|
354
|
+
return getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
355
|
+
},
|
|
356
|
+
async runHarnessBootstrap(repoRoot) {
|
|
357
|
+
if (!deps.runtime) {
|
|
264
358
|
throw new VcmError({
|
|
265
359
|
code: "HARNESS_BOOTSTRAP_UNAVAILABLE",
|
|
266
360
|
message: "Harness bootstrap sessions are not available in this VCM runtime.",
|
|
267
361
|
statusCode: 501
|
|
268
362
|
});
|
|
269
363
|
}
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
status: currentStatus,
|
|
274
|
-
session: currentStatus.session,
|
|
275
|
-
prompt: buildHarnessBootstrapPrompt(repoRoot)
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
if (!currentStatus.canStart) {
|
|
364
|
+
const status = await getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
365
|
+
const session = status.session;
|
|
366
|
+
if (!session || session.status !== "running" || !deps.runtime.getSession(session.id)) {
|
|
279
367
|
throw new VcmError({
|
|
280
|
-
code: "
|
|
281
|
-
message: "
|
|
368
|
+
code: "HARNESS_BOOTSTRAP_SESSION_NOT_RUNNING",
|
|
369
|
+
message: "Start the Harness Bootstrap terminal before running bootstrap.",
|
|
282
370
|
statusCode: 409
|
|
283
371
|
});
|
|
284
372
|
}
|
|
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
373
|
const prompt = buildHarnessBootstrapPrompt(repoRoot);
|
|
320
|
-
await submitTerminalInput(deps.runtime,
|
|
374
|
+
await submitTerminalInput(deps.runtime, session.id, prompt);
|
|
321
375
|
const nextStatus = await getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
322
376
|
return {
|
|
323
377
|
status: {
|
|
@@ -346,6 +400,66 @@ function dedupeHarnessChanges(changedFiles) {
|
|
|
346
400
|
}
|
|
347
401
|
return changes;
|
|
348
402
|
}
|
|
403
|
+
async function launchHarnessBootstrapSession(deps, repoRoot, now, input, options) {
|
|
404
|
+
if (!deps.runtime || !deps.projectService) {
|
|
405
|
+
throw new VcmError({
|
|
406
|
+
code: "HARNESS_BOOTSTRAP_UNAVAILABLE",
|
|
407
|
+
message: "Harness bootstrap sessions are not available in this VCM runtime.",
|
|
408
|
+
statusCode: 501
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
const currentStatus = await getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
412
|
+
if (!options.forceFresh && currentStatus.session?.status === "running") {
|
|
413
|
+
return currentStatus.session;
|
|
414
|
+
}
|
|
415
|
+
if (!currentStatus.canStart && !(options.forceFresh && currentStatus.session?.status === "running")) {
|
|
416
|
+
throw new VcmError({
|
|
417
|
+
code: "HARNESS_BOOTSTRAP_NOT_READY",
|
|
418
|
+
message: "Install the fixed VCM harness before starting harness bootstrap.",
|
|
419
|
+
statusCode: 409
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
const config = await deps.projectService.loadConfig(repoRoot);
|
|
423
|
+
const claudeSessionId = randomUUID();
|
|
424
|
+
const permissionMode = normalizeClaudePermissionMode(input.permissionMode);
|
|
425
|
+
const model = normalizeClaudeModel(input.model);
|
|
426
|
+
const effort = normalizeClaudeEffort(input.effort);
|
|
427
|
+
const command = buildClaudeStartCommand(config.claudeCommand, permissionMode, claudeSessionId, model, effort);
|
|
428
|
+
const logPath = path.join(repoRoot, BOOTSTRAP_LOG_PATH);
|
|
429
|
+
const runtimeSession = await deps.runtime.createSession({
|
|
430
|
+
taskSlug: BOOTSTRAP_TASK_SLUG,
|
|
431
|
+
role: BOOTSTRAP_RUNTIME_ROLE,
|
|
432
|
+
command: command.command,
|
|
433
|
+
args: command.args,
|
|
434
|
+
cwd: repoRoot,
|
|
435
|
+
env: {
|
|
436
|
+
VCM_API_URL: deps.apiUrl,
|
|
437
|
+
VCM_TASK_REPO_ROOT: repoRoot,
|
|
438
|
+
VCM_HARNESS_BOOTSTRAP: "1",
|
|
439
|
+
VCM_SESSION_ID: claudeSessionId
|
|
440
|
+
},
|
|
441
|
+
cols: input.cols,
|
|
442
|
+
rows: input.rows,
|
|
443
|
+
logPath
|
|
444
|
+
});
|
|
445
|
+
const session = {
|
|
446
|
+
id: runtimeSession.id,
|
|
447
|
+
claudeSessionId,
|
|
448
|
+
status: runtimeSession.status === "crashed" ? "crashed" : runtimeSession.status === "exited" ? "exited" : "running",
|
|
449
|
+
command: command.display,
|
|
450
|
+
permissionMode,
|
|
451
|
+
model,
|
|
452
|
+
effort,
|
|
453
|
+
cwd: repoRoot,
|
|
454
|
+
logPath: BOOTSTRAP_LOG_PATH,
|
|
455
|
+
startedAt: runtimeSession.startedAt,
|
|
456
|
+
updatedAt: now(),
|
|
457
|
+
lastOutputAt: runtimeSession.lastOutputAt,
|
|
458
|
+
exitCode: runtimeSession.exitCode
|
|
459
|
+
};
|
|
460
|
+
await persistHarnessBootstrapSession(deps.fs, repoRoot, session);
|
|
461
|
+
return session;
|
|
462
|
+
}
|
|
349
463
|
function normalizeHarnessGitPath(value) {
|
|
350
464
|
if (!value || value.includes("\0") || path.posix.isAbsolute(value)) {
|
|
351
465
|
throw new VcmError({
|
|
@@ -369,6 +483,104 @@ function normalizeHarnessGitPath(value) {
|
|
|
369
483
|
function shortCommit(commit) {
|
|
370
484
|
return commit.slice(0, 7);
|
|
371
485
|
}
|
|
486
|
+
async function readHarnessFileContent(fs, repoRoot, filePath) {
|
|
487
|
+
const definition = getHarnessFileDefinition(filePath);
|
|
488
|
+
const absolutePath = resolveHarnessPath(repoRoot, definition.path);
|
|
489
|
+
const exists = await fs.pathExists(absolutePath);
|
|
490
|
+
const content = exists ? await fs.readText(absolutePath) : "";
|
|
491
|
+
const editable = isProjectEditableHarnessFile(definition);
|
|
492
|
+
return {
|
|
493
|
+
path: definition.path,
|
|
494
|
+
kind: definition.kind,
|
|
495
|
+
title: definition.title,
|
|
496
|
+
content,
|
|
497
|
+
editable,
|
|
498
|
+
readonlyReason: editable ? undefined : getReadonlyHarnessReason(definition)
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function getHarnessFileDefinition(filePath) {
|
|
502
|
+
const normalizedPath = normalizeHarnessFilePath(filePath);
|
|
503
|
+
const definition = HARNESS_FILES.find((file) => file.path === normalizedPath) ?? getSpecialHarnessFileDefinition(normalizedPath);
|
|
504
|
+
if (!definition) {
|
|
505
|
+
throw new VcmError({
|
|
506
|
+
code: "HARNESS_FILE_UNKNOWN",
|
|
507
|
+
message: "This path is not a known VCM harness file.",
|
|
508
|
+
statusCode: 404,
|
|
509
|
+
hint: normalizedPath
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
return definition;
|
|
513
|
+
}
|
|
514
|
+
function getSpecialHarnessFileDefinition(filePath) {
|
|
515
|
+
if (filePath !== CLAUDE_SETTINGS_PATH) {
|
|
516
|
+
return undefined;
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
kind: "claude-settings",
|
|
520
|
+
path: CLAUDE_SETTINGS_PATH,
|
|
521
|
+
title: "Claude Code Settings",
|
|
522
|
+
ownership: "raw-file",
|
|
523
|
+
renderRules: () => ""
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function normalizeHarnessFilePath(filePath) {
|
|
527
|
+
if (!filePath || filePath.includes("\0") || path.posix.isAbsolute(filePath)) {
|
|
528
|
+
throw new VcmError({
|
|
529
|
+
code: "HARNESS_FILE_PATH_INVALID",
|
|
530
|
+
message: "Harness file path is invalid.",
|
|
531
|
+
statusCode: 400,
|
|
532
|
+
hint: filePath
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
const normalized = path.posix.normalize(filePath).replace(/^\.\//, "");
|
|
536
|
+
if (normalized === "." || normalized.startsWith("../")) {
|
|
537
|
+
throw new VcmError({
|
|
538
|
+
code: "HARNESS_FILE_PATH_INVALID",
|
|
539
|
+
message: "Harness file path must stay inside the repository.",
|
|
540
|
+
statusCode: 400,
|
|
541
|
+
hint: filePath
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
return normalized;
|
|
545
|
+
}
|
|
546
|
+
function isProjectEditableHarnessFile(definition) {
|
|
547
|
+
return definition.ownership !== "whole-file" && definition.ownership !== "raw-file";
|
|
548
|
+
}
|
|
549
|
+
function getReadonlyHarnessReason(definition) {
|
|
550
|
+
if (definition.path === CLAUDE_SETTINGS_PATH) {
|
|
551
|
+
return "Claude Code hooks are generated by VCM fixed harness install.";
|
|
552
|
+
}
|
|
553
|
+
if (definition.ownership === "whole-file" || definition.ownership === "raw-file") {
|
|
554
|
+
return "This file is VCM-owned whole-file template content. Use fixed harness update instead.";
|
|
555
|
+
}
|
|
556
|
+
return "This harness file is not editable from Harness Studio.";
|
|
557
|
+
}
|
|
558
|
+
function assertManagedBlockUnchanged(definition, currentContent, nextContent) {
|
|
559
|
+
const currentBlock = extractManagedBlock(definition, currentContent);
|
|
560
|
+
const nextBlock = extractManagedBlock(definition, nextContent);
|
|
561
|
+
if (currentBlock && nextBlock !== currentBlock) {
|
|
562
|
+
throw new VcmError({
|
|
563
|
+
code: "HARNESS_MANAGED_BLOCK_PROTECTED",
|
|
564
|
+
message: "VCM fixed managed blocks cannot be edited from Harness Studio.",
|
|
565
|
+
statusCode: 409,
|
|
566
|
+
hint: "Edit project-specific content outside the VCM:BEGIN / VCM:END block, or use fixed harness update."
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
if (!currentBlock && nextBlock) {
|
|
570
|
+
throw new VcmError({
|
|
571
|
+
code: "HARNESS_MANAGED_BLOCK_PROTECTED",
|
|
572
|
+
message: "VCM fixed managed blocks must be installed by VCM.",
|
|
573
|
+
statusCode: 409,
|
|
574
|
+
hint: "Use fixed harness update instead of adding a VCM managed block manually."
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function extractManagedBlock(definition, content) {
|
|
579
|
+
if (definition.ownership === "whole-file" || definition.ownership === "raw-file") {
|
|
580
|
+
return undefined;
|
|
581
|
+
}
|
|
582
|
+
return content.match(getManagedBlockPattern(definition))?.[0];
|
|
583
|
+
}
|
|
372
584
|
async function analyzeHarnessFiles(fs, repoRoot) {
|
|
373
585
|
const analyses = [];
|
|
374
586
|
for (const definition of HARNESS_FILES) {
|
|
@@ -508,7 +720,7 @@ async function removeLegacyCodexHarnessPaths(fs, repoRoot) {
|
|
|
508
720
|
}
|
|
509
721
|
return changes;
|
|
510
722
|
}
|
|
511
|
-
function renderHarnessStatus(analyses, legacyChanges = []) {
|
|
723
|
+
function renderHarnessStatus(harnessRevision, analyses, legacyChanges = []) {
|
|
512
724
|
const files = analyses.map((analysis) => analysis.status);
|
|
513
725
|
const plannedChanges = analyses
|
|
514
726
|
.map((analysis) => analysis.plannedChange)
|
|
@@ -528,6 +740,7 @@ function renderHarnessStatus(analyses, legacyChanges = []) {
|
|
|
528
740
|
analysis.status.exists));
|
|
529
741
|
return {
|
|
530
742
|
version: VCM_HARNESS_VERSION,
|
|
743
|
+
harnessRevision,
|
|
531
744
|
initialized,
|
|
532
745
|
files,
|
|
533
746
|
needsApply: plannedChanges.length > 0,
|
|
@@ -537,6 +750,9 @@ function renderHarnessStatus(analyses, legacyChanges = []) {
|
|
|
537
750
|
: []
|
|
538
751
|
};
|
|
539
752
|
}
|
|
753
|
+
async function readHarnessRevisionValue(fs, repoRoot) {
|
|
754
|
+
return (await readHarnessRevisionState(fs, repoRoot)).revision;
|
|
755
|
+
}
|
|
540
756
|
function renderManagedBlock(definition, rules) {
|
|
541
757
|
const endSpacing = definition.blankLineBeforeEnd ? "\n\n" : "\n";
|
|
542
758
|
if (definition.commentStyle === "hash") {
|
|
@@ -924,6 +1140,9 @@ async function getCurrentHarnessBootstrapSession(deps, repoRoot, now) {
|
|
|
924
1140
|
claudeSessionId: persisted?.claudeSessionId ?? runtimeSession.id,
|
|
925
1141
|
status: runtimeSession.status === "crashed" ? "crashed" : runtimeSession.status === "exited" ? "exited" : "running",
|
|
926
1142
|
command: persisted?.command ?? "claude",
|
|
1143
|
+
permissionMode: persisted?.permissionMode,
|
|
1144
|
+
model: persisted?.model,
|
|
1145
|
+
effort: persisted?.effort,
|
|
927
1146
|
cwd: persisted?.cwd ?? repoRoot,
|
|
928
1147
|
logPath: persisted?.logPath ?? BOOTSTRAP_LOG_PATH,
|
|
929
1148
|
startedAt: runtimeSession.startedAt,
|
|
@@ -965,6 +1184,9 @@ async function loadPersistedHarnessBootstrapSession(fs, repoRoot) {
|
|
|
965
1184
|
claudeSessionId: payload.claudeSessionId,
|
|
966
1185
|
status,
|
|
967
1186
|
command: payload.command,
|
|
1187
|
+
permissionMode: normalizeClaudePermissionMode(payload.permissionMode),
|
|
1188
|
+
model: normalizeClaudeModel(payload.model),
|
|
1189
|
+
effort: normalizeClaudeEffort(payload.effort),
|
|
968
1190
|
cwd: payload.cwd,
|
|
969
1191
|
logPath: payload.logPath,
|
|
970
1192
|
startedAt: typeof payload.startedAt === "string" ? payload.startedAt : undefined,
|
|
@@ -1012,17 +1234,56 @@ function bootstrapWarnings(status, checks, session) {
|
|
|
1012
1234
|
}
|
|
1013
1235
|
return warnings;
|
|
1014
1236
|
}
|
|
1015
|
-
function buildClaudeStartCommand(command = "claude", permissionMode = "default", claudeSessionId) {
|
|
1016
|
-
const args = ["--session-id", claudeSessionId];
|
|
1237
|
+
function buildClaudeStartCommand(command = "claude", permissionMode = "default", claudeSessionId, model = "default", effort = "default") {
|
|
1238
|
+
const args = ["--session-id", claudeSessionId, "--model", model];
|
|
1239
|
+
if (effort === "ultracode") {
|
|
1240
|
+
args.push("--settings", JSON.stringify({ ultracode: true }));
|
|
1241
|
+
}
|
|
1242
|
+
else if (effort !== "default") {
|
|
1243
|
+
args.push("--effort", effort);
|
|
1244
|
+
}
|
|
1017
1245
|
if (permissionMode === "bypassPermissions") {
|
|
1018
1246
|
args.push("--permission-mode", "bypassPermissions");
|
|
1019
1247
|
}
|
|
1020
1248
|
return {
|
|
1021
1249
|
command,
|
|
1022
1250
|
args,
|
|
1023
|
-
display:
|
|
1251
|
+
display: [command, ...args].map(formatDisplayArg).join(" ")
|
|
1024
1252
|
};
|
|
1025
1253
|
}
|
|
1254
|
+
function formatDisplayArg(value) {
|
|
1255
|
+
if (/^[A-Za-z0-9_./:=@+-]+$/.test(value)) {
|
|
1256
|
+
return value;
|
|
1257
|
+
}
|
|
1258
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
1259
|
+
}
|
|
1260
|
+
function normalizeClaudePermissionMode(value) {
|
|
1261
|
+
return value === "bypassPermissions" || value === "dangerously-skip-permissions"
|
|
1262
|
+
? "bypassPermissions"
|
|
1263
|
+
: "default";
|
|
1264
|
+
}
|
|
1265
|
+
function normalizeClaudeModel(value) {
|
|
1266
|
+
if (value === "best"
|
|
1267
|
+
|| value === "fable"
|
|
1268
|
+
|| value === "opus"
|
|
1269
|
+
|| value === "opus[1m]"
|
|
1270
|
+
|| value === "claude-opus-4-8"
|
|
1271
|
+
|| value === "claude-opus-4-8[1m]") {
|
|
1272
|
+
return value;
|
|
1273
|
+
}
|
|
1274
|
+
return "default";
|
|
1275
|
+
}
|
|
1276
|
+
function normalizeClaudeEffort(value) {
|
|
1277
|
+
if (value === "low"
|
|
1278
|
+
|| value === "medium"
|
|
1279
|
+
|| value === "high"
|
|
1280
|
+
|| value === "xhigh"
|
|
1281
|
+
|| value === "max"
|
|
1282
|
+
|| value === "ultracode") {
|
|
1283
|
+
return value;
|
|
1284
|
+
}
|
|
1285
|
+
return "default";
|
|
1286
|
+
}
|
|
1026
1287
|
function buildHarnessBootstrapPrompt(repoRoot) {
|
|
1027
1288
|
return `Use the vcm-harness-bootstrap skill to finish the VCM harness bootstrap for this repository.
|
|
1028
1289
|
|