vibe-coding-master 0.7.4 → 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -16
- package/dist/backend/adapters/git-adapter.js +15 -0
- package/dist/backend/api/artifact-routes.js +3 -0
- package/dist/backend/api/harness-routes.js +50 -27
- package/dist/backend/api/runtime-state-routes.js +7 -3
- package/dist/backend/api/task-routes.js +36 -4
- package/dist/backend/api/translation-routes.js +11 -2
- package/dist/backend/api/translation-worker-routes.js +37 -9
- package/dist/backend/cli/install-vcm-harness.js +40 -2
- package/dist/backend/gateway/gateway-service.js +34 -17
- package/dist/backend/server.js +12 -3
- package/dist/backend/services/artifact-service.js +5 -1
- package/dist/backend/services/auto-memory-service.js +156 -81
- package/dist/backend/services/claude-hook-service.js +50 -35
- package/dist/backend/services/command-dispatcher.js +1 -1
- package/dist/backend/services/gate-review-service.js +335 -31
- package/dist/backend/services/harness-feedback-service.js +19 -8
- package/dist/backend/services/harness-service.js +112 -34
- package/dist/backend/services/message-service.js +39 -2
- package/dist/backend/services/round-service.js +10 -121
- package/dist/backend/services/runtime-coordinator-service.js +18 -10
- package/dist/backend/services/runtime-recovery-service.js +1 -2
- package/dist/backend/services/session-service.js +36 -98
- package/dist/backend/services/status-service.js +1 -0
- package/dist/backend/services/task-close-service.js +12 -27
- package/dist/backend/services/task-workflow-service.js +228 -0
- package/dist/backend/services/translation-worker-service.js +14 -7
- package/dist/backend/templates/handoff.js +128 -1
- package/dist/backend/templates/harness/architect-agent.js +85 -22
- package/dist/backend/templates/harness/claude-root.js +25 -29
- package/dist/backend/templates/harness/coder-agent.js +5 -7
- package/dist/backend/templates/harness/coder-worker-agent.js +3 -3
- package/dist/backend/templates/harness/gate-review.js +292 -65
- package/dist/backend/templates/harness/harness-engineer-agent.js +8 -8
- package/dist/backend/templates/harness/memory-block.js +69 -0
- package/dist/backend/templates/harness/project-known-issues.js +1 -0
- package/dist/backend/templates/harness/project-manager-agent.js +217 -75
- package/dist/backend/templates/harness/role-memory.js +9 -12
- package/dist/backend/templates/harness/tester-agent.js +8 -4
- package/dist/backend/templates/harness/vcm-architecture-interview-skill.js +82 -0
- package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +4 -3
- package/dist/backend/templates/harness/vcm-harness-bootstrap-skill.js +14 -3
- package/dist/backend/templates/harness/vcm-propose-memory-skill.js +2 -2
- package/dist/backend/templates/harness/vcm-route-message-skill.js +5 -0
- package/dist/backend/templates/harness/vcm-task-state-skill.js +110 -0
- package/dist/backend/templates/message-envelope.js +1 -1
- package/dist/shared/constants.js +0 -10
- package/dist/shared/types/workflow.js +1 -0
- package/dist/shared/validation/artifact-check.js +41 -1
- package/dist-frontend/assets/index-BO2AuF-q.js +97 -0
- package/dist-frontend/assets/index-C2etsYlK.css +32 -0
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/scripts/harness-tools/check-durable-docs +298 -0
- package/scripts/verify-package.mjs +1 -0
- package/dist-frontend/assets/index-DCb-S6Ls.css +0 -32
- package/dist-frontend/assets/index-NTlycxx9.js +0 -97
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import { isVcmRoleName } from "../../shared/constants.js";
|
|
4
4
|
const DEFAULT_SETTLE_MS = 10_000;
|
|
5
5
|
const setGlobalTimeout = globalThis.setTimeout.bind(globalThis);
|
|
6
6
|
const clearGlobalTimeout = globalThis.clearTimeout.bind(globalThis);
|
|
@@ -52,7 +52,7 @@ export function createRoundService(deps) {
|
|
|
52
52
|
return undefined;
|
|
53
53
|
}
|
|
54
54
|
const active = sessions
|
|
55
|
-
.filter((session) => session.status === "running" && session.activityStatus === "running")
|
|
55
|
+
.filter((session) => isVcmRoleName(session.role) && session.status === "running" && session.activityStatus === "running")
|
|
56
56
|
.sort((left, right) => timestampMs(roleActivityTimestamp(right, timestamp)) - timestampMs(roleActivityTimestamp(left, timestamp)))[0];
|
|
57
57
|
if (!active) {
|
|
58
58
|
return undefined;
|
|
@@ -109,8 +109,6 @@ export function createRoundService(deps) {
|
|
|
109
109
|
...state,
|
|
110
110
|
currentRound: stopped,
|
|
111
111
|
lastStoppedRound: stopped,
|
|
112
|
-
awaitingUser: resolveAwaitingUserOnStop(state.awaitingUser, current.activeRole, stopped.stoppedAt ?? timestamp, state.pendingUserReply),
|
|
113
|
-
pendingUserReply: undefined,
|
|
114
112
|
updatedAt: timestamp
|
|
115
113
|
};
|
|
116
114
|
await save(input, next);
|
|
@@ -205,17 +203,15 @@ export function createRoundService(deps) {
|
|
|
205
203
|
roundId: shouldStartNewRound ? id() : current?.id ?? "",
|
|
206
204
|
settleMs
|
|
207
205
|
});
|
|
208
|
-
|
|
209
|
-
const next = applyPendingUserReplyStash(answered, input, timestamp);
|
|
210
|
-
await save(input, next);
|
|
206
|
+
await save(input, recorded);
|
|
211
207
|
if (input.eventName === "UserPromptSubmit") {
|
|
212
208
|
clearSettleTimer(input);
|
|
213
209
|
await updateSessionStatus(input, "running");
|
|
214
210
|
}
|
|
215
|
-
else if (
|
|
216
|
-
scheduleSettleTimer(input,
|
|
211
|
+
else if (recorded.currentRound) {
|
|
212
|
+
scheduleSettleTimer(input, recorded.currentRound, timestamp, input.settleGuard);
|
|
217
213
|
}
|
|
218
|
-
return toSessionRoundState(
|
|
214
|
+
return toSessionRoundState(recorded, timestamp);
|
|
219
215
|
});
|
|
220
216
|
}
|
|
221
217
|
async function withTaskLock(input, run) {
|
|
@@ -425,7 +421,6 @@ function applyManualInterrupt(input) {
|
|
|
425
421
|
lastStoppedRound: stopped,
|
|
426
422
|
totalCompletedTurnCount: input.state.totalCompletedTurnCount + 1,
|
|
427
423
|
totalCcActiveMs: input.state.totalCcActiveMs + activeDurationMs,
|
|
428
|
-
pendingUserReply: undefined,
|
|
429
424
|
updatedAt: input.timestamp
|
|
430
425
|
};
|
|
431
426
|
}
|
|
@@ -450,7 +445,7 @@ function toSessionRoundState(state, updatedAt) {
|
|
|
450
445
|
totalCcActiveMs: state.totalCcActiveMs,
|
|
451
446
|
currentRoundCcActiveMs: 0,
|
|
452
447
|
roleRecovery: state.roleRecovery,
|
|
453
|
-
flowPause: computeFlowPause(undefined, state.roleRecovery
|
|
448
|
+
flowPause: computeFlowPause(undefined, state.roleRecovery),
|
|
454
449
|
roles: [],
|
|
455
450
|
updatedAt
|
|
456
451
|
};
|
|
@@ -480,7 +475,7 @@ function toSessionRoundState(state, updatedAt) {
|
|
|
480
475
|
totalCcActiveMs: state.totalCcActiveMs + activeDurationMs,
|
|
481
476
|
currentRoundCcActiveMs,
|
|
482
477
|
roleRecovery: state.roleRecovery,
|
|
483
|
-
flowPause: computeFlowPause(current, state.roleRecovery
|
|
478
|
+
flowPause: computeFlowPause(current, state.roleRecovery),
|
|
484
479
|
roles: current.roles,
|
|
485
480
|
updatedAt
|
|
486
481
|
};
|
|
@@ -489,14 +484,11 @@ function toSessionRoundState(state, updatedAt) {
|
|
|
489
484
|
* Authoritative flow-pause predicate (single source of truth). The frontend
|
|
490
485
|
* consumes this instead of re-deriving the pause decision.
|
|
491
486
|
*
|
|
492
|
-
* Reason precedence: `role-recovery-failed` > `
|
|
493
|
-
* - `awaiting-user` is emitted whenever `awaitingUser` is set and recovery has not
|
|
494
|
-
* failed. It is STICKY: it stays paused regardless of round status (the round may
|
|
495
|
-
* auto-continue under another role while the user's decision is still pending).
|
|
487
|
+
* Reason precedence: `role-recovery-failed` > `stopped-no-next-turn`.
|
|
496
488
|
* - `stopped-no-next-turn` is emitted only when a real round has ended and the auto
|
|
497
489
|
* flow has not advanced and we are not mid active-recovery.
|
|
498
490
|
*/
|
|
499
|
-
function computeFlowPause(current, roleRecovery
|
|
491
|
+
function computeFlowPause(current, roleRecovery) {
|
|
500
492
|
const recovering = roleRecovery?.status === "waiting" || roleRecovery?.status === "retrying";
|
|
501
493
|
if (recovering) {
|
|
502
494
|
return undefined;
|
|
@@ -514,16 +506,6 @@ function computeFlowPause(current, roleRecovery, awaitingUser) {
|
|
|
514
506
|
}
|
|
515
507
|
: undefined;
|
|
516
508
|
}
|
|
517
|
-
if (awaitingUser) {
|
|
518
|
-
return {
|
|
519
|
-
paused: true,
|
|
520
|
-
reason: "awaiting-user",
|
|
521
|
-
role: awaitingUser.role,
|
|
522
|
-
since: awaitingUser.since,
|
|
523
|
-
message: awaitingUser.message,
|
|
524
|
-
messageTruncated: awaitingUser.messageTruncated
|
|
525
|
-
};
|
|
526
|
-
}
|
|
527
509
|
if (roundStopped && !nonAlertingStop) {
|
|
528
510
|
return {
|
|
529
511
|
paused: true,
|
|
@@ -534,97 +516,6 @@ function computeFlowPause(current, roleRecovery, awaitingUser) {
|
|
|
534
516
|
}
|
|
535
517
|
return undefined;
|
|
536
518
|
}
|
|
537
|
-
/**
|
|
538
|
-
* Sticky await-user resolution on settle->stopped: keep any existing anchor
|
|
539
|
-
* untouched, and only set a new one when the settling role addresses the human
|
|
540
|
-
* operator. Non-user-facing roles never raise an await-user anchor.
|
|
541
|
-
*/
|
|
542
|
-
function resolveAwaitingUserOnStop(existing, role, since, pendingReply) {
|
|
543
|
-
if (existing) {
|
|
544
|
-
return existing;
|
|
545
|
-
}
|
|
546
|
-
if (!isUserFacingRole(role)) {
|
|
547
|
-
return undefined;
|
|
548
|
-
}
|
|
549
|
-
if (pendingReply && pendingReply.role === role) {
|
|
550
|
-
return {
|
|
551
|
-
role,
|
|
552
|
-
since,
|
|
553
|
-
capturedAt: pendingReply.capturedAt,
|
|
554
|
-
message: pendingReply.text,
|
|
555
|
-
messageTruncated: pendingReply.truncated
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
return { role, since };
|
|
559
|
-
}
|
|
560
|
-
/**
|
|
561
|
-
* Maintain the transient `pendingUserReply` stash. A user-facing role's Stop with
|
|
562
|
-
* a captured reply records it until settle promotes it to `awaitingUser.message`.
|
|
563
|
-
* The same role's next UserPromptSubmit obsoletes the stash (turn answered/resumed),
|
|
564
|
-
* so stale text can never attach to a later anchor.
|
|
565
|
-
*/
|
|
566
|
-
function applyPendingUserReplyStash(state, input, timestamp) {
|
|
567
|
-
if (input.eventName === "Stop" && isUserFacingRole(input.role) && input.userFacingReply) {
|
|
568
|
-
return {
|
|
569
|
-
...state,
|
|
570
|
-
pendingUserReply: {
|
|
571
|
-
role: input.role,
|
|
572
|
-
text: input.userFacingReply.text,
|
|
573
|
-
truncated: input.userFacingReply.truncated,
|
|
574
|
-
capturedAt: timestamp
|
|
575
|
-
}
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
if (input.eventName === "UserPromptSubmit"
|
|
579
|
-
&& state.pendingUserReply?.role === input.role) {
|
|
580
|
-
return { ...state, pendingUserReply: undefined };
|
|
581
|
-
}
|
|
582
|
-
return state;
|
|
583
|
-
}
|
|
584
|
-
/**
|
|
585
|
-
* Clear the await-user anchor only when the awaiting role itself receives its
|
|
586
|
-
* next UserPromptSubmit (the user answered / that role resumed). Other roles,
|
|
587
|
-
* other events, and round auto-continuation never clear it.
|
|
588
|
-
*/
|
|
589
|
-
function clearAwaitingUserIfAnswered(state, eventName, role) {
|
|
590
|
-
if (eventName !== "UserPromptSubmit" || state.awaitingUser?.role !== role) {
|
|
591
|
-
return state;
|
|
592
|
-
}
|
|
593
|
-
return { ...state, awaitingUser: undefined };
|
|
594
|
-
}
|
|
595
|
-
function normalizeAwaitingUser(input) {
|
|
596
|
-
if (!input || typeof input !== "object") {
|
|
597
|
-
return undefined;
|
|
598
|
-
}
|
|
599
|
-
const record = input;
|
|
600
|
-
if (typeof record.role !== "string" || typeof record.since !== "string") {
|
|
601
|
-
return undefined;
|
|
602
|
-
}
|
|
603
|
-
return {
|
|
604
|
-
role: record.role,
|
|
605
|
-
since: record.since,
|
|
606
|
-
capturedAt: typeof record.capturedAt === "string" ? record.capturedAt : undefined,
|
|
607
|
-
message: typeof record.message === "string" ? record.message : undefined,
|
|
608
|
-
messageTruncated: record.messageTruncated === true ? true : undefined
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
function normalizePendingUserReply(input) {
|
|
612
|
-
if (!input || typeof input !== "object") {
|
|
613
|
-
return undefined;
|
|
614
|
-
}
|
|
615
|
-
const record = input;
|
|
616
|
-
if (typeof record.role !== "string"
|
|
617
|
-
|| typeof record.text !== "string"
|
|
618
|
-
|| typeof record.capturedAt !== "string") {
|
|
619
|
-
return undefined;
|
|
620
|
-
}
|
|
621
|
-
return {
|
|
622
|
-
role: record.role,
|
|
623
|
-
text: record.text,
|
|
624
|
-
truncated: record.truncated === true,
|
|
625
|
-
capturedAt: record.capturedAt
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
519
|
function normalizeRoundFile(input, taskSlug, updatedAt) {
|
|
629
520
|
const legacy = input;
|
|
630
521
|
return {
|
|
@@ -637,8 +528,6 @@ function normalizeRoundFile(input, taskSlug, updatedAt) {
|
|
|
637
528
|
totalCompletedTurnCount: normalizeNumber(input.totalCompletedTurnCount ?? legacy.totalStopCount),
|
|
638
529
|
totalCcActiveMs: normalizeNumber(input.totalCcActiveMs),
|
|
639
530
|
roleRecovery: normalizeRoleRecovery(input.roleRecovery),
|
|
640
|
-
awaitingUser: normalizeAwaitingUser(input.awaitingUser),
|
|
641
|
-
pendingUserReply: normalizePendingUserReply(input.pendingUserReply),
|
|
642
531
|
updatedAt: typeof input.updatedAt === "string" ? input.updatedAt : updatedAt
|
|
643
532
|
};
|
|
644
533
|
}
|
|
@@ -111,12 +111,11 @@ export function createRuntimeCoordinatorService(deps) {
|
|
|
111
111
|
return activeTasks[0] ?? null;
|
|
112
112
|
}
|
|
113
113
|
async function reconcileHarnessEngineer(repoRoot, task) {
|
|
114
|
-
const existing = await deps.sessionService.
|
|
115
|
-
if (!
|
|
114
|
+
const existing = await deps.sessionService.getRoleSession(repoRoot, task.taskSlug, "harness-engineer");
|
|
115
|
+
if (!shouldAutoEnsureTaskToolSession(existing)) {
|
|
116
116
|
return;
|
|
117
117
|
}
|
|
118
|
-
await
|
|
119
|
-
taskSlug: task.taskSlug,
|
|
118
|
+
await ensureTaskToolRoleSession(repoRoot, task.taskSlug, "harness-engineer", {
|
|
120
119
|
permissionMode: existing?.permissionMode,
|
|
121
120
|
model: existing?.model,
|
|
122
121
|
effort: existing?.effort
|
|
@@ -126,22 +125,31 @@ export function createRuntimeCoordinatorService(deps) {
|
|
|
126
125
|
if (!enabled) {
|
|
127
126
|
return;
|
|
128
127
|
}
|
|
129
|
-
const existing = await deps.sessionService.
|
|
130
|
-
if (!
|
|
128
|
+
const existing = await deps.sessionService.getRoleSession(repoRoot, task.taskSlug, "translator");
|
|
129
|
+
if (!shouldAutoEnsureTaskToolSession(existing)) {
|
|
131
130
|
return;
|
|
132
131
|
}
|
|
133
|
-
await
|
|
134
|
-
taskSlug: task.taskSlug,
|
|
132
|
+
await ensureTaskToolRoleSession(repoRoot, task.taskSlug, "translator", {
|
|
135
133
|
permissionMode: existing?.permissionMode,
|
|
136
134
|
model: existing?.model,
|
|
137
135
|
effort: existing?.effort
|
|
138
136
|
});
|
|
139
137
|
}
|
|
140
|
-
function
|
|
138
|
+
function shouldAutoEnsureTaskToolSession(session) {
|
|
141
139
|
return Boolean(session && (session.status === "running" || session.claudeSessionId));
|
|
142
140
|
}
|
|
141
|
+
async function ensureTaskToolRoleSession(repoRoot, taskSlug, role, input) {
|
|
142
|
+
const existing = await deps.sessionService.getRoleSession(repoRoot, taskSlug, role);
|
|
143
|
+
if (existing?.status === "running") {
|
|
144
|
+
return existing;
|
|
145
|
+
}
|
|
146
|
+
if (existing?.claudeSessionId) {
|
|
147
|
+
return deps.sessionService.resumeRoleSession(repoRoot, taskSlug, role, input);
|
|
148
|
+
}
|
|
149
|
+
return deps.sessionService.startRoleSession(repoRoot, taskSlug, role, input);
|
|
150
|
+
}
|
|
143
151
|
async function startConversationTranslationListeners(repoRoot, task) {
|
|
144
|
-
const translator = await deps.sessionService.
|
|
152
|
+
const translator = await deps.sessionService.getRoleSession(repoRoot, task.taskSlug, "translator");
|
|
145
153
|
if (translator?.status !== "running") {
|
|
146
154
|
return;
|
|
147
155
|
}
|
|
@@ -17,7 +17,7 @@ export function createRuntimeRecoveryService(deps) {
|
|
|
17
17
|
};
|
|
18
18
|
await runStep(context, "cleanup translation runtime", () => deps.translationWorkerService?.cleanupStartupRuntime(repoRoot) ?? Promise.resolve());
|
|
19
19
|
const config = await deps.projectService.loadConfig(repoRoot);
|
|
20
|
-
await runStep(context, "recover project tool sessions", () => recoverProjectToolSessions(repoRoot, recoveredAt, context));
|
|
20
|
+
await runStep(context, "recover legacy project tool sessions", () => recoverProjectToolSessions(repoRoot, recoveredAt, context));
|
|
21
21
|
await runStep(context, "recover harness bootstrap", () => recoverHarnessBootstrap(repoRoot, recoveredAt, context));
|
|
22
22
|
await runStep(context, "cleanup legacy harness feedback state", () => cleanupLegacyHarnessFeedback(repoRoot, context));
|
|
23
23
|
const tasks = await deps.taskService.listTasks(repoRoot);
|
|
@@ -156,7 +156,6 @@ export function createRuntimeRecoveryService(deps) {
|
|
|
156
156
|
state.lastStoppedRound = current;
|
|
157
157
|
state.totalCompletedTurnCount = (state.totalCompletedTurnCount ?? 0) + completedIncrement;
|
|
158
158
|
state.totalCcActiveMs = (state.totalCcActiveMs ?? 0) + activeDurationMs;
|
|
159
|
-
state.pendingUserReply = undefined;
|
|
160
159
|
}
|
|
161
160
|
if (state.roleRecovery?.status === "waiting" || state.roleRecovery?.status === "retrying") {
|
|
162
161
|
state.roleRecovery = undefined;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { ROLE_NAMES, isDispatchableRole } from "../../shared/constants.js";
|
|
3
3
|
import { VcmError } from "../errors.js";
|
|
4
4
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
5
5
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
6
|
-
import { ensureTaskMemorySnapshot } from "./auto-memory-service.js";
|
|
7
6
|
import { claudeTranscriptPath } from "./claude-transcript-service.js";
|
|
8
7
|
import { readHarnessRevisionState } from "./harness-revision.js";
|
|
9
8
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
@@ -50,7 +49,6 @@ export function createSessionService(deps) {
|
|
|
50
49
|
const config = await deps.projectService.loadConfig(repoRoot);
|
|
51
50
|
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
52
51
|
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
53
|
-
await ensureTaskMemorySnapshot(deps.fs, repoRoot, taskRepoRoot);
|
|
54
52
|
const paths = deps.artifactService.getHandoffPaths(taskRepoRoot, task.handoffDir);
|
|
55
53
|
const persisted = await loadPersistedRoleRecordForRole(deps.fs, repoRoot, taskRepoRoot, config.stateRoot, taskSlug, role);
|
|
56
54
|
const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
|
|
@@ -124,8 +122,31 @@ export function createSessionService(deps) {
|
|
|
124
122
|
};
|
|
125
123
|
deps.registry.upsert(record);
|
|
126
124
|
await persistRoleSessionRecord(deps.fs, repoRoot, taskRepoRoot, config.stateRoot, record);
|
|
125
|
+
if (role === "project-manager") {
|
|
126
|
+
await restoreProjectManagerWorkflowContext(record, taskRepoRoot, config.stateRoot);
|
|
127
|
+
}
|
|
127
128
|
return withHarnessRevisionView(repoRoot, record);
|
|
128
129
|
}
|
|
130
|
+
async function restoreProjectManagerWorkflowContext(record, taskRepoRoot, stateRoot) {
|
|
131
|
+
if (!deps.taskWorkflowService || record.status !== "running") {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const state = await deps.taskWorkflowService.getState({
|
|
136
|
+
taskRepoRoot,
|
|
137
|
+
stateRoot,
|
|
138
|
+
taskSlug: record.taskSlug
|
|
139
|
+
});
|
|
140
|
+
const context = deps.taskWorkflowService.renderPmResumeContext(state);
|
|
141
|
+
if (!context || await waitForSessionInputReady(record.id) === "exited") {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
await submitTerminalInput(deps.runtime, record.id, context);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Workflow context is advisory; restore failures cannot fail session launch.
|
|
148
|
+
}
|
|
149
|
+
}
|
|
129
150
|
async function launchProjectTranslatorSession(repoRoot, input, launchMode) {
|
|
130
151
|
const taskContext = await resolveProjectToolTaskContext(repoRoot, input, "Translator");
|
|
131
152
|
const live = toRoleSessionRecordView(getRegisteredProjectTranslatorSession(deps.registry, deps.runtime), deps.runtime);
|
|
@@ -237,7 +258,6 @@ export function createSessionService(deps) {
|
|
|
237
258
|
}
|
|
238
259
|
async function launchProjectHarnessEngineerSession(repoRoot, input, launchMode) {
|
|
239
260
|
const taskContext = await resolveProjectToolTaskContext(repoRoot, input, "Harness Engineer");
|
|
240
|
-
await ensureTaskMemorySnapshot(deps.fs, repoRoot, taskContext.taskRepoRoot);
|
|
241
261
|
const live = toRoleSessionRecordView(getRegisteredProjectHarnessEngineerSession(deps.registry, deps.runtime), deps.runtime);
|
|
242
262
|
if (live && live.status === "running") {
|
|
243
263
|
return withHarnessRevisionView(repoRoot, await migrateRunningProjectToolSessionCwd(repoRoot, live, taskContext.taskRepoRoot));
|
|
@@ -881,32 +901,12 @@ export function createSessionService(deps) {
|
|
|
881
901
|
return notifyHarnessUpdatedForSession(repoRoot, current);
|
|
882
902
|
},
|
|
883
903
|
startRoleSession(repoRoot, taskSlug, role, input = {}) {
|
|
884
|
-
if (role === TRANSLATOR_ROLE) {
|
|
885
|
-
return this.startProjectTranslatorSession(repoRoot, { ...input, taskSlug });
|
|
886
|
-
}
|
|
887
|
-
if (role === HARNESS_ENGINEER_ROLE) {
|
|
888
|
-
return this.startProjectHarnessEngineerSession(repoRoot, { ...input, taskSlug });
|
|
889
|
-
}
|
|
890
904
|
return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
|
|
891
905
|
},
|
|
892
906
|
resumeRoleSession(repoRoot, taskSlug, role, input = {}) {
|
|
893
|
-
if (role === TRANSLATOR_ROLE) {
|
|
894
|
-
return this.resumeProjectTranslatorSession(repoRoot, { ...input, taskSlug });
|
|
895
|
-
}
|
|
896
|
-
if (role === HARNESS_ENGINEER_ROLE) {
|
|
897
|
-
return this.resumeProjectHarnessEngineerSession(repoRoot, { ...input, taskSlug });
|
|
898
|
-
}
|
|
899
907
|
return launchRoleSession(repoRoot, taskSlug, role, input, "resume");
|
|
900
908
|
},
|
|
901
909
|
async stopRoleSession(repoRoot, taskSlug, role) {
|
|
902
|
-
if (role === TRANSLATOR_ROLE) {
|
|
903
|
-
void taskSlug;
|
|
904
|
-
return this.stopProjectTranslatorSession(repoRoot);
|
|
905
|
-
}
|
|
906
|
-
if (role === HARNESS_ENGINEER_ROLE) {
|
|
907
|
-
void taskSlug;
|
|
908
|
-
return this.stopProjectHarnessEngineerSession(repoRoot);
|
|
909
|
-
}
|
|
910
910
|
const existing = await this.getRoleSession(repoRoot, taskSlug, role);
|
|
911
911
|
if (!existing) {
|
|
912
912
|
throw new VcmError({
|
|
@@ -931,12 +931,6 @@ export function createSessionService(deps) {
|
|
|
931
931
|
return updated;
|
|
932
932
|
},
|
|
933
933
|
async restartRoleSession(repoRoot, taskSlug, role, input = {}) {
|
|
934
|
-
if (role === TRANSLATOR_ROLE) {
|
|
935
|
-
return this.restartProjectTranslatorSession(repoRoot, { ...input, taskSlug });
|
|
936
|
-
}
|
|
937
|
-
if (role === HARNESS_ENGINEER_ROLE) {
|
|
938
|
-
return this.restartProjectHarnessEngineerSession(repoRoot, { ...input, taskSlug });
|
|
939
|
-
}
|
|
940
934
|
const existing = await this.getRoleSession(repoRoot, taskSlug, role);
|
|
941
935
|
if (!existing) {
|
|
942
936
|
return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
|
|
@@ -951,14 +945,6 @@ export function createSessionService(deps) {
|
|
|
951
945
|
return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
|
|
952
946
|
},
|
|
953
947
|
async getRoleSession(repoRoot, taskSlug, role) {
|
|
954
|
-
if (role === TRANSLATOR_ROLE) {
|
|
955
|
-
void taskSlug;
|
|
956
|
-
return this.getProjectTranslatorSession(repoRoot);
|
|
957
|
-
}
|
|
958
|
-
if (role === HARNESS_ENGINEER_ROLE) {
|
|
959
|
-
void taskSlug;
|
|
960
|
-
return this.getProjectHarnessEngineerSession(repoRoot);
|
|
961
|
-
}
|
|
962
948
|
const config = await deps.projectService.loadConfig(repoRoot);
|
|
963
949
|
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
964
950
|
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
@@ -976,7 +962,7 @@ export function createSessionService(deps) {
|
|
|
976
962
|
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
977
963
|
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
978
964
|
const persistedTaskSession = await loadPersistedTaskSessionRecord(deps.fs, taskRepoRoot, config.stateRoot, taskSlug);
|
|
979
|
-
for (const role of
|
|
965
|
+
for (const role of ROLE_NAMES) {
|
|
980
966
|
const record = deps.registry.getByRole(taskSlug, role)
|
|
981
967
|
?? normalizePersistedRoleRecord(persistedTaskSession?.roles[role]?.record);
|
|
982
968
|
const session = toRoleSessionRecordView(record, deps.runtime);
|
|
@@ -998,24 +984,6 @@ export function createSessionService(deps) {
|
|
|
998
984
|
return notifyHarnessUpdatedForSession(repoRoot, current);
|
|
999
985
|
},
|
|
1000
986
|
async recordRoleHookEvent(repoRoot, input) {
|
|
1001
|
-
if (input.role === TRANSLATOR_ROLE) {
|
|
1002
|
-
void input.taskSlug;
|
|
1003
|
-
return this.recordProjectTranslatorHookEvent(repoRoot, {
|
|
1004
|
-
eventName: input.eventName,
|
|
1005
|
-
sessionId: input.sessionId,
|
|
1006
|
-
transcriptPath: input.transcriptPath,
|
|
1007
|
-
cwd: input.cwd
|
|
1008
|
-
});
|
|
1009
|
-
}
|
|
1010
|
-
if (input.role === HARNESS_ENGINEER_ROLE) {
|
|
1011
|
-
void input.taskSlug;
|
|
1012
|
-
return this.recordProjectHarnessEngineerHookEvent(repoRoot, {
|
|
1013
|
-
eventName: input.eventName,
|
|
1014
|
-
sessionId: input.sessionId,
|
|
1015
|
-
transcriptPath: input.transcriptPath,
|
|
1016
|
-
cwd: input.cwd
|
|
1017
|
-
});
|
|
1018
|
-
}
|
|
1019
987
|
const current = await this.getRoleSession(repoRoot, input.taskSlug, input.role);
|
|
1020
988
|
if (!current || (!input.allowSessionMismatch && !matchesRoleHookSession(current, input))) {
|
|
1021
989
|
return undefined;
|
|
@@ -1060,13 +1028,13 @@ export function createSessionService(deps) {
|
|
|
1060
1028
|
if (!role || !taskSlug) {
|
|
1061
1029
|
return undefined;
|
|
1062
1030
|
}
|
|
1063
|
-
if (role === TRANSLATOR_ROLE) {
|
|
1031
|
+
if (role === TRANSLATOR_ROLE && taskSlug === PROJECT_TRANSLATOR_SCOPE) {
|
|
1064
1032
|
const current = await getProjectToolSessionView(repoRoot, TRANSLATOR_ROLE);
|
|
1065
1033
|
return current?.id === sessionId
|
|
1066
1034
|
? markProjectToolActivityIdle(repoRoot, current, persistTranslatorSession)
|
|
1067
1035
|
: undefined;
|
|
1068
1036
|
}
|
|
1069
|
-
if (role === HARNESS_ENGINEER_ROLE) {
|
|
1037
|
+
if (role === HARNESS_ENGINEER_ROLE && taskSlug === PROJECT_HARNESS_ENGINEER_SCOPE) {
|
|
1070
1038
|
const current = await getProjectToolSessionView(repoRoot, HARNESS_ENGINEER_ROLE);
|
|
1071
1039
|
return current?.id === sessionId
|
|
1072
1040
|
? markProjectToolActivityIdle(repoRoot, current, persistHarnessEngineerSession)
|
|
@@ -1094,15 +1062,13 @@ export function createSessionService(deps) {
|
|
|
1094
1062
|
return updated;
|
|
1095
1063
|
},
|
|
1096
1064
|
async markRoleActivityIdle(repoRoot, taskSlug, role) {
|
|
1097
|
-
if (role === TRANSLATOR_ROLE) {
|
|
1098
|
-
void taskSlug;
|
|
1065
|
+
if (role === TRANSLATOR_ROLE && taskSlug === PROJECT_TRANSLATOR_SCOPE) {
|
|
1099
1066
|
const current = await getProjectToolSessionView(repoRoot, TRANSLATOR_ROLE);
|
|
1100
1067
|
return current
|
|
1101
1068
|
? markProjectToolActivityIdle(repoRoot, current, persistTranslatorSession)
|
|
1102
1069
|
: undefined;
|
|
1103
1070
|
}
|
|
1104
|
-
if (role === HARNESS_ENGINEER_ROLE) {
|
|
1105
|
-
void taskSlug;
|
|
1071
|
+
if (role === HARNESS_ENGINEER_ROLE && taskSlug === PROJECT_HARNESS_ENGINEER_SCOPE) {
|
|
1106
1072
|
const current = await getProjectToolSessionView(repoRoot, HARNESS_ENGINEER_ROLE);
|
|
1107
1073
|
return current
|
|
1108
1074
|
? markProjectToolActivityIdle(repoRoot, current, persistHarnessEngineerSession)
|
|
@@ -1227,15 +1193,8 @@ function getHandoffArtifactPath(paths, role) {
|
|
|
1227
1193
|
return undefined;
|
|
1228
1194
|
}
|
|
1229
1195
|
function getRegisteredRoleSession(registry, runtime, taskSlug, role) {
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
}
|
|
1233
|
-
const candidates = registry.list().filter((session) => session.role === role);
|
|
1234
|
-
const live = candidates.find((session) => runtime.getSession(session.id)?.status === "running");
|
|
1235
|
-
const scoped = live
|
|
1236
|
-
?? candidates.find((session) => session.taskSlug === taskSlug)
|
|
1237
|
-
?? candidates.sort(compareSessionUpdatedAtDesc)[0];
|
|
1238
|
-
return scopeProjectRoleSession(scoped, taskSlug);
|
|
1196
|
+
void runtime;
|
|
1197
|
+
return registry.getByRole(taskSlug, role);
|
|
1239
1198
|
}
|
|
1240
1199
|
function getRegisteredProjectTranslatorSession(registry, runtime) {
|
|
1241
1200
|
const candidates = registry.list().filter((session) => session.role === TRANSLATOR_ROLE);
|
|
@@ -1250,24 +1209,8 @@ function getRegisteredProjectHarnessEngineerSession(registry, runtime) {
|
|
|
1250
1209
|
function compareSessionUpdatedAtDesc(left, right) {
|
|
1251
1210
|
return (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "");
|
|
1252
1211
|
}
|
|
1253
|
-
function scopeProjectRoleSession(record, taskSlug) {
|
|
1254
|
-
if (!record || (record.role !== TRANSLATOR_ROLE && record.role !== HARNESS_ENGINEER_ROLE)) {
|
|
1255
|
-
return record;
|
|
1256
|
-
}
|
|
1257
|
-
return {
|
|
1258
|
-
...record,
|
|
1259
|
-
taskSlug
|
|
1260
|
-
};
|
|
1261
|
-
}
|
|
1262
1212
|
async function loadPersistedRoleRecordForRole(fs, baseRepoRoot, taskRepoRoot, stateRoot, taskSlug, role) {
|
|
1263
|
-
|
|
1264
|
-
void taskSlug;
|
|
1265
|
-
return loadPersistedTranslatorSession(fs, baseRepoRoot);
|
|
1266
|
-
}
|
|
1267
|
-
if (role === HARNESS_ENGINEER_ROLE) {
|
|
1268
|
-
void taskSlug;
|
|
1269
|
-
return loadPersistedHarnessEngineerSession(fs, baseRepoRoot);
|
|
1270
|
-
}
|
|
1213
|
+
void baseRepoRoot;
|
|
1271
1214
|
return loadPersistedRoleRecord(fs, taskRepoRoot, stateRoot, taskSlug, role);
|
|
1272
1215
|
}
|
|
1273
1216
|
async function loadPersistedTranslatorSession(fs, repoRoot) {
|
|
@@ -1350,10 +1293,12 @@ function normalizeHarnessRevision(value) {
|
|
|
1350
1293
|
}
|
|
1351
1294
|
function buildHarnessRefreshPrompt(role) {
|
|
1352
1295
|
return [
|
|
1296
|
+
"[VCM HARNESS UPDATED]",
|
|
1353
1297
|
"VCM harness was updated.",
|
|
1354
1298
|
"Before continuing, re-read the current project `CLAUDE.md`, your agent definition, and any relevant VCM skills from disk.",
|
|
1355
1299
|
`Your agent definition is \`.claude/agents/${role}.md\` when that file exists.`,
|
|
1356
|
-
"Follow the latest rules from disk. Briefly acknowledge when ready."
|
|
1300
|
+
"Follow the latest rules from disk. Briefly acknowledge when ready.",
|
|
1301
|
+
"[/VCM HARNESS UPDATED]"
|
|
1357
1302
|
].join("\n");
|
|
1358
1303
|
}
|
|
1359
1304
|
async function persistTaskSession(fs, repoRoot, stateRoot, session) {
|
|
@@ -1402,14 +1347,7 @@ async function clearPersistedRoleSessionRecord(fs, repoRoot, stateRoot, taskSlug
|
|
|
1402
1347
|
});
|
|
1403
1348
|
}
|
|
1404
1349
|
async function persistRoleSessionRecord(fs, baseRepoRoot, taskRepoRoot, stateRoot, session) {
|
|
1405
|
-
|
|
1406
|
-
await persistTranslatorSession(fs, baseRepoRoot, session);
|
|
1407
|
-
return;
|
|
1408
|
-
}
|
|
1409
|
-
if (session.role === HARNESS_ENGINEER_ROLE) {
|
|
1410
|
-
await persistHarnessEngineerSession(fs, baseRepoRoot, session);
|
|
1411
|
-
return;
|
|
1412
|
-
}
|
|
1350
|
+
void baseRepoRoot;
|
|
1413
1351
|
await persistTaskSession(fs, taskRepoRoot, stateRoot, session);
|
|
1414
1352
|
}
|
|
1415
1353
|
async function persistTranslatorSession(fs, repoRoot, session) {
|
|
@@ -50,6 +50,7 @@ function degradedArtifactSummary(handoffDir) {
|
|
|
50
50
|
messagesDir,
|
|
51
51
|
roleCommandPaths: Object.fromEntries(DISPATCHABLE_ROLES.map((role) => [role, `${roleCommandsDir}/${role}.md`])),
|
|
52
52
|
messageRoutePaths: {},
|
|
53
|
+
architectureBriefPath: `${handoffDir}/architecture-brief.md`,
|
|
53
54
|
architecturePlanPath: `${handoffDir}/architecture-plan.md`,
|
|
54
55
|
knownIssuesPath: `${handoffDir}/known-issues.md`,
|
|
55
56
|
testReportPath: `${handoffDir}/test-report.md`,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { VcmError } from "../errors.js";
|
|
1
|
+
import { ROLE_NAMES } from "../../shared/constants.js";
|
|
3
2
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
4
3
|
export function createTaskCloseService(deps) {
|
|
5
4
|
return {
|
|
@@ -7,10 +6,18 @@ export function createTaskCloseService(deps) {
|
|
|
7
6
|
const task = await deps.taskService.markTaskCleaned(repoRoot, taskSlug);
|
|
8
7
|
const warnings = [];
|
|
9
8
|
await stopTaskRoleSessions(repoRoot, taskSlug, warnings);
|
|
10
|
-
await moveOrStopProjectToolSession("Translator", () => deps.sessionService.moveProjectTranslatorSessionToSafeCwd(repoRoot), () => deps.sessionService.stopProjectTranslatorSession(repoRoot), warnings);
|
|
11
|
-
await moveOrStopProjectToolSession("Harness Engineer", () => deps.sessionService.moveProjectHarnessEngineerSessionToSafeCwd(repoRoot), () => deps.sessionService.stopProjectHarnessEngineerSession(repoRoot), warnings);
|
|
12
9
|
await bestEffort("Unable to stop task translation runtime", () => deps.translationService.stopTask(getTaskRuntimeRepoRoot(task), taskSlug, { clearCache: true }), warnings);
|
|
13
10
|
await bestEffort("Unable to clear task round runtime", () => deps.roundService.stopTask(taskSlug), warnings);
|
|
11
|
+
if (deps.projectService && deps.taskWorkflowService) {
|
|
12
|
+
await bestEffort("Unable to clear task workflow state", async () => {
|
|
13
|
+
const config = await deps.projectService.loadConfig(repoRoot);
|
|
14
|
+
await deps.taskWorkflowService.clearState({
|
|
15
|
+
taskRepoRoot: getTaskRuntimeRepoRoot(task),
|
|
16
|
+
stateRoot: config.stateRoot,
|
|
17
|
+
taskSlug
|
|
18
|
+
});
|
|
19
|
+
}, warnings);
|
|
20
|
+
}
|
|
14
21
|
try {
|
|
15
22
|
const result = await deps.taskService.cleanupTask(repoRoot, taskSlug);
|
|
16
23
|
const combinedWarnings = [...warnings, ...(result.warnings ?? [])];
|
|
@@ -46,32 +53,13 @@ export function createTaskCloseService(deps) {
|
|
|
46
53
|
return;
|
|
47
54
|
}
|
|
48
55
|
for (const session of sessions) {
|
|
49
|
-
if (session.status !== "running" || !
|
|
56
|
+
if (session.status !== "running" || !ROLE_NAMES.some((role) => role === session.role)) {
|
|
50
57
|
continue;
|
|
51
58
|
}
|
|
52
59
|
await bestEffort(`Unable to stop ${session.role} session`, () => deps.sessionService.stopRoleSession(repoRoot, taskSlug, session.role), warnings);
|
|
53
60
|
}
|
|
54
61
|
}
|
|
55
62
|
}
|
|
56
|
-
async function moveOrStopProjectToolSession(label, move, stop, warnings) {
|
|
57
|
-
try {
|
|
58
|
-
await move();
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
if (isMissingSession(error)) {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
warnings.push(`Unable to move ${label} session to the base repository: ${describeError(error)}`);
|
|
65
|
-
try {
|
|
66
|
-
await stop();
|
|
67
|
-
}
|
|
68
|
-
catch (stopError) {
|
|
69
|
-
if (!isMissingSession(stopError)) {
|
|
70
|
-
warnings.push(`Unable to stop ${label} session after cwd migration failed: ${describeError(stopError)}`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
63
|
async function bestEffort(message, operation, warnings) {
|
|
76
64
|
try {
|
|
77
65
|
await operation();
|
|
@@ -80,9 +68,6 @@ async function bestEffort(message, operation, warnings) {
|
|
|
80
68
|
warnings.push(`${message}: ${describeError(error)}`);
|
|
81
69
|
}
|
|
82
70
|
}
|
|
83
|
-
function isMissingSession(error) {
|
|
84
|
-
return error instanceof VcmError && error.code === "SESSION_MISSING";
|
|
85
|
-
}
|
|
86
71
|
function describeError(error) {
|
|
87
72
|
return error instanceof Error ? error.message : String(error);
|
|
88
73
|
}
|