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.
Files changed (57) hide show
  1. package/README.md +19 -16
  2. package/dist/backend/adapters/git-adapter.js +15 -0
  3. package/dist/backend/api/artifact-routes.js +3 -0
  4. package/dist/backend/api/harness-routes.js +50 -27
  5. package/dist/backend/api/runtime-state-routes.js +7 -3
  6. package/dist/backend/api/task-routes.js +36 -4
  7. package/dist/backend/api/translation-routes.js +11 -2
  8. package/dist/backend/api/translation-worker-routes.js +37 -9
  9. package/dist/backend/cli/install-vcm-harness.js +40 -2
  10. package/dist/backend/gateway/gateway-service.js +34 -17
  11. package/dist/backend/server.js +12 -3
  12. package/dist/backend/services/artifact-service.js +5 -1
  13. package/dist/backend/services/auto-memory-service.js +156 -81
  14. package/dist/backend/services/claude-hook-service.js +50 -35
  15. package/dist/backend/services/command-dispatcher.js +1 -1
  16. package/dist/backend/services/gate-review-service.js +335 -31
  17. package/dist/backend/services/harness-feedback-service.js +19 -8
  18. package/dist/backend/services/harness-service.js +112 -34
  19. package/dist/backend/services/message-service.js +39 -2
  20. package/dist/backend/services/round-service.js +10 -121
  21. package/dist/backend/services/runtime-coordinator-service.js +18 -10
  22. package/dist/backend/services/runtime-recovery-service.js +1 -2
  23. package/dist/backend/services/session-service.js +36 -98
  24. package/dist/backend/services/status-service.js +1 -0
  25. package/dist/backend/services/task-close-service.js +12 -27
  26. package/dist/backend/services/task-workflow-service.js +228 -0
  27. package/dist/backend/services/translation-worker-service.js +14 -7
  28. package/dist/backend/templates/handoff.js +128 -1
  29. package/dist/backend/templates/harness/architect-agent.js +85 -22
  30. package/dist/backend/templates/harness/claude-root.js +25 -29
  31. package/dist/backend/templates/harness/coder-agent.js +5 -7
  32. package/dist/backend/templates/harness/coder-worker-agent.js +3 -3
  33. package/dist/backend/templates/harness/gate-review.js +292 -65
  34. package/dist/backend/templates/harness/harness-engineer-agent.js +8 -8
  35. package/dist/backend/templates/harness/memory-block.js +69 -0
  36. package/dist/backend/templates/harness/project-known-issues.js +1 -0
  37. package/dist/backend/templates/harness/project-manager-agent.js +217 -75
  38. package/dist/backend/templates/harness/role-memory.js +9 -12
  39. package/dist/backend/templates/harness/tester-agent.js +8 -4
  40. package/dist/backend/templates/harness/vcm-architecture-interview-skill.js +82 -0
  41. package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +4 -3
  42. package/dist/backend/templates/harness/vcm-harness-bootstrap-skill.js +14 -3
  43. package/dist/backend/templates/harness/vcm-propose-memory-skill.js +2 -2
  44. package/dist/backend/templates/harness/vcm-route-message-skill.js +5 -0
  45. package/dist/backend/templates/harness/vcm-task-state-skill.js +110 -0
  46. package/dist/backend/templates/message-envelope.js +1 -1
  47. package/dist/shared/constants.js +0 -10
  48. package/dist/shared/types/workflow.js +1 -0
  49. package/dist/shared/validation/artifact-check.js +41 -1
  50. package/dist-frontend/assets/index-BO2AuF-q.js +97 -0
  51. package/dist-frontend/assets/index-C2etsYlK.css +32 -0
  52. package/dist-frontend/index.html +2 -2
  53. package/package.json +1 -1
  54. package/scripts/harness-tools/check-durable-docs +298 -0
  55. package/scripts/verify-package.mjs +1 -0
  56. package/dist-frontend/assets/index-DCb-S6Ls.css +0 -32
  57. 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 { isUserFacingRole } from "../../shared/constants.js";
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
- const answered = clearAwaitingUserIfAnswered(recorded, input.eventName, input.role);
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 (next.currentRound) {
216
- scheduleSettleTimer(input, next.currentRound, timestamp, input.settleGuard);
211
+ else if (recorded.currentRound) {
212
+ scheduleSettleTimer(input, recorded.currentRound, timestamp, input.settleGuard);
217
213
  }
218
- return toSessionRoundState(next, timestamp);
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, state.awaitingUser),
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, state.awaitingUser),
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` > `awaiting-user` > `stopped-no-next-turn`.
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, awaitingUser) {
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.getProjectHarnessEngineerSession(repoRoot);
115
- if (!shouldAutoEnsureProjectToolSession(existing)) {
114
+ const existing = await deps.sessionService.getRoleSession(repoRoot, task.taskSlug, "harness-engineer");
115
+ if (!shouldAutoEnsureTaskToolSession(existing)) {
116
116
  return;
117
117
  }
118
- await deps.sessionService.ensureProjectHarnessEngineerSession(repoRoot, {
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.getProjectTranslatorSession(repoRoot);
130
- if (!shouldAutoEnsureProjectToolSession(existing)) {
128
+ const existing = await deps.sessionService.getRoleSession(repoRoot, task.taskSlug, "translator");
129
+ if (!shouldAutoEnsureTaskToolSession(existing)) {
131
130
  return;
132
131
  }
133
- await deps.sessionService.ensureProjectTranslatorSession(repoRoot, {
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 shouldAutoEnsureProjectToolSession(session) {
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.getProjectTranslatorSession(repoRoot);
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 { VCM_ROLE_NAMES, isDispatchableRole } from "../../shared/constants.js";
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 VCM_ROLE_NAMES) {
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
- if (role !== TRANSLATOR_ROLE && role !== HARNESS_ENGINEER_ROLE) {
1231
- return registry.getByRole(taskSlug, role);
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
- if (role === TRANSLATOR_ROLE) {
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
- if (session.role === TRANSLATOR_ROLE) {
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 { VCM_ROLE_NAMES } from "../../shared/constants.js";
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" || !VCM_ROLE_NAMES.some((role) => role === session.role)) {
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
  }