vibe-coding-master 0.4.0 → 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/server.js +8 -8
- package/dist/backend/services/claude-hook-service.js +5 -0
- package/dist/backend/services/harness-service.js +119 -174
- package/dist-frontend/assets/{index-CMuJhDdX.js → index-uYhgIwNK.js} +22 -22
- package/dist-frontend/index.html +1 -1
- package/docs/full-harness-baseline.md +1 -1
- package/docs/product-design.md +1 -1
- package/docs/vcm-cc-best-practices.md +7 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -486,7 +486,7 @@ For `.gitignore`, VCM uses a gitignore-native managed block:
|
|
|
486
486
|
|
|
487
487
|
VCM also JSON-merges `.claude/settings.json` to install Claude Code `PreToolUse`, `UserPromptSubmit`, `Stop`, and `PermissionRequest` hooks plus a managed `env.BASH_DEFAULT_TIMEOUT_MS` so foreground watch windows fit inside the Bash tool timeout. The hooks post directly to the local VCM backend, so roles do not need a VCM CLI command to confirm delivery or report turn completion. The `Stop` hook forwards the backend response to Claude Code, which lets VCM block turn-end while a validation job is still running.
|
|
488
488
|
|
|
489
|
-
Bootstrap is AI-assisted. VCM starts
|
|
489
|
+
Bootstrap is AI-assisted. VCM starts or resumes the project-scoped Harness Engineer Claude Code session in the connected repository and asks it to use the `vcm-harness-bootstrap` skill. Bootstrap fills project-specific content and generated context:
|
|
490
490
|
|
|
491
491
|
```text
|
|
492
492
|
CLAUDE.md project context outside the VCM managed block
|
package/dist/backend/server.js
CHANGED
|
@@ -167,14 +167,6 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
167
167
|
const registry = createSessionRegistry();
|
|
168
168
|
const artifactService = createArtifactService(fs);
|
|
169
169
|
const projectService = createProjectService({ fs, git, appSettings });
|
|
170
|
-
const harnessService = createHarnessService({
|
|
171
|
-
fs,
|
|
172
|
-
git,
|
|
173
|
-
runtime,
|
|
174
|
-
projectService,
|
|
175
|
-
apiUrl: options.apiUrl,
|
|
176
|
-
runFixedInstaller: createScriptFixedHarnessInstaller(path.join(getAppRoot(), "scripts/install-vcm-harness.mjs"))
|
|
177
|
-
});
|
|
178
170
|
const taskService = createTaskService({ fs, git, artifactService, projectService });
|
|
179
171
|
const sessionService = createSessionService({
|
|
180
172
|
fs,
|
|
@@ -186,6 +178,13 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
186
178
|
taskService,
|
|
187
179
|
apiUrl: options.apiUrl
|
|
188
180
|
});
|
|
181
|
+
const harnessService = createHarnessService({
|
|
182
|
+
fs,
|
|
183
|
+
git,
|
|
184
|
+
runtime,
|
|
185
|
+
harnessEngineerSessions: sessionService,
|
|
186
|
+
runFixedInstaller: createScriptFixedHarnessInstaller(path.join(getAppRoot(), "scripts/install-vcm-harness.mjs"))
|
|
187
|
+
});
|
|
189
188
|
const commandDispatcher = createCommandDispatcher({
|
|
190
189
|
runtime,
|
|
191
190
|
sessionService,
|
|
@@ -264,6 +263,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
264
263
|
translationService,
|
|
265
264
|
appSettings,
|
|
266
265
|
runtime,
|
|
266
|
+
harnessService,
|
|
267
267
|
gatewayService,
|
|
268
268
|
jobGuard: createJobGuardService(),
|
|
269
269
|
translationWorkerService
|
|
@@ -83,6 +83,11 @@ export function createClaudeHookService(deps) {
|
|
|
83
83
|
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
84
84
|
cwd: stringOrUndefined(input.event.cwd)
|
|
85
85
|
});
|
|
86
|
+
await deps.harnessService?.recordHarnessBootstrapHook(context.project.repoRoot, {
|
|
87
|
+
eventName,
|
|
88
|
+
sessionId: session?.id,
|
|
89
|
+
claudeSessionId: stringOrUndefined(input.event.session_id)
|
|
90
|
+
});
|
|
86
91
|
return {
|
|
87
92
|
ok: true,
|
|
88
93
|
eventName,
|
|
@@ -1,4 +1,3 @@
|
|
|
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";
|
|
@@ -19,10 +18,8 @@ import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
|
19
18
|
import { VcmError } from "../errors.js";
|
|
20
19
|
import { bumpHarnessRevision, readHarnessRevisionState } from "./harness-revision.js";
|
|
21
20
|
const execFileAsync = promisify(execFile);
|
|
22
|
-
const BOOTSTRAP_TASK_SLUG = "__vcm-harness-bootstrap__";
|
|
23
|
-
const BOOTSTRAP_RUNTIME_ROLE = "project-manager";
|
|
24
|
-
const BOOTSTRAP_LOG_PATH = ".ai/vcm/bootstrap/bootstrap.log";
|
|
25
21
|
const BOOTSTRAP_SESSION_PATH = ".ai/vcm/bootstrap/session.json";
|
|
22
|
+
const HARNESS_ENGINEER_SESSION_PATH = ".ai/vcm/harness-engineer/session.json";
|
|
26
23
|
export const VCM_HARNESS_VERSION = 1;
|
|
27
24
|
const MANAGED_BLOCK_PATTERN = /<!-- VCM:BEGIN(?:\s+version=(\d+))? -->[\s\S]*?<!-- VCM:END -->/m;
|
|
28
25
|
const HASH_MANAGED_BLOCK_PATTERN = /# VCM:BEGIN(?:\s+version=(\d+))?\n[\s\S]*?# VCM:END/m;
|
|
@@ -307,7 +304,7 @@ export function createHarnessService(deps) {
|
|
|
307
304
|
return getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
308
305
|
},
|
|
309
306
|
async startHarnessBootstrap(repoRoot, input = {}) {
|
|
310
|
-
const session = await
|
|
307
|
+
const session = await ensureHarnessEngineerForBootstrap(deps, repoRoot, now, input);
|
|
311
308
|
const nextStatus = await getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
312
309
|
return {
|
|
313
310
|
status: {
|
|
@@ -319,11 +316,7 @@ export function createHarnessService(deps) {
|
|
|
319
316
|
};
|
|
320
317
|
},
|
|
321
318
|
async restartHarnessBootstrap(repoRoot, input = {}) {
|
|
322
|
-
const
|
|
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 });
|
|
319
|
+
const session = await restartHarnessEngineerForBootstrap(deps, repoRoot, now, input);
|
|
327
320
|
const nextStatus = await getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
328
321
|
return {
|
|
329
322
|
status: {
|
|
@@ -335,26 +328,20 @@ export function createHarnessService(deps) {
|
|
|
335
328
|
};
|
|
336
329
|
},
|
|
337
330
|
async stopHarnessBootstrap(repoRoot) {
|
|
338
|
-
const existing = await
|
|
331
|
+
const existing = await getCurrentHarnessEngineerBootstrapSession(deps, repoRoot);
|
|
339
332
|
if (!existing) {
|
|
340
333
|
throw new VcmError({
|
|
341
334
|
code: "HARNESS_BOOTSTRAP_SESSION_MISSING",
|
|
342
|
-
message: "Harness
|
|
335
|
+
message: "Harness Engineer session has not been started.",
|
|
343
336
|
statusCode: 404
|
|
344
337
|
});
|
|
345
338
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
await persistHarnessBootstrapSession(deps.fs, repoRoot, {
|
|
350
|
-
...existing,
|
|
351
|
-
status: "exited",
|
|
352
|
-
updatedAt: now()
|
|
353
|
-
});
|
|
339
|
+
await deps.harnessEngineerSessions?.stopProjectHarnessEngineerSession(repoRoot);
|
|
340
|
+
await clearHarnessBootstrapRunState(deps.fs, repoRoot);
|
|
354
341
|
return getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
355
342
|
},
|
|
356
343
|
async runHarnessBootstrap(repoRoot) {
|
|
357
|
-
if (!deps.runtime) {
|
|
344
|
+
if (!deps.runtime || !deps.harnessEngineerSessions) {
|
|
358
345
|
throw new VcmError({
|
|
359
346
|
code: "HARNESS_BOOTSTRAP_UNAVAILABLE",
|
|
360
347
|
message: "Harness bootstrap sessions are not available in this VCM runtime.",
|
|
@@ -366,11 +353,20 @@ export function createHarnessService(deps) {
|
|
|
366
353
|
if (!session || session.status !== "running" || !deps.runtime.getSession(session.id)) {
|
|
367
354
|
throw new VcmError({
|
|
368
355
|
code: "HARNESS_BOOTSTRAP_SESSION_NOT_RUNNING",
|
|
369
|
-
message: "Start the Harness
|
|
356
|
+
message: "Start the Harness Engineer session before running bootstrap.",
|
|
370
357
|
statusCode: 409
|
|
371
358
|
});
|
|
372
359
|
}
|
|
373
360
|
const prompt = buildHarnessBootstrapPrompt(repoRoot);
|
|
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
|
+
});
|
|
374
370
|
await submitTerminalInput(deps.runtime, session.id, prompt);
|
|
375
371
|
const nextStatus = await getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
376
372
|
return {
|
|
@@ -381,6 +377,21 @@ export function createHarnessService(deps) {
|
|
|
381
377
|
session,
|
|
382
378
|
prompt
|
|
383
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);
|
|
384
395
|
}
|
|
385
396
|
};
|
|
386
397
|
}
|
|
@@ -400,8 +411,8 @@ function dedupeHarnessChanges(changedFiles) {
|
|
|
400
411
|
}
|
|
401
412
|
return changes;
|
|
402
413
|
}
|
|
403
|
-
async function
|
|
404
|
-
if (!deps.
|
|
414
|
+
async function ensureHarnessEngineerForBootstrap(deps, repoRoot, now, input) {
|
|
415
|
+
if (!deps.harnessEngineerSessions) {
|
|
405
416
|
throw new VcmError({
|
|
406
417
|
code: "HARNESS_BOOTSTRAP_UNAVAILABLE",
|
|
407
418
|
message: "Harness bootstrap sessions are not available in this VCM runtime.",
|
|
@@ -409,56 +420,62 @@ async function launchHarnessBootstrapSession(deps, repoRoot, now, input, options
|
|
|
409
420
|
});
|
|
410
421
|
}
|
|
411
422
|
const currentStatus = await getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
412
|
-
if (
|
|
423
|
+
if (currentStatus.session?.status === "running") {
|
|
413
424
|
return currentStatus.session;
|
|
414
425
|
}
|
|
415
|
-
if (!currentStatus.canStart
|
|
426
|
+
if (!currentStatus.canStart) {
|
|
416
427
|
throw new VcmError({
|
|
417
428
|
code: "HARNESS_BOOTSTRAP_NOT_READY",
|
|
418
429
|
message: "Install the fixed VCM harness before starting harness bootstrap.",
|
|
419
430
|
statusCode: 409
|
|
420
431
|
});
|
|
421
432
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
|
459
472
|
};
|
|
460
|
-
|
|
461
|
-
|
|
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";
|
|
462
479
|
}
|
|
463
480
|
function normalizeHarnessGitPath(value) {
|
|
464
481
|
if (!value || value.includes("\0") || path.posix.isAbsolute(value)) {
|
|
@@ -909,26 +926,28 @@ async function getHarnessBootstrapStatus(deps, repoRoot, now) {
|
|
|
909
926
|
await checkModuleArchitectureDocs(deps.fs, repoRoot, moduleIndex),
|
|
910
927
|
await checkFilledMarkdown(deps.fs, repoRoot, "docs/TESTING.md", "Testing doc", "testing-doc")
|
|
911
928
|
];
|
|
912
|
-
const
|
|
929
|
+
const runState = await loadPersistedHarnessBootstrapRunState(deps.fs, repoRoot);
|
|
930
|
+
const session = await getCurrentHarnessEngineerBootstrapSession(deps, repoRoot);
|
|
913
931
|
const fixedHarnessReady = checks[0]?.status === "ok";
|
|
914
932
|
const projectChecks = checks.slice(1);
|
|
915
933
|
const projectComplete = projectChecks.every((check) => check.status === "ok");
|
|
916
934
|
const projectStarted = projectChecks.some((check) => check.status === "ok" || check.status === "incomplete");
|
|
935
|
+
const runActive = runState?.status === "running" && session?.status === "running";
|
|
917
936
|
const status = !fixedHarnessReady
|
|
918
937
|
? "not_ready"
|
|
919
|
-
:
|
|
938
|
+
: runActive
|
|
920
939
|
? "running"
|
|
921
|
-
: projectComplete
|
|
940
|
+
: runState?.status === "complete" || projectComplete
|
|
922
941
|
? "complete"
|
|
923
942
|
: projectStarted
|
|
924
943
|
? "incomplete"
|
|
925
944
|
: "not_started";
|
|
926
945
|
return {
|
|
927
946
|
status,
|
|
928
|
-
canStart: fixedHarnessReady &&
|
|
947
|
+
canStart: fixedHarnessReady && !runActive,
|
|
929
948
|
checks,
|
|
930
949
|
session,
|
|
931
|
-
warnings: bootstrapWarnings(status, checks, session)
|
|
950
|
+
warnings: bootstrapWarnings(status, checks, session, runState)
|
|
932
951
|
};
|
|
933
952
|
}
|
|
934
953
|
async function checkFixedHarness(fs, repoRoot) {
|
|
@@ -1129,74 +1148,47 @@ function isFilledMarkdown(content) {
|
|
|
1129
1148
|
.filter((line) => line && !line.startsWith("#"));
|
|
1130
1149
|
return meaningfulLines.join("\n").length >= 120;
|
|
1131
1150
|
}
|
|
1132
|
-
async function
|
|
1133
|
-
const persisted = await loadPersistedHarnessBootstrapSession(deps.fs, repoRoot);
|
|
1134
|
-
const runtimeSession = deps.runtime
|
|
1135
|
-
?.listSessions(BOOTSTRAP_TASK_SLUG)
|
|
1136
|
-
.find((session) => session.id === persisted?.id || session.status === "running");
|
|
1137
|
-
if (runtimeSession) {
|
|
1138
|
-
const session = {
|
|
1139
|
-
id: runtimeSession.id,
|
|
1140
|
-
claudeSessionId: persisted?.claudeSessionId ?? runtimeSession.id,
|
|
1141
|
-
status: runtimeSession.status === "crashed" ? "crashed" : runtimeSession.status === "exited" ? "exited" : "running",
|
|
1142
|
-
command: persisted?.command ?? "claude",
|
|
1143
|
-
permissionMode: persisted?.permissionMode,
|
|
1144
|
-
model: persisted?.model,
|
|
1145
|
-
effort: persisted?.effort,
|
|
1146
|
-
cwd: persisted?.cwd ?? repoRoot,
|
|
1147
|
-
logPath: persisted?.logPath ?? BOOTSTRAP_LOG_PATH,
|
|
1148
|
-
startedAt: runtimeSession.startedAt,
|
|
1149
|
-
updatedAt: now(),
|
|
1150
|
-
lastOutputAt: runtimeSession.lastOutputAt,
|
|
1151
|
-
exitCode: runtimeSession.exitCode
|
|
1152
|
-
};
|
|
1153
|
-
await persistHarnessBootstrapSession(deps.fs, repoRoot, session);
|
|
1154
|
-
return session;
|
|
1155
|
-
}
|
|
1156
|
-
if (persisted?.status === "running") {
|
|
1157
|
-
return {
|
|
1158
|
-
...persisted,
|
|
1159
|
-
status: "resumable",
|
|
1160
|
-
updatedAt: now()
|
|
1161
|
-
};
|
|
1162
|
-
}
|
|
1163
|
-
return undefined;
|
|
1164
|
-
}
|
|
1165
|
-
async function loadPersistedHarnessBootstrapSession(fs, repoRoot) {
|
|
1151
|
+
async function loadPersistedHarnessBootstrapRunState(fs, repoRoot) {
|
|
1166
1152
|
const payload = await readOptionalJsonObject(fs, repoRoot, BOOTSTRAP_SESSION_PATH);
|
|
1167
1153
|
if (!payload) {
|
|
1168
1154
|
return undefined;
|
|
1169
1155
|
}
|
|
1170
|
-
if (
|
|
1171
|
-
typeof payload.claudeSessionId !== "string" ||
|
|
1172
|
-
typeof payload.command !== "string" ||
|
|
1173
|
-
typeof payload.cwd !== "string" ||
|
|
1174
|
-
typeof payload.logPath !== "string" ||
|
|
1156
|
+
if (payload.version !== 1 ||
|
|
1175
1157
|
typeof payload.updatedAt !== "string") {
|
|
1176
1158
|
return undefined;
|
|
1177
1159
|
}
|
|
1178
1160
|
const status = payload.status;
|
|
1179
|
-
if (status !== "running" && status !== "
|
|
1161
|
+
if (status !== "running" && status !== "complete") {
|
|
1180
1162
|
return undefined;
|
|
1181
1163
|
}
|
|
1182
1164
|
return {
|
|
1183
|
-
|
|
1184
|
-
claudeSessionId: payload.claudeSessionId,
|
|
1165
|
+
version: 1,
|
|
1185
1166
|
status,
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
model: normalizeClaudeModel(payload.model),
|
|
1189
|
-
effort: normalizeClaudeEffort(payload.effort),
|
|
1190
|
-
cwd: payload.cwd,
|
|
1191
|
-
logPath: payload.logPath,
|
|
1167
|
+
sessionId: typeof payload.sessionId === "string" ? payload.sessionId : undefined,
|
|
1168
|
+
claudeSessionId: typeof payload.claudeSessionId === "string" ? payload.claudeSessionId : undefined,
|
|
1192
1169
|
startedAt: typeof payload.startedAt === "string" ? payload.startedAt : undefined,
|
|
1170
|
+
completedAt: typeof payload.completedAt === "string" ? payload.completedAt : undefined,
|
|
1193
1171
|
updatedAt: payload.updatedAt,
|
|
1194
|
-
|
|
1195
|
-
exitCode: typeof payload.exitCode === "number" || payload.exitCode === null ? payload.exitCode : undefined
|
|
1172
|
+
lastHookEvent: isHarnessBootstrapHookEvent(payload.lastHookEvent) ? payload.lastHookEvent : undefined
|
|
1196
1173
|
};
|
|
1197
1174
|
}
|
|
1198
|
-
async function
|
|
1199
|
-
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;
|
|
1200
1192
|
}
|
|
1201
1193
|
async function readOptionalText(fs, repoRoot, relativePath) {
|
|
1202
1194
|
const absolutePath = resolveHarnessPath(repoRoot, relativePath);
|
|
@@ -1220,13 +1212,16 @@ async function readOptionalJsonObject(fs, repoRoot, relativePath) {
|
|
|
1220
1212
|
return undefined;
|
|
1221
1213
|
}
|
|
1222
1214
|
}
|
|
1223
|
-
function bootstrapWarnings(status, checks, session) {
|
|
1215
|
+
function bootstrapWarnings(status, checks, session, runState) {
|
|
1224
1216
|
const warnings = [];
|
|
1225
1217
|
if (status === "not_ready") {
|
|
1226
1218
|
warnings.push("Install or update the fixed VCM harness before running harness bootstrap.");
|
|
1227
1219
|
}
|
|
1228
1220
|
if (session?.status === "resumable") {
|
|
1229
|
-
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.");
|
|
1230
1225
|
}
|
|
1231
1226
|
const unknownChecks = checks.filter((check) => check.status === "unknown");
|
|
1232
1227
|
if (unknownChecks.length > 0) {
|
|
@@ -1234,56 +1229,6 @@ function bootstrapWarnings(status, checks, session) {
|
|
|
1234
1229
|
}
|
|
1235
1230
|
return warnings;
|
|
1236
1231
|
}
|
|
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
|
-
}
|
|
1245
|
-
if (permissionMode === "bypassPermissions") {
|
|
1246
|
-
args.push("--permission-mode", "bypassPermissions");
|
|
1247
|
-
}
|
|
1248
|
-
return {
|
|
1249
|
-
command,
|
|
1250
|
-
args,
|
|
1251
|
-
display: [command, ...args].map(formatDisplayArg).join(" ")
|
|
1252
|
-
};
|
|
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
|
-
}
|
|
1287
1232
|
function buildHarnessBootstrapPrompt(repoRoot) {
|
|
1288
1233
|
return `Use the vcm-harness-bootstrap skill to finish the VCM harness bootstrap for this repository.
|
|
1289
1234
|
|