vibe-coding-master 0.3.31 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,21 +3,40 @@ import path from "node:path";
3
3
  import { CORE_VCM_ROLE_NAMES, isDispatchableRole } from "../../shared/constants.js";
4
4
  import { VcmError } from "../errors.js";
5
5
  import { resolveRepoPath } from "../adapters/filesystem.js";
6
+ import { submitTerminalInput } from "../runtime/terminal-submit.js";
6
7
  import { claudeTranscriptPath } from "./claude-transcript-service.js";
8
+ import { readHarnessRevisionState } from "./harness-revision.js";
7
9
  import { getTaskRuntimeRepoRoot } from "./task-service.js";
8
10
  const GATE_REVIEWER_ROLE = "gate-reviewer";
9
11
  const TRANSLATOR_ROLE = "translator";
12
+ const HARNESS_ENGINEER_ROLE = "harness-engineer";
10
13
  const TRANSLATION_DIR = ".ai/vcm/translations";
11
14
  const TRANSLATOR_SESSION_PATH = ".ai/vcm/translations/session.json";
15
+ const HARNESS_ENGINEER_DIR = ".ai/vcm/harness-engineer";
16
+ const HARNESS_ENGINEER_SESSION_PATH = ".ai/vcm/harness-engineer/session.json";
12
17
  const GATE_REVIEWER_SESSION_PATH = ".ai/vcm/gate-reviewer/session.json";
13
18
  const PROJECT_TRANSLATOR_SCOPE = "__project__";
19
+ const PROJECT_HARNESS_ENGINEER_SCOPE = "__project_harness_engineer__";
14
20
  const PROJECT_GATE_REVIEWER_SCOPE = "__project_gate_reviewer__";
15
21
  export function createSessionService(deps) {
16
22
  const now = deps.now ?? (() => new Date().toISOString());
23
+ async function readCurrentHarnessRevision(repoRoot) {
24
+ return (await readHarnessRevisionState(deps.fs, repoRoot)).revision;
25
+ }
26
+ async function withHarnessRevisionView(repoRoot, record) {
27
+ const currentRevision = await readCurrentHarnessRevision(repoRoot);
28
+ const sessionRevision = normalizeHarnessRevision(record.harnessRevision);
29
+ return {
30
+ ...record,
31
+ harnessRevision: sessionRevision,
32
+ harnessCurrentRevision: currentRevision,
33
+ harnessOutdated: sessionRevision < currentRevision
34
+ };
35
+ }
17
36
  async function launchRoleSession(repoRoot, taskSlug, role, input, launchMode) {
18
37
  const live = toRoleSessionRecordView(getRegisteredRoleSession(deps.registry, deps.runtime, taskSlug, role), deps.runtime);
19
38
  if (live && live.status === "running") {
20
- return live;
39
+ return withHarnessRevisionView(repoRoot, live);
21
40
  }
22
41
  const config = await deps.projectService.loadConfig(repoRoot);
23
42
  const task = await deps.taskService.loadTask(repoRoot, taskSlug);
@@ -62,6 +81,7 @@ export function createSessionService(deps) {
62
81
  rows: input.rows
63
82
  });
64
83
  const timestamp = now();
84
+ const harnessRevision = await readCurrentHarnessRevision(repoRoot);
65
85
  const record = {
66
86
  id: runtimeSession.id,
67
87
  claudeSessionId,
@@ -84,16 +104,17 @@ export function createSessionService(deps) {
84
104
  startedAt: runtimeSession.startedAt,
85
105
  updatedAt: timestamp,
86
106
  lastOutputAt: runtimeSession.lastOutputAt,
107
+ harnessRevision,
87
108
  exitCode: runtimeSession.exitCode
88
109
  };
89
110
  deps.registry.upsert(record);
90
111
  await persistRoleSessionRecord(deps.fs, repoRoot, taskRepoRoot, config.stateRoot, record);
91
- return record;
112
+ return withHarnessRevisionView(repoRoot, record);
92
113
  }
93
114
  async function launchProjectGateReviewerSession(repoRoot, input, launchMode, activeTaskSlug) {
94
115
  const live = toRoleSessionRecordView(getRegisteredProjectGateReviewerSession(deps.registry, deps.runtime), deps.runtime);
95
116
  if (live && live.status === "running") {
96
- return bindProjectGateReviewerSession(repoRoot, live, activeTaskSlug);
117
+ return withHarnessRevisionView(repoRoot, await bindProjectGateReviewerSession(repoRoot, live, activeTaskSlug));
97
118
  }
98
119
  const config = await deps.projectService.loadConfig(repoRoot);
99
120
  const persisted = await loadPersistedProjectGateReviewerSession(deps.fs, repoRoot);
@@ -139,6 +160,7 @@ export function createSessionService(deps) {
139
160
  rows: input.rows
140
161
  });
141
162
  const timestamp = now();
163
+ const harnessRevision = await readCurrentHarnessRevision(repoRoot);
142
164
  const record = {
143
165
  id: runtimeSession.id,
144
166
  claudeSessionId,
@@ -159,11 +181,12 @@ export function createSessionService(deps) {
159
181
  lastOutputAt: runtimeSession.lastOutputAt,
160
182
  activeTaskSlug,
161
183
  activeTaskRepoRoot,
184
+ harnessRevision,
162
185
  exitCode: runtimeSession.exitCode
163
186
  };
164
187
  deps.registry.upsert(record);
165
188
  await persistProjectGateReviewerSession(deps.fs, repoRoot, record);
166
- return record;
189
+ return withHarnessRevisionView(repoRoot, record);
167
190
  }
168
191
  async function bindProjectGateReviewerSession(repoRoot, record, activeTaskSlug) {
169
192
  if (!activeTaskSlug) {
@@ -185,7 +208,7 @@ export function createSessionService(deps) {
185
208
  async function launchProjectTranslatorSession(repoRoot, input, launchMode) {
186
209
  const live = toRoleSessionRecordView(getRegisteredProjectTranslatorSession(deps.registry, deps.runtime), deps.runtime);
187
210
  if (live && live.status === "running") {
188
- return live;
211
+ return withHarnessRevisionView(repoRoot, live);
189
212
  }
190
213
  const config = await deps.projectService.loadConfig(repoRoot);
191
214
  const persisted = await loadPersistedTranslatorSession(deps.fs, repoRoot);
@@ -228,6 +251,7 @@ export function createSessionService(deps) {
228
251
  rows: input.rows
229
252
  });
230
253
  const timestamp = now();
254
+ const harnessRevision = await readCurrentHarnessRevision(repoRoot);
231
255
  const record = {
232
256
  id: runtimeSession.id,
233
257
  claudeSessionId,
@@ -246,11 +270,135 @@ export function createSessionService(deps) {
246
270
  startedAt: runtimeSession.startedAt,
247
271
  updatedAt: timestamp,
248
272
  lastOutputAt: runtimeSession.lastOutputAt,
273
+ harnessRevision,
249
274
  exitCode: runtimeSession.exitCode
250
275
  };
251
276
  deps.registry.upsert(record);
252
277
  await persistTranslatorSession(deps.fs, repoRoot, record);
253
- return record;
278
+ return withHarnessRevisionView(repoRoot, record);
279
+ }
280
+ async function launchProjectHarnessEngineerSession(repoRoot, input, launchMode) {
281
+ const live = toRoleSessionRecordView(getRegisteredProjectHarnessEngineerSession(deps.registry, deps.runtime), deps.runtime);
282
+ if (live && live.status === "running") {
283
+ return withHarnessRevisionView(repoRoot, live);
284
+ }
285
+ const config = await deps.projectService.loadConfig(repoRoot);
286
+ const persisted = await loadPersistedHarnessEngineerSession(deps.fs, repoRoot);
287
+ const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
288
+ const model = normalizeClaudeModel(input.model ?? persisted?.model);
289
+ const effort = normalizeClaudeEffort(input.effort ?? persisted?.effort ?? "medium");
290
+ const claudeSessionId = launchMode === "resume"
291
+ ? persisted?.claudeSessionId
292
+ : randomUUID();
293
+ if (!claudeSessionId) {
294
+ throw new VcmError({
295
+ code: "HARNESS_ENGINEER_SESSION_MISSING",
296
+ message: "Harness Engineer does not have a session id to resume.",
297
+ statusCode: 409,
298
+ hint: "Start Harness Engineer once before using Resume."
299
+ });
300
+ }
301
+ await deps.fs.ensureDir(resolveRepoPath(repoRoot, HARNESS_ENGINEER_DIR));
302
+ const transcriptPath = launchMode === "resume" && persisted?.transcriptPath
303
+ ? persisted.transcriptPath
304
+ : claudeTranscriptPath(repoRoot, claudeSessionId);
305
+ const startCommand = {
306
+ ...deps.claude.buildRoleStartCommand(HARNESS_ENGINEER_ROLE, config.claudeCommand, permissionMode, claudeSessionId, launchMode === "resume", model, effort),
307
+ cwd: repoRoot
308
+ };
309
+ const runtimeSession = await deps.runtime.createSession({
310
+ taskSlug: PROJECT_HARNESS_ENGINEER_SCOPE,
311
+ role: HARNESS_ENGINEER_ROLE,
312
+ command: startCommand.command,
313
+ args: startCommand.args,
314
+ cwd: startCommand.cwd,
315
+ env: {
316
+ VCM_API_URL: deps.apiUrl,
317
+ VCM_TASK_REPO_ROOT: repoRoot,
318
+ VCM_TASK_SLUG: PROJECT_HARNESS_ENGINEER_SCOPE,
319
+ VCM_ROLE: HARNESS_ENGINEER_ROLE,
320
+ VCM_SESSION_ID: claudeSessionId
321
+ },
322
+ cols: input.cols,
323
+ rows: input.rows
324
+ });
325
+ const timestamp = now();
326
+ const harnessRevision = await readCurrentHarnessRevision(repoRoot);
327
+ const record = {
328
+ id: runtimeSession.id,
329
+ claudeSessionId,
330
+ transcriptPath,
331
+ taskSlug: PROJECT_HARNESS_ENGINEER_SCOPE,
332
+ role: HARNESS_ENGINEER_ROLE,
333
+ status: runtimeSession.status,
334
+ activityStatus: "idle",
335
+ command: startCommand.display,
336
+ permissionMode,
337
+ model,
338
+ effort,
339
+ cwd: startCommand.cwd,
340
+ terminalBackend: "node-pty",
341
+ pid: runtimeSession.pid,
342
+ startedAt: runtimeSession.startedAt,
343
+ updatedAt: timestamp,
344
+ lastOutputAt: runtimeSession.lastOutputAt,
345
+ harnessRevision,
346
+ exitCode: runtimeSession.exitCode
347
+ };
348
+ deps.registry.upsert(record);
349
+ await persistHarnessEngineerSession(deps.fs, repoRoot, record);
350
+ return withHarnessRevisionView(repoRoot, record);
351
+ }
352
+ async function notifyHarnessUpdatedForSession(repoRoot, session) {
353
+ const runtimeSession = deps.runtime.getSession(session.id);
354
+ if (!runtimeSession || runtimeSession.status !== "running") {
355
+ throw new VcmError({
356
+ code: "SESSION_NOT_RUNNING",
357
+ message: `${session.role} must be running before VCM can notify it about harness updates.`,
358
+ statusCode: 409,
359
+ hint: "Resume or restart the role to load the latest harness settings."
360
+ });
361
+ }
362
+ const currentRevision = await readCurrentHarnessRevision(repoRoot);
363
+ const timestamp = now();
364
+ await submitTerminalInput(deps.runtime, session.id, buildHarnessRefreshPrompt(session.role));
365
+ const updated = {
366
+ ...session,
367
+ harnessRevision: currentRevision,
368
+ harnessCurrentRevision: currentRevision,
369
+ harnessOutdated: false,
370
+ lastHarnessNotifyAt: timestamp,
371
+ updatedAt: timestamp
372
+ };
373
+ deps.registry.upsert(normalizeProjectScopedRecordForPersistence(updated));
374
+ await persistNotifiedHarnessSession(repoRoot, updated);
375
+ return withHarnessRevisionView(repoRoot, updated);
376
+ }
377
+ async function persistNotifiedHarnessSession(repoRoot, session) {
378
+ if (session.role === GATE_REVIEWER_ROLE) {
379
+ await persistProjectGateReviewerSession(deps.fs, repoRoot, {
380
+ ...session,
381
+ taskSlug: PROJECT_GATE_REVIEWER_SCOPE
382
+ });
383
+ return;
384
+ }
385
+ if (session.role === TRANSLATOR_ROLE) {
386
+ await persistTranslatorSession(deps.fs, repoRoot, {
387
+ ...session,
388
+ taskSlug: PROJECT_TRANSLATOR_SCOPE
389
+ });
390
+ return;
391
+ }
392
+ if (session.role === HARNESS_ENGINEER_ROLE) {
393
+ await persistHarnessEngineerSession(deps.fs, repoRoot, {
394
+ ...session,
395
+ taskSlug: PROJECT_HARNESS_ENGINEER_SCOPE
396
+ });
397
+ return;
398
+ }
399
+ const config = await deps.projectService.loadConfig(repoRoot);
400
+ const task = await deps.taskService.loadTask(repoRoot, session.taskSlug);
401
+ await persistRoleSessionRecord(deps.fs, repoRoot, getTaskRuntimeRepoRoot(task), config.stateRoot, session);
254
402
  }
255
403
  return {
256
404
  startProjectGateReviewerSession(repoRoot, input = {}) {
@@ -296,7 +444,8 @@ export function createSessionService(deps) {
296
444
  async getProjectGateReviewerSession(repoRoot) {
297
445
  const record = getRegisteredProjectGateReviewerSession(deps.registry, deps.runtime)
298
446
  ?? await loadPersistedProjectGateReviewerSession(deps.fs, repoRoot);
299
- return toRoleSessionRecordView(record, deps.runtime);
447
+ const view = toRoleSessionRecordView(record, deps.runtime);
448
+ return view ? withHarnessRevisionView(repoRoot, view) : undefined;
300
449
  },
301
450
  async ensureProjectGateReviewerSession(repoRoot, input = {}) {
302
451
  const existing = await this.getProjectGateReviewerSession(repoRoot);
@@ -356,7 +505,8 @@ export function createSessionService(deps) {
356
505
  async getProjectTranslatorSession(repoRoot) {
357
506
  const record = getRegisteredProjectTranslatorSession(deps.registry, deps.runtime)
358
507
  ?? await loadPersistedTranslatorSession(deps.fs, repoRoot);
359
- return toRoleSessionRecordView(record, deps.runtime);
508
+ const view = toRoleSessionRecordView(record, deps.runtime);
509
+ return view ? withHarnessRevisionView(repoRoot, view) : undefined;
360
510
  },
361
511
  async ensureProjectTranslatorSession(repoRoot, input = {}) {
362
512
  const existing = await this.getProjectTranslatorSession(repoRoot);
@@ -398,6 +548,113 @@ export function createSessionService(deps) {
398
548
  await persistTranslatorSession(deps.fs, repoRoot, updated);
399
549
  return updated;
400
550
  },
551
+ async notifyProjectTranslatorHarnessUpdated(repoRoot) {
552
+ const current = await this.getProjectTranslatorSession(repoRoot);
553
+ if (!current) {
554
+ throw new VcmError({
555
+ code: "SESSION_MISSING",
556
+ message: "Translator session has not been started.",
557
+ statusCode: 404
558
+ });
559
+ }
560
+ return notifyHarnessUpdatedForSession(repoRoot, current);
561
+ },
562
+ startProjectHarnessEngineerSession(repoRoot, input = {}) {
563
+ return launchProjectHarnessEngineerSession(repoRoot, input, "fresh");
564
+ },
565
+ resumeProjectHarnessEngineerSession(repoRoot, input = {}) {
566
+ return launchProjectHarnessEngineerSession(repoRoot, input, "resume");
567
+ },
568
+ async stopProjectHarnessEngineerSession(repoRoot) {
569
+ const existing = await this.getProjectHarnessEngineerSession(repoRoot);
570
+ if (!existing) {
571
+ throw new VcmError({
572
+ code: "SESSION_MISSING",
573
+ message: "Harness Engineer session has not been started.",
574
+ statusCode: 404
575
+ });
576
+ }
577
+ if (deps.runtime.getSession(existing.id)) {
578
+ await deps.runtime.stop(existing.id);
579
+ }
580
+ const updated = {
581
+ ...existing,
582
+ status: "exited",
583
+ activityStatus: "idle",
584
+ updatedAt: now()
585
+ };
586
+ deps.registry.upsert(updated);
587
+ await persistHarnessEngineerSession(deps.fs, repoRoot, updated);
588
+ return updated;
589
+ },
590
+ async restartProjectHarnessEngineerSession(repoRoot, input = {}) {
591
+ const existing = await this.getProjectHarnessEngineerSession(repoRoot);
592
+ if (!existing) {
593
+ return launchProjectHarnessEngineerSession(repoRoot, input, "fresh");
594
+ }
595
+ if (deps.runtime.getSession(existing.id)) {
596
+ await deps.runtime.stop(existing.id);
597
+ }
598
+ deps.registry.remove(existing.id);
599
+ return launchProjectHarnessEngineerSession(repoRoot, input, "fresh");
600
+ },
601
+ async getProjectHarnessEngineerSession(repoRoot) {
602
+ const record = getRegisteredProjectHarnessEngineerSession(deps.registry, deps.runtime)
603
+ ?? await loadPersistedHarnessEngineerSession(deps.fs, repoRoot);
604
+ const view = toRoleSessionRecordView(record, deps.runtime);
605
+ return view ? withHarnessRevisionView(repoRoot, view) : undefined;
606
+ },
607
+ async ensureProjectHarnessEngineerSession(repoRoot, input = {}) {
608
+ const existing = await this.getProjectHarnessEngineerSession(repoRoot);
609
+ if (existing?.status === "running") {
610
+ return existing;
611
+ }
612
+ if (existing?.claudeSessionId) {
613
+ return this.resumeProjectHarnessEngineerSession(repoRoot, {
614
+ permissionMode: input.permissionMode ?? existing.permissionMode,
615
+ model: input.model ?? existing.model,
616
+ effort: input.effort ?? existing.effort,
617
+ cols: input.cols,
618
+ rows: input.rows
619
+ });
620
+ }
621
+ return this.startProjectHarnessEngineerSession(repoRoot, input);
622
+ },
623
+ async recordProjectHarnessEngineerHookEvent(repoRoot, input) {
624
+ const current = await this.getProjectHarnessEngineerSession(repoRoot);
625
+ if (!current) {
626
+ return undefined;
627
+ }
628
+ const timestamp = now();
629
+ const isTurnEnd = isTurnEndHook(input.eventName);
630
+ const isCompact = isCompactHook(input.eventName);
631
+ const updated = {
632
+ ...current,
633
+ claudeSessionId: input.sessionId ?? current.claudeSessionId,
634
+ transcriptPath: input.transcriptPath ?? current.transcriptPath,
635
+ cwd: input.cwd ?? current.cwd,
636
+ activityStatus: isTurnEnd ? "idle" : isCompact ? current.activityStatus : "running",
637
+ lastHookEventAt: timestamp,
638
+ lastTurnEndedAt: isTurnEnd ? timestamp : current.lastTurnEndedAt,
639
+ lastTurnStartedAt: isTurnEnd || isCompact ? current.lastTurnStartedAt : timestamp,
640
+ lastCompactAt: isCompact ? timestamp : current.lastCompactAt,
641
+ updatedAt: timestamp
642
+ };
643
+ deps.registry.upsert(updated);
644
+ await persistHarnessEngineerSession(deps.fs, repoRoot, updated);
645
+ return updated;
646
+ },
647
+ async notifyProjectHarnessEngineerHarnessUpdated(repoRoot) {
648
+ const current = await this.getProjectHarnessEngineerSession(repoRoot);
649
+ if (!current) {
650
+ throw new VcmError({
651
+ code: "SESSION_MISSING",
652
+ message: "Harness Engineer session has not been started.",
653
+ statusCode: 404
654
+ });
655
+ }
656
+ return notifyHarnessUpdatedForSession(repoRoot, current);
657
+ },
401
658
  startRoleSession(repoRoot, taskSlug, role, input = {}) {
402
659
  if (role === GATE_REVIEWER_ROLE) {
403
660
  return launchProjectGateReviewerSession(repoRoot, input, "fresh", taskSlug)
@@ -407,6 +664,10 @@ export function createSessionService(deps) {
407
664
  void taskSlug;
408
665
  return this.startProjectTranslatorSession(repoRoot, input);
409
666
  }
667
+ if (role === HARNESS_ENGINEER_ROLE) {
668
+ void taskSlug;
669
+ return this.startProjectHarnessEngineerSession(repoRoot, input);
670
+ }
410
671
  return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
411
672
  },
412
673
  resumeRoleSession(repoRoot, taskSlug, role, input = {}) {
@@ -418,6 +679,10 @@ export function createSessionService(deps) {
418
679
  void taskSlug;
419
680
  return this.resumeProjectTranslatorSession(repoRoot, input);
420
681
  }
682
+ if (role === HARNESS_ENGINEER_ROLE) {
683
+ void taskSlug;
684
+ return this.resumeProjectHarnessEngineerSession(repoRoot, input);
685
+ }
421
686
  return launchRoleSession(repoRoot, taskSlug, role, input, "resume");
422
687
  },
423
688
  async stopRoleSession(repoRoot, taskSlug, role) {
@@ -428,6 +693,10 @@ export function createSessionService(deps) {
428
693
  void taskSlug;
429
694
  return this.stopProjectTranslatorSession(repoRoot);
430
695
  }
696
+ if (role === HARNESS_ENGINEER_ROLE) {
697
+ void taskSlug;
698
+ return this.stopProjectHarnessEngineerSession(repoRoot);
699
+ }
431
700
  const existing = await this.getRoleSession(repoRoot, taskSlug, role);
432
701
  if (!existing) {
433
702
  throw new VcmError({
@@ -466,6 +735,10 @@ export function createSessionService(deps) {
466
735
  void taskSlug;
467
736
  return this.restartProjectTranslatorSession(repoRoot, input);
468
737
  }
738
+ if (role === HARNESS_ENGINEER_ROLE) {
739
+ void taskSlug;
740
+ return this.restartProjectHarnessEngineerSession(repoRoot, input);
741
+ }
469
742
  const existing = await this.getRoleSession(repoRoot, taskSlug, role);
470
743
  if (!existing) {
471
744
  return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
@@ -482,12 +755,17 @@ export function createSessionService(deps) {
482
755
  if (!session) {
483
756
  return undefined;
484
757
  }
485
- return scopeProjectRoleSession(await bindProjectGateReviewerSession(repoRoot, session, taskSlug), taskSlug);
758
+ const scoped = scopeProjectRoleSession(await bindProjectGateReviewerSession(repoRoot, session, taskSlug), taskSlug);
759
+ return scoped ? withHarnessRevisionView(repoRoot, scoped) : undefined;
486
760
  }
487
761
  if (role === TRANSLATOR_ROLE) {
488
762
  void taskSlug;
489
763
  return this.getProjectTranslatorSession(repoRoot);
490
764
  }
765
+ if (role === HARNESS_ENGINEER_ROLE) {
766
+ void taskSlug;
767
+ return this.getProjectHarnessEngineerSession(repoRoot);
768
+ }
491
769
  const config = await deps.projectService.loadConfig(repoRoot);
492
770
  const task = await deps.taskService.loadTask(repoRoot, taskSlug);
493
771
  const taskRepoRoot = getTaskRuntimeRepoRoot(task);
@@ -496,7 +774,8 @@ export function createSessionService(deps) {
496
774
  if (!record) {
497
775
  return undefined;
498
776
  }
499
- return toRoleSessionRecordView(record, deps.runtime);
777
+ const view = toRoleSessionRecordView(record, deps.runtime);
778
+ return view ? withHarnessRevisionView(repoRoot, view) : undefined;
500
779
  },
501
780
  async listRoleSessions(repoRoot, taskSlug) {
502
781
  const sessions = [];
@@ -516,7 +795,18 @@ export function createSessionService(deps) {
516
795
  if (gateReviewerSession) {
517
796
  sessions.push(gateReviewerSession);
518
797
  }
519
- return sessions;
798
+ return Promise.all(sessions.map((session) => withHarnessRevisionView(repoRoot, session)));
799
+ },
800
+ async notifyRoleHarnessUpdated(repoRoot, taskSlug, role) {
801
+ const current = await this.getRoleSession(repoRoot, taskSlug, role);
802
+ if (!current) {
803
+ throw new VcmError({
804
+ code: "SESSION_MISSING",
805
+ message: `${role} session has not been started.`,
806
+ statusCode: 404
807
+ });
808
+ }
809
+ return notifyHarnessUpdatedForSession(repoRoot, current);
520
810
  },
521
811
  async recordRoleHookEvent(repoRoot, input) {
522
812
  if (input.role === GATE_REVIEWER_ROLE) {
@@ -553,6 +843,15 @@ export function createSessionService(deps) {
553
843
  cwd: input.cwd
554
844
  });
555
845
  }
846
+ if (input.role === HARNESS_ENGINEER_ROLE) {
847
+ void input.taskSlug;
848
+ return this.recordProjectHarnessEngineerHookEvent(repoRoot, {
849
+ eventName: input.eventName,
850
+ sessionId: input.sessionId,
851
+ transcriptPath: input.transcriptPath,
852
+ cwd: input.cwd
853
+ });
854
+ }
556
855
  const current = await this.getRoleSession(repoRoot, input.taskSlug, input.role);
557
856
  if (!current || (!input.allowSessionMismatch && !matchesRoleHookSession(current, input))) {
558
857
  return undefined;
@@ -704,7 +1003,7 @@ function getHandoffArtifactPath(paths, role) {
704
1003
  return undefined;
705
1004
  }
706
1005
  function getRegisteredRoleSession(registry, runtime, taskSlug, role) {
707
- if (role !== TRANSLATOR_ROLE && role !== GATE_REVIEWER_ROLE) {
1006
+ if (role !== TRANSLATOR_ROLE && role !== GATE_REVIEWER_ROLE && role !== HARNESS_ENGINEER_ROLE) {
708
1007
  return registry.getByRole(taskSlug, role);
709
1008
  }
710
1009
  const candidates = registry.list().filter((session) => session.role === role);
@@ -724,11 +1023,16 @@ function getRegisteredProjectTranslatorSession(registry, runtime) {
724
1023
  const live = candidates.find((session) => runtime.getSession(session.id)?.status === "running");
725
1024
  return live ?? candidates.sort(compareSessionUpdatedAtDesc)[0];
726
1025
  }
1026
+ function getRegisteredProjectHarnessEngineerSession(registry, runtime) {
1027
+ const candidates = registry.list().filter((session) => session.role === HARNESS_ENGINEER_ROLE);
1028
+ const live = candidates.find((session) => runtime.getSession(session.id)?.status === "running");
1029
+ return live ?? candidates.sort(compareSessionUpdatedAtDesc)[0];
1030
+ }
727
1031
  function compareSessionUpdatedAtDesc(left, right) {
728
1032
  return (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "");
729
1033
  }
730
1034
  function scopeProjectRoleSession(record, taskSlug) {
731
- if (!record || (record.role !== TRANSLATOR_ROLE && record.role !== GATE_REVIEWER_ROLE)) {
1035
+ if (!record || (record.role !== TRANSLATOR_ROLE && record.role !== GATE_REVIEWER_ROLE && record.role !== HARNESS_ENGINEER_ROLE)) {
732
1036
  return record;
733
1037
  }
734
1038
  return {
@@ -745,6 +1049,10 @@ async function loadPersistedRoleRecordForRole(fs, baseRepoRoot, taskRepoRoot, st
745
1049
  void taskSlug;
746
1050
  return loadPersistedTranslatorSession(fs, baseRepoRoot);
747
1051
  }
1052
+ if (role === HARNESS_ENGINEER_ROLE) {
1053
+ void taskSlug;
1054
+ return loadPersistedHarnessEngineerSession(fs, baseRepoRoot);
1055
+ }
748
1056
  return loadPersistedRoleRecord(fs, taskRepoRoot, stateRoot, taskSlug, role);
749
1057
  }
750
1058
  async function loadPersistedProjectGateReviewerSession(fs, repoRoot) {
@@ -777,6 +1085,21 @@ async function loadPersistedTranslatorSession(fs, repoRoot) {
777
1085
  taskSlug: PROJECT_TRANSLATOR_SCOPE
778
1086
  };
779
1087
  }
1088
+ async function loadPersistedHarnessEngineerSession(fs, repoRoot) {
1089
+ const sessionPath = resolveRepoPath(repoRoot, HARNESS_ENGINEER_SESSION_PATH);
1090
+ if (!(await fs.pathExists(sessionPath))) {
1091
+ return undefined;
1092
+ }
1093
+ const payload = await fs.readJson(sessionPath);
1094
+ const record = normalizePersistedRoleRecord(isProjectRoleSessionFile(payload) ? payload.record : payload);
1095
+ if (!record) {
1096
+ return undefined;
1097
+ }
1098
+ return {
1099
+ ...record,
1100
+ taskSlug: PROJECT_HARNESS_ENGINEER_SCOPE
1101
+ };
1102
+ }
780
1103
  async function loadPersistedRoleRecord(fs, repoRoot, stateRoot, taskSlug, role) {
781
1104
  const current = await loadPersistedTaskSessionRecord(fs, repoRoot, stateRoot, taskSlug);
782
1105
  return normalizePersistedRoleRecord(current?.roles[role]?.record);
@@ -805,6 +1128,40 @@ function normalizePersistedRoleRecord(record) {
805
1128
  }
806
1129
  : undefined;
807
1130
  }
1131
+ function normalizeProjectScopedRecordForPersistence(record) {
1132
+ if (record.role === GATE_REVIEWER_ROLE) {
1133
+ return {
1134
+ ...record,
1135
+ taskSlug: PROJECT_GATE_REVIEWER_SCOPE
1136
+ };
1137
+ }
1138
+ if (record.role === TRANSLATOR_ROLE) {
1139
+ return {
1140
+ ...record,
1141
+ taskSlug: PROJECT_TRANSLATOR_SCOPE
1142
+ };
1143
+ }
1144
+ if (record.role === HARNESS_ENGINEER_ROLE) {
1145
+ return {
1146
+ ...record,
1147
+ taskSlug: PROJECT_HARNESS_ENGINEER_SCOPE
1148
+ };
1149
+ }
1150
+ return record;
1151
+ }
1152
+ function normalizeHarnessRevision(value) {
1153
+ return typeof value === "number" && Number.isInteger(value) && value >= 0
1154
+ ? value
1155
+ : 0;
1156
+ }
1157
+ function buildHarnessRefreshPrompt(role) {
1158
+ return [
1159
+ "VCM harness was updated.",
1160
+ "Before continuing, re-read the current project `CLAUDE.md`, your agent definition, and any relevant VCM skills from disk.",
1161
+ `Your agent definition is \`.claude/agents/${role}.md\` when that file exists.`,
1162
+ "Follow the latest rules from disk. Briefly acknowledge when ready."
1163
+ ].join("\n");
1164
+ }
808
1165
  async function persistTaskSession(fs, repoRoot, stateRoot, session) {
809
1166
  const sessionPath = getTaskSessionPath(repoRoot, stateRoot, session.taskSlug);
810
1167
  const empty = createEmptyTaskSessionRecord(session.taskSlug, session.updatedAt);
@@ -839,6 +1196,10 @@ async function persistRoleSessionRecord(fs, baseRepoRoot, taskRepoRoot, stateRoo
839
1196
  await persistTranslatorSession(fs, baseRepoRoot, session);
840
1197
  return;
841
1198
  }
1199
+ if (session.role === HARNESS_ENGINEER_ROLE) {
1200
+ await persistHarnessEngineerSession(fs, baseRepoRoot, session);
1201
+ return;
1202
+ }
842
1203
  await persistTaskSession(fs, taskRepoRoot, stateRoot, session);
843
1204
  }
844
1205
  async function persistProjectGateReviewerSession(fs, repoRoot, session) {
@@ -863,6 +1224,17 @@ async function persistTranslatorSession(fs, repoRoot, session) {
863
1224
  }
864
1225
  });
865
1226
  }
1227
+ async function persistHarnessEngineerSession(fs, repoRoot, session) {
1228
+ await fs.writeJsonAtomic(resolveRepoPath(repoRoot, HARNESS_ENGINEER_SESSION_PATH), {
1229
+ version: 1,
1230
+ role: session.role,
1231
+ updatedAt: session.updatedAt,
1232
+ record: {
1233
+ ...session,
1234
+ taskSlug: PROJECT_HARNESS_ENGINEER_SCOPE
1235
+ }
1236
+ });
1237
+ }
866
1238
  function isProjectRoleSessionFile(value) {
867
1239
  return "record" in value && typeof value.record === "object" && value.record !== null;
868
1240
  }
@@ -0,0 +1,63 @@
1
+ export function renderHarnessEngineerHarnessRules() {
2
+ return `## Role
3
+
4
+ You are VCM \`harness-engineer\`: a project-scoped harness maintenance tool role.
5
+
6
+ Maintain and improve this repository's VCM harness. Understand both VCM fixed
7
+ harness rules and project-specific harness customization before proposing any
8
+ change.
9
+
10
+ ## Scope
11
+
12
+ You may inspect:
13
+
14
+ - \`CLAUDE.md\`
15
+ - \`.claude/agents/**\`
16
+ - \`.claude/skills/**\`
17
+ - \`.ai/tools/**\`
18
+ - \`.ai/vcm-harness-manifest.json\`
19
+ - \`.ai/generated/**\`
20
+ - durable project docs such as \`docs/ARCHITECTURE.md\`, \`docs/TESTING.md\`,
21
+ and \`docs/known-issues.md\`
22
+
23
+ Do not act as PM, Architect, Coder, Reviewer, Gate Reviewer, or Translator.
24
+ You are not part of the task workflow round state.
25
+
26
+ ## Change Policy
27
+
28
+ - Propose harness changes as reviewable diffs.
29
+ - Do not silently apply edits.
30
+ - Do not overwrite VCM fixed managed blocks.
31
+ - Keep project-specific customization outside VCM managed blocks.
32
+ - If a fixed managed block appears wrong, draft a VCM issue instead of editing
33
+ the block.
34
+ - Include affected files, impacted roles, session restart/reminder impact, and
35
+ validation recommendations with every proposal.
36
+ - Do not edit production source code as part of harness maintenance.
37
+
38
+ ## VCM Feedback
39
+
40
+ If the issue is a VCM product, installer, UI, or fixed template problem, draft a
41
+ GitHub issue for:
42
+
43
+ \`https://github.com/CodingForMoney/VibeCodingMaster\`
44
+
45
+ Issue drafts must include title, problem, reproduction, expected behavior,
46
+ actual behavior, VCM version when known, affected harness/UI area, impact, and a
47
+ suggested fix if known.
48
+
49
+ Do not submit issues yourself. Do not include private source code, secrets,
50
+ private logs, or unnecessary repository details. Summarize private context
51
+ instead of copying it.
52
+
53
+ ## Output
54
+
55
+ When asked to improve harness content, respond with:
56
+
57
+ 1. diagnosis
58
+ 2. proposed diff or issue draft
59
+ 3. affected roles/sessions
60
+ 4. validation steps
61
+ 5. whether the user should apply, revise, or discard
62
+ `;
63
+ }