vibe-coding-master 0.4.0 → 0.4.2
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 +3 -3
- package/dist/backend/cli/install-vcm-harness.js +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/backend/services/message-service.js +0 -32
- package/dist/backend/templates/harness/architect-agent.js +1 -1
- package/dist/backend/templates/harness/claude-root.js +1 -1
- package/dist/backend/templates/harness/coder-agent.js +1 -1
- package/dist/backend/templates/harness/vcm-harness-bootstrap-skill.js +1 -1
- 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 +5 -5
- package/docs/product-design.md +1 -1
- package/docs/vcm-cc-best-practices.md +12 -18
- package/package.json +1 -1
- package/scripts/harness-tools/__pycache__/generate-module-indexcpython-314.pyc +0 -0
- package/scripts/harness-tools/__pycache__/generate-public-surfacecpython-314.pyc +0 -0
- package/scripts/harness-tools/generate-module-index +225 -5
- package/scripts/harness-tools/generate-public-surface +388 -3
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ When Gate Review Gates are enabled for a task, or when a Gate Reviewer session a
|
|
|
30
30
|
- Manual and automatic orchestration modes.
|
|
31
31
|
- Two-stage VCM harness setup: deterministic fixed install plus AI-assisted bootstrap.
|
|
32
32
|
- VCM-managed root rules, role agents, repo-local VCM skills, Claude Code hooks, generated-context tools, and PR template.
|
|
33
|
-
-
|
|
33
|
+
- Generated context for Rust and npm workspace module indexing plus public surface indexing.
|
|
34
34
|
- Translation panel powered by the long-lived Translator session.
|
|
35
35
|
- Mobile Gateway through Tencent iLink Bot API / Weixin DM, for talking to PM and managing tasks from Weixin.
|
|
36
36
|
- Durable task state, role session state, handoff artifacts, and message history.
|
|
@@ -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
|
|
@@ -497,7 +497,7 @@ docs/TESTING.md
|
|
|
497
497
|
.ai/generated/public-surface.json
|
|
498
498
|
```
|
|
499
499
|
|
|
500
|
-
The generated-context tools
|
|
500
|
+
The generated-context tools support Rust/Cargo projects and npm workspace TypeScript/JavaScript projects. Other repository shapes can still install the fixed harness, but generated context should be treated as unsupported until project-specific generators exist.
|
|
501
501
|
|
|
502
502
|
After applying harness changes or completing bootstrap, VCM reports the exact files changed or checks completed and reminds the user to review and commit them before starting long-running work.
|
|
503
503
|
|
|
@@ -314,7 +314,7 @@ Installs only fixed VCM harness content.
|
|
|
314
314
|
This deterministic installer handles VCM-owned managed blocks, VCM-owned whole
|
|
315
315
|
files, VCM Claude settings hooks, generic long-running helper tools, and the
|
|
316
316
|
harness manifest. It also creates blank durable project doc templates when
|
|
317
|
-
missing and installs
|
|
317
|
+
missing and installs generated-context tools for Rust and npm workspace projects. It does not copy
|
|
318
318
|
example project docs, generated context artifacts, module-level architecture
|
|
319
319
|
docs, or task runtime handoff artifacts.`);
|
|
320
320
|
}
|
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
|
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { CORE_VCM_ROLE_NAMES } from "../../shared/constants.js";
|
|
4
|
-
import { VcmError } from "../errors.js";
|
|
5
4
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
6
5
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
7
6
|
import { renderMessageEnvelope } from "../templates/message-envelope.js";
|
|
8
7
|
const PM_ROLE = "project-manager";
|
|
9
|
-
const PM_TO_ROLE_TYPES = new Set(["task", "question", "review-request", "revise", "cancel"]);
|
|
10
|
-
const ROLE_TO_PM_TYPES = new Set(["result", "question", "blocked", "finding"]);
|
|
11
8
|
const DEFAULT_PRE_DISPATCH_SWITCH_DELAY_MS = 500;
|
|
12
9
|
const DEFAULT_AUTO_DISPATCH_ENTER_DELAY_MS = 500;
|
|
13
10
|
const DEFAULT_DISPATCH_CONFIRMATION_RETRY_DELAYS_MS = [1500, 3000];
|
|
@@ -64,7 +61,6 @@ export function createMessageService(deps) {
|
|
|
64
61
|
return all.filter((routeFile) => routeFile.pending);
|
|
65
62
|
}
|
|
66
63
|
async function dispatchRouteFile(input, routeFile, state, timestamp) {
|
|
67
|
-
validateMessagePolicy(routeFile.fromRole, routeFile.toRole, routeFile.type);
|
|
68
64
|
const session = await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, routeFile.toRole);
|
|
69
65
|
if (!session || session.status !== "running") {
|
|
70
66
|
return {
|
|
@@ -370,34 +366,6 @@ function selectDispatchCandidates(routeFiles, stoppedRole) {
|
|
|
370
366
|
return updated !== 0 ? updated : left.path.localeCompare(right.path);
|
|
371
367
|
});
|
|
372
368
|
}
|
|
373
|
-
function validateMessagePolicy(fromRole, toRole, type) {
|
|
374
|
-
if (!CORE_VCM_ROLE_NAMES.some((role) => role === toRole)) {
|
|
375
|
-
throw new VcmError({
|
|
376
|
-
code: "MESSAGE_TARGET_UNKNOWN",
|
|
377
|
-
message: `Unknown target role: ${toRole}`,
|
|
378
|
-
statusCode: 400
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
if (!CORE_VCM_ROLE_NAMES.some((role) => role === fromRole)) {
|
|
382
|
-
throw new VcmError({
|
|
383
|
-
code: "MESSAGE_SENDER_UNKNOWN",
|
|
384
|
-
message: `Unknown sender role: ${fromRole}`,
|
|
385
|
-
statusCode: 400
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
if (fromRole === PM_ROLE && toRole !== PM_ROLE && PM_TO_ROLE_TYPES.has(type)) {
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
if (fromRole !== PM_ROLE && toRole === PM_ROLE && ROLE_TO_PM_TYPES.has(type)) {
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
throw new VcmError({
|
|
395
|
-
code: "MESSAGE_POLICY_DENIED",
|
|
396
|
-
message: `${fromRole} cannot send ${type} messages to ${toRole}.`,
|
|
397
|
-
statusCode: 403,
|
|
398
|
-
hint: "Use project-manager as the orchestration hub unless this task explicitly allows a peer route."
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
369
|
async function withTaskLock(locks, key, run) {
|
|
402
370
|
const previous = locks.get(key) ?? Promise.resolve();
|
|
403
371
|
const next = previous.catch(() => undefined).then(run);
|
|
@@ -80,7 +80,7 @@ export function renderArchitectHarnessRules() {
|
|
|
80
80
|
- Update affected \`<module>/ARCHITECTURE.md\` when module-level detailed design changes: boundaries, behavior, important public surface explanations, internal risks, or module-specific architecture notes.
|
|
81
81
|
- Treat \`.ai/generated/public-surface.json\` as the full machine index for public surface. Verify or report its freshness when public APIs changed; do not replace it with prose in architecture docs.
|
|
82
82
|
- When module structure changes, require \`.ai/tools/generate-module-index --check\` or regeneration.
|
|
83
|
-
- When
|
|
83
|
+
- When public APIs, routes, or externally consumed surfaces change, require \`.ai/tools/generate-public-surface --check\` or regeneration.
|
|
84
84
|
- Read \`.ai/vcm/handoffs/known-issues.md\` and promote confirmed unresolved issues to \`docs/known-issues.md\`.
|
|
85
85
|
- Write \`.ai/vcm/handoffs/docs-sync-report.md\` with decision, evidence reviewed, architecture drift check, docs updated, docs left unchanged, remaining documentation risks, and handoff notes.
|
|
86
86
|
|
|
@@ -22,7 +22,7 @@ export function renderRootClaudeHarnessRules() {
|
|
|
22
22
|
- \`docs/TESTING.md\`: validation strategy, commands, validation levels, integration/E2E case definitions, final-validation cleanup, and known testing gaps; reviewer-owned.
|
|
23
23
|
- \`docs/known-issues.md\`: durable known issues and accepted limitations; architect-owned.
|
|
24
24
|
- \`.ai/generated/module-index.json\`: generated module index; use it to find layers, modules, manifests, module docs, source files, test files, and workspace dependencies.
|
|
25
|
-
- \`.ai/generated/public-surface.json\`: generated
|
|
25
|
+
- \`.ai/generated/public-surface.json\`: generated public surface index; use it to inspect module-to-module public APIs, routes, and source evidence.
|
|
26
26
|
|
|
27
27
|
## VCM Task Flow
|
|
28
28
|
|
|
@@ -48,7 +48,7 @@ export function renderCoderHarnessRules() {
|
|
|
48
48
|
### Generated Context
|
|
49
49
|
|
|
50
50
|
- Regenerate \`.ai/generated/module-index.json\` with \`.ai/tools/generate-module-index\` after module, manifest, source-file, or test-file changes.
|
|
51
|
-
- Regenerate \`.ai/generated/public-surface.json\` with \`.ai/tools/generate-public-surface\` after
|
|
51
|
+
- Regenerate \`.ai/generated/public-surface.json\` with \`.ai/tools/generate-public-surface\` after public API, route, externally consumed surface, or public visibility changes.
|
|
52
52
|
- Do not hand-edit generated context files.
|
|
53
53
|
|
|
54
54
|
### Baseline Tests
|
|
@@ -49,7 +49,7 @@ This skill is an operating procedure. It does not replace the deterministic VCM
|
|
|
49
49
|
|
|
50
50
|
- Document the project-level module overview, module responsibilities, module relationships, dependency direction, and project-wide constraints.
|
|
51
51
|
- Link to module-level \`ARCHITECTURE.md\` files when present.
|
|
52
|
-
- Explain generated-context ownership, especially that \`.ai/generated/public-surface.json\` is the machine index for
|
|
52
|
+
- Explain generated-context ownership, especially that \`.ai/generated/public-surface.json\` is the machine index for public APIs, routes, and externally consumed surfaces.
|
|
53
53
|
|
|
54
54
|
### Module-Level \`ARCHITECTURE.md\`
|
|
55
55
|
|