vibe-coding-master 0.3.32 → 0.4.1

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