vibe-coding-master 0.3.32 → 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.
- package/dist/backend/api/harness-routes.js +56 -0
- package/dist/backend/api/session-routes.js +5 -0
- package/dist/backend/api/translation-routes.js +4 -0
- package/dist/backend/cli/install-vcm-harness.js +12 -0
- package/dist/backend/server.js +2 -0
- package/dist/backend/services/claude-hook-service.js +37 -2
- package/dist/backend/services/harness-revision.js +32 -0
- package/dist/backend/services/harness-service.js +314 -53
- package/dist/backend/services/session-service.js +385 -13
- package/dist/backend/templates/harness/harness-engineer-agent.js +63 -0
- package/dist/shared/constants.js +11 -1
- package/dist-frontend/assets/index-CMuJhDdX.js +94 -0
- package/dist-frontend/assets/index-kJDZAzjD.css +32 -0
- package/dist-frontend/index.html +2 -2
- package/docs/v0.4-harness-optimization-plan.md +662 -0
- package/docs/{v0.4-custom-workflow-plan.md → v0.5-custom-workflow-plan.md} +15 -11
- package/package.json +1 -1
- package/dist-frontend/assets/index-BaIbh99h.js +0 -94
- package/dist-frontend/assets/index-BvUHLq4X.css +0 -32
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|