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 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
- - Rust generated context for module indexing and crate-external public surface indexing.
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 a visible temporary 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:
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 currently target Rust projects. Non-Rust repositories can still install the fixed harness, but generated context should be treated as unsupported until project-specific generators exist.
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 Rust generated-context generator tools. It does not copy
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
  }
@@ -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 launchHarnessBootstrapSession(deps, repoRoot, now, input, { forceFresh: false });
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 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 });
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 getCurrentHarnessBootstrapSession(deps, repoRoot, now);
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 bootstrap terminal has not been started.",
335
+ message: "Harness Engineer session has not been started.",
343
336
  statusCode: 404
344
337
  });
345
338
  }
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
- });
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 Bootstrap terminal before running bootstrap.",
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 launchHarnessBootstrapSession(deps, repoRoot, now, input, options) {
404
- if (!deps.runtime || !deps.projectService) {
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 (!options.forceFresh && currentStatus.session?.status === "running") {
423
+ if (currentStatus.session?.status === "running") {
413
424
  return currentStatus.session;
414
425
  }
415
- if (!currentStatus.canStart && !(options.forceFresh && currentStatus.session?.status === "running")) {
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
- 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
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
- await persistHarnessBootstrapSession(deps.fs, repoRoot, session);
461
- return session;
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 session = await getCurrentHarnessBootstrapSession(deps, repoRoot, now);
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
- : session?.status === "running"
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 && session?.status !== "running",
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 getCurrentHarnessBootstrapSession(deps, repoRoot, now) {
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 (typeof payload.id !== "string" ||
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 !== "exited" && status !== "crashed" && status !== "resumable") {
1161
+ if (status !== "running" && status !== "complete") {
1180
1162
  return undefined;
1181
1163
  }
1182
1164
  return {
1183
- id: payload.id,
1184
- claudeSessionId: payload.claudeSessionId,
1165
+ version: 1,
1185
1166
  status,
1186
- command: payload.command,
1187
- permissionMode: normalizeClaudePermissionMode(payload.permissionMode),
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
- lastOutputAt: typeof payload.lastOutputAt === "string" ? payload.lastOutputAt : undefined,
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 persistHarnessBootstrapSession(fs, repoRoot, session) {
1199
- await fs.writeJsonAtomic(resolveHarnessPath(repoRoot, BOOTSTRAP_SESSION_PATH), session);
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 previous bootstrap terminal is no longer active; start bootstrap again or finish the remaining files manually.");
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 crate-external public APIs change, require \`.ai/tools/generate-public-surface --check\` or regeneration.
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 crate-external public API index; use it to inspect module-to-module public interfaces and source evidence.
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 crate-external public API or public visibility changes.
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 crate-external public APIs.
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