vibe-coding-master 0.7.15 → 0.7.17

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 (33) hide show
  1. package/README.md +30 -9
  2. package/dist/backend/adapters/claude-adapter.js +4 -1
  3. package/dist/backend/api/session-routes.js +5 -0
  4. package/dist/backend/api/usage-analytics-routes.js +31 -0
  5. package/dist/backend/cli/install-vcm-harness.js +51 -2
  6. package/dist/backend/runtime/session-registry.js +7 -0
  7. package/dist/backend/server.js +26 -9
  8. package/dist/backend/services/architect-restart-service.js +136 -0
  9. package/dist/backend/services/ccr-integration-service.js +70 -11
  10. package/dist/backend/services/claude-hook-service.js +6 -0
  11. package/dist/backend/services/claude-transcript-service.js +10 -10
  12. package/dist/backend/services/gate-review-service.js +40 -0
  13. package/dist/backend/services/harness-service.js +51 -4
  14. package/dist/backend/services/message-service.js +5 -0
  15. package/dist/backend/services/runtime-coordinator-service.js +37 -42
  16. package/dist/backend/services/session-service.js +83 -19
  17. package/dist/backend/services/task-close-service.js +1 -0
  18. package/dist/backend/services/usage-analytics-service.js +346 -0
  19. package/dist/backend/templates/harness/architect-agent.js +18 -10
  20. package/dist/backend/templates/harness/architect-scaffold-worker-agent.js +23 -0
  21. package/dist/backend/templates/harness/claude-root.js +1 -1
  22. package/dist/backend/templates/harness/gate-review.js +7 -2
  23. package/dist/backend/templates/harness/project-manager-agent.js +1 -1
  24. package/dist/backend/templates/harness/restart-architect-skill.js +75 -0
  25. package/dist/backend/templates/harness/vcm-architecture-interview-skill.js +31 -5
  26. package/dist/backend/templates/harness/vcm-route-message-skill.js +3 -3
  27. package/dist/shared/types/usage-analytics.js +1 -0
  28. package/dist-frontend/assets/index-CStWyouh.js +97 -0
  29. package/dist-frontend/assets/{index-CiEUp9Si.css → index-Ci7z8tW3.css} +1 -1
  30. package/dist-frontend/index.html +2 -2
  31. package/package.json +1 -1
  32. package/dist/backend/adapters/claude-settings-adapter.js +0 -79
  33. package/dist-frontend/assets/index-BAE_pjXJ.js +0 -97
@@ -227,6 +227,9 @@ export function createClaudeHookService(deps) {
227
227
  role: input.role,
228
228
  prompt: stringOrUndefined(input.event.prompt)
229
229
  });
230
+ if (submitted) {
231
+ await deps.architectRestartService?.recordRouteAccepted(context.project.repoRoot, context.taskSlug, submitted);
232
+ }
230
233
  return {
231
234
  ok: true,
232
235
  eventName,
@@ -437,6 +440,9 @@ export function createClaudeHookService(deps) {
437
440
  occurredAt: session.lastTurnEndedAt ?? session.updatedAt
438
441
  });
439
442
  }
443
+ if (eventName === "Stop" && input.role === "architect" && session) {
444
+ await deps.architectRestartService?.recordArchitectStop(context.project.repoRoot, context.taskSlug, session.id);
445
+ }
440
446
  if (options.notifyGateway && session && input.role === "project-manager") {
441
447
  void deps.gatewayService?.handlePmStop({
442
448
  repoRoot: context.project.repoRoot,
@@ -208,14 +208,14 @@ export function resolveExistingClaudeTranscriptPath(session) {
208
208
  if (sessionPath) {
209
209
  return sessionPath;
210
210
  }
211
- const cwdPath = existingFile(claudeTranscriptPath(session.cwd, session.claudeSessionId));
211
+ const cwdPath = existingFile(claudeTranscriptPath(session.cwd, session.claudeSessionId, session.claudeConfigDir));
212
212
  if (cwdPath) {
213
213
  return cwdPath;
214
214
  }
215
- return findClaudeTranscriptPathBySessionId(session.claudeSessionId);
215
+ return findClaudeTranscriptPathBySessionId(session.claudeSessionId, session.claudeConfigDir);
216
216
  }
217
- export function findClaudeTranscriptPathBySessionId(claudeSessionId) {
218
- const root = claudeProjectsRoot();
217
+ export function findClaudeTranscriptPathBySessionId(claudeSessionId, configDir) {
218
+ const root = claudeProjectsRoot(configDir);
219
219
  let projectDirs;
220
220
  try {
221
221
  projectDirs = readdirSync(root, { withFileTypes: true });
@@ -253,17 +253,17 @@ function existingFile(candidate) {
253
253
  return undefined;
254
254
  }
255
255
  }
256
- export function claudeProjectsRoot() {
257
- return join(homedir(), ".claude", "projects");
256
+ export function claudeProjectsRoot(configDir) {
257
+ return join(configDir ?? join(homedir(), ".claude"), "projects");
258
258
  }
259
259
  export function projectHash(projectDir) {
260
260
  return projectDir.replace(/[\/\s]+/g, "-");
261
261
  }
262
- export function projectsTranscriptDir(projectDir) {
263
- return join(claudeProjectsRoot(), projectHash(projectDir));
262
+ export function projectsTranscriptDir(projectDir, configDir) {
263
+ return join(claudeProjectsRoot(configDir), projectHash(projectDir));
264
264
  }
265
- export function claudeTranscriptPath(projectDir, claudeSessionId) {
266
- return join(projectsTranscriptDir(projectDir), `${claudeSessionId}.jsonl`);
265
+ export function claudeTranscriptPath(projectDir, claudeSessionId, configDir) {
266
+ return join(projectsTranscriptDir(projectDir, configDir), `${claudeSessionId}.jsonl`);
267
267
  }
268
268
  export function parseAssistantContent(line) {
269
269
  let obj;
@@ -56,6 +56,7 @@ const CODE_DIFF_ANALYSIS_FIELDS = [
56
56
  const SOURCE_ARTIFACTS = {
57
57
  "architecture-plan": [
58
58
  ".ai/vcm/handoffs/architecture-brief.md",
59
+ ".ai/vcm/handoffs/architecture-evidence.md",
59
60
  ".ai/vcm/handoffs/architecture-plan.md"
60
61
  ],
61
62
  "validation-adequacy": [
@@ -228,6 +229,30 @@ export function createGateReviewService(deps) {
228
229
  message: architectureBriefError
229
230
  };
230
231
  }
232
+ const architectureEvidenceError = await readArchitectureEvidenceError(deps.fs, context.taskRepoRoot);
233
+ if (architectureEvidenceError) {
234
+ index = applyGateState(index, gate, {
235
+ status: "failed",
236
+ decision: undefined,
237
+ error: architectureEvidenceError,
238
+ exceptionReason: undefined,
239
+ requestId: undefined,
240
+ requestPath: undefined,
241
+ inputHash: undefined,
242
+ requestedAt: undefined,
243
+ startedAt: undefined,
244
+ completedAt: now(),
245
+ callbackStatus: "not_sent",
246
+ callbackError: undefined
247
+ }, now(), true);
248
+ await saveIndex(deps.fs, context.taskRepoRoot, index);
249
+ return {
250
+ status: "failed_to_start",
251
+ gate,
252
+ record: index.gates[gate],
253
+ message: architectureEvidenceError
254
+ };
255
+ }
231
256
  }
232
257
  const coreInput = await readCoreInputArtifact(deps.fs, context.taskRepoRoot, gate);
233
258
  if (coreInput && coreInput.status !== "ready") {
@@ -961,6 +986,21 @@ async function readArchitectureBriefError(fs, taskRepoRoot) {
961
986
  }
962
987
  return undefined;
963
988
  }
989
+ async function readArchitectureEvidenceError(fs, taskRepoRoot) {
990
+ const relativePath = ".ai/vcm/handoffs/architecture-evidence.md";
991
+ const absolutePath = resolveRepoPath(taskRepoRoot, relativePath);
992
+ if (!await fs.pathExists(absolutePath)) {
993
+ return `${relativePath} is missing. Complete architecture evidence before requesting architecture-plan review.`;
994
+ }
995
+ const content = await fs.readText(absolutePath);
996
+ if (content.trim().length === 0) {
997
+ return `${relativePath} is empty. Complete architecture evidence before requesting architecture-plan review.`;
998
+ }
999
+ if (!/^\s*Architecture Evidence Status\s*:\s*complete\s*$/im.test(content)) {
1000
+ return `${relativePath} is incomplete. Finish current-worktree evidence before requesting architecture-plan review.`;
1001
+ }
1002
+ return undefined;
1003
+ }
964
1004
  async function commandStdout(runner, cwd, args) {
965
1005
  const result = await runner.run("git", args, { cwd });
966
1006
  return result.exitCode === 0 ? result.stdout : "";
@@ -4,6 +4,7 @@ import { promisify } from "node:util";
4
4
  import { renderArchitectHarnessRules } from "../templates/harness/architect-agent.js";
5
5
  import { renderCoderHarnessRules } from "../templates/harness/coder-agent.js";
6
6
  import { renderCoderWorkerHarnessRules } from "../templates/harness/coder-worker-agent.js";
7
+ import { renderArchitectScaffoldWorkerHarnessRules } from "../templates/harness/architect-scaffold-worker-agent.js";
7
8
  import { renderGateReviewerAgentRules, renderRequestGateReviewTool, renderTranslatorAgentRules, renderVcmGateReviewSkillRules } from "../templates/harness/gate-review.js";
8
9
  import { renderHarnessEngineerHarnessRules } from "../templates/harness/harness-engineer-agent.js";
9
10
  import { renderRootClaudeHarnessRules } from "../templates/harness/claude-root.js";
@@ -24,6 +25,7 @@ import { renderVcmReportHarnessIssueSkillRules } from "../templates/harness/vcm-
24
25
  import { renderVcmRouteMessageSkillRules } from "../templates/harness/vcm-route-message-skill.js";
25
26
  import { renderUpdateTaskStateTool, renderVcmTaskStateSkillRules } from "../templates/harness/vcm-task-state-skill.js";
26
27
  import { renderCheckScaffoldLedgerTool } from "../templates/harness/check-scaffold-ledger.js";
28
+ import { renderRequestArchitectRestartTool, renderRestartArchitectSkillRules } from "../templates/harness/restart-architect-skill.js";
27
29
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
28
30
  import { VcmError } from "../errors.js";
29
31
  import { bumpHarnessRevision, readHarnessRevisionState } from "./harness-revision.js";
@@ -172,6 +174,14 @@ const HARNESS_FILES = [
172
174
  ownership: "whole-file",
173
175
  renderRules: renderVcmProposeMemorySkillRules
174
176
  },
177
+ {
178
+ kind: "skill-restart-architect",
179
+ path: ".claude/skills/restart-architect/SKILL.md",
180
+ title: "Restart Architect Skill",
181
+ frontmatter: renderSkillFrontmatter("restart-architect", "Use after Architect completes and commits architecture planning and scaffold work."),
182
+ ownership: "whole-file",
183
+ renderRules: renderRestartArchitectSkillRules
184
+ },
175
185
  {
176
186
  kind: "agent-gate-reviewer",
177
187
  path: ".claude/agents/gate-reviewer.md",
@@ -202,6 +212,13 @@ const HARNESS_FILES = [
202
212
  frontmatter: renderAgentFrontmatter("vcm-coder-worker", "Bounded VCM implementation worker for assigned modules, files, and VCM:CODE markers from Coder.", { model: "inherit" }),
203
213
  renderRules: renderCoderWorkerHarnessRules
204
214
  },
215
+ {
216
+ kind: "agent-architect-scaffold-worker",
217
+ path: ".claude/agents/vcm-architect-scaffold-worker.md",
218
+ title: "VCM Architect Scaffold Worker Agent",
219
+ frontmatter: renderAgentFrontmatter("vcm-architect-scaffold-worker", "Foreground Architect worker for exact scaffold execution and scaffold validation.", { model: "opus", effort: "xhigh" }),
220
+ renderRules: renderArchitectScaffoldWorkerHarnessRules
221
+ },
205
222
  {
206
223
  kind: "tool-request-gate-review",
207
224
  path: ".ai/tools/request-gate-review",
@@ -223,6 +240,13 @@ const HARNESS_FILES = [
223
240
  ownership: "raw-file",
224
241
  renderRules: renderCheckScaffoldLedgerTool
225
242
  },
243
+ {
244
+ kind: "tool-request-architect-restart",
245
+ path: ".ai/tools/request-architect-restart",
246
+ title: "Request Architect Restart Tool",
247
+ ownership: "raw-file",
248
+ renderRules: renderRequestArchitectRestartTool
249
+ },
226
250
  {
227
251
  kind: "agent-project-manager",
228
252
  path: ".claude/agents/project-manager.md",
@@ -237,7 +261,7 @@ const HARNESS_FILES = [
237
261
  title: "Architect Agent",
238
262
  memoryBlock: true,
239
263
  blankLineBeforeEnd: true,
240
- frontmatter: renderAgentFrontmatter("architect", "VCM architecture role for plans, module boundaries, public contracts, verifiable behavior, and docs sync."),
264
+ frontmatter: renderAgentFrontmatter("architect", "VCM architecture role for plans, module boundaries, public contracts, verifiable behavior, and docs sync.", { tools: "Read, Grep, Glob, Bash, Edit, Write, Agent" }),
241
265
  renderRules: renderArchitectHarnessRules
242
266
  },
243
267
  {
@@ -1192,6 +1216,9 @@ async function analyzeHarnessFile(fs, repoRoot, definition) {
1192
1216
  };
1193
1217
  }
1194
1218
  const insertedContent = `${currentContent.trimEnd()}\n\n${expectedBlock}\n`;
1219
+ const nextContent = definition.kind === "agent-architect"
1220
+ ? ensureAgentTool(insertedContent, "Agent")
1221
+ : insertedContent;
1195
1222
  return {
1196
1223
  definition,
1197
1224
  status: {
@@ -1206,13 +1233,16 @@ async function analyzeHarnessFile(fs, repoRoot, definition) {
1206
1233
  action: "insert",
1207
1234
  reason: "File exists but does not contain VCM managed rules."
1208
1235
  },
1209
- nextContent: definition.memoryBlock ? ensureVcmMemoryBlock(insertedContent) : insertedContent
1236
+ nextContent: definition.memoryBlock ? ensureVcmMemoryBlock(nextContent) : nextContent
1210
1237
  };
1211
1238
  }
1212
1239
  const managedVersion = match[1] ? Number(match[1]) : undefined;
1213
1240
  const currentBlock = match[0];
1214
1241
  const blockUpdatedContent = currentContent.replace(managedBlockPattern, expectedBlock);
1215
- const nextContent = definition.memoryBlock ? ensureVcmMemoryBlock(blockUpdatedContent) : blockUpdatedContent;
1242
+ const memoryUpdatedContent = definition.memoryBlock ? ensureVcmMemoryBlock(blockUpdatedContent) : blockUpdatedContent;
1243
+ const nextContent = definition.kind === "agent-architect"
1244
+ ? ensureAgentTool(memoryUpdatedContent, "Agent")
1245
+ : memoryUpdatedContent;
1216
1246
  const action = currentContent === nextContent ? "ok" : "update";
1217
1247
  return {
1218
1248
  definition,
@@ -1323,6 +1353,22 @@ function renderNewHarnessFile(definition, block, contentAfterBlock = definition.
1323
1353
  const suffix = contentAfterBlock?.trim();
1324
1354
  return `${frontmatter}# ${definition.title}\n\n${block}${suffix ? `\n\n${suffix}` : ""}\n`;
1325
1355
  }
1356
+ function ensureAgentTool(content, requiredTool) {
1357
+ const frontmatterMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---/);
1358
+ if (!frontmatterMatch) {
1359
+ return content;
1360
+ }
1361
+ const toolsMatch = frontmatterMatch[0].match(/^tools:\s*(.*)$/m);
1362
+ if (!toolsMatch) {
1363
+ return content.replace(/^(---\r?\n[\s\S]*?)(\r?\n---)/, `$1\ntools: ${requiredTool}$2`);
1364
+ }
1365
+ const tools = toolsMatch[1].split(",").map((tool) => tool.trim()).filter(Boolean);
1366
+ if (tools.includes(requiredTool)) {
1367
+ return content;
1368
+ }
1369
+ const nextTools = [...tools, requiredTool].join(", ");
1370
+ return content.replace(frontmatterMatch[0], frontmatterMatch[0].replace(toolsMatch[0], `tools: ${nextTools}`));
1371
+ }
1326
1372
  function migrateLegacyHarnessFile(definition, currentContent, block) {
1327
1373
  const legacyContent = definition.legacyWholeFile?.trimEnd();
1328
1374
  if (!legacyContent) {
@@ -1454,7 +1500,8 @@ function isPlainObject(value) {
1454
1500
  function renderAgentFrontmatter(name, description, options = {}) {
1455
1501
  const tools = options.tools ?? "Read, Grep, Glob, Bash, Edit, Write";
1456
1502
  const model = options.model ? `\nmodel: ${options.model}` : "";
1457
- return `---\nname: ${name}\ndescription: ${description}\ntools: ${tools}${model}\n---`;
1503
+ const effort = options.effort ? `\neffort: ${options.effort}` : "";
1504
+ return `---\nname: ${name}\ndescription: ${description}\ntools: ${tools}${model}${effort}\n---`;
1458
1505
  }
1459
1506
  function renderSkillFrontmatter(name, description) {
1460
1507
  return `---\nname: ${name}\ndescription: ${description}\n---`;
@@ -130,6 +130,11 @@ export function createMessageService(deps) {
130
130
  }).catch(() => undefined);
131
131
  }
132
132
  scheduleDispatchConfirmation(input, delivered, session.id);
133
+ await deps.onRouteDelivered?.({
134
+ repoRoot: input.repoRoot,
135
+ taskSlug: input.taskSlug,
136
+ message: delivered
137
+ });
133
138
  return {
134
139
  message: delivered,
135
140
  delivered: true,
@@ -34,6 +34,40 @@ export function createRuntimeCoordinatorService(deps) {
34
34
  }
35
35
  }
36
36
  }
37
+ function reconcileProject(repoRoot, input = {}) {
38
+ return withRepoLock(repoRoot, async () => {
39
+ const [activeTask, gatewayStatus] = await Promise.all([
40
+ resolveActiveTask(repoRoot, input.taskSlug),
41
+ deps.gatewayService.getStatus().catch(() => null)
42
+ ]);
43
+ const preferences = await deps.appSettings.getPreferences();
44
+ if (!activeTask) {
45
+ return { activeTask: null, gatewayStatus };
46
+ }
47
+ const taskRepoRoot = getTaskRuntimeRepoRoot(activeTask);
48
+ const stateRoot = await deps.getStateRoot(repoRoot);
49
+ await deps.turnReconciler.reconcileTask(repoRoot, activeTask, stateRoot);
50
+ const harnessInitialized = await deps.harnessService.getHarnessStatus(taskRepoRoot)
51
+ .then((status) => status.initialized)
52
+ .catch(() => false);
53
+ await Promise.all([
54
+ reconcileHarnessEngineer(repoRoot, activeTask),
55
+ reconcileTranslator(repoRoot, activeTask, preferences.translationEnabled && harnessInitialized)
56
+ ]);
57
+ if (preferences.translationEnabled && harnessInitialized) {
58
+ await startConversationTranslationListeners(repoRoot, activeTask);
59
+ }
60
+ else {
61
+ await deps.translationService.stopTask(taskRepoRoot, activeTask.taskSlug).catch(() => undefined);
62
+ }
63
+ await reconcileAutoMemory(repoRoot, activeTask, preferences.autoTaskHarnessReviewEnabled ? "auto" : undefined);
64
+ const memoryReadiness = await getTaskRetrospectiveMemoryReadiness(repoRoot, activeTask);
65
+ if ((preferences.autoTaskHarnessReviewEnabled || memoryReadiness.trigger) && memoryReadiness.ready) {
66
+ await maybeStartTaskHarnessRetrospective(repoRoot, activeTask, memoryReadiness.trigger ?? "auto");
67
+ }
68
+ return { activeTask, gatewayStatus };
69
+ });
70
+ }
37
71
  return {
38
72
  start() {
39
73
  if (reconcileTimer !== undefined) {
@@ -51,53 +85,14 @@ export function createRuntimeCoordinatorService(deps) {
51
85
  clearTimer(reconcileTimer);
52
86
  reconcileTimer = undefined;
53
87
  },
54
- reconcileProject(repoRoot, input = {}) {
55
- return withRepoLock(repoRoot, async () => {
56
- const [activeTask, gatewayStatus] = await Promise.all([
57
- resolveActiveTask(repoRoot, input.taskSlug),
58
- deps.gatewayService.getStatus().catch(() => null)
59
- ]);
60
- const preferences = await deps.appSettings.getPreferences();
61
- if (!activeTask) {
62
- return { activeTask: null, gatewayStatus };
63
- }
64
- const taskRepoRoot = getTaskRuntimeRepoRoot(activeTask);
65
- const stateRoot = await deps.getStateRoot(repoRoot);
66
- await deps.turnReconciler.reconcileTask(repoRoot, activeTask, stateRoot);
67
- const harnessInitialized = await deps.harnessService.getHarnessStatus(taskRepoRoot)
68
- .then((status) => status.initialized)
69
- .catch(() => false);
70
- await Promise.all([
71
- reconcileHarnessEngineer(repoRoot, activeTask),
72
- reconcileTranslator(repoRoot, activeTask, preferences.translationEnabled && harnessInitialized)
73
- ]);
74
- if (preferences.translationEnabled && harnessInitialized) {
75
- await startConversationTranslationListeners(repoRoot, activeTask);
76
- }
77
- else {
78
- await deps.translationService.stopTask(taskRepoRoot, activeTask.taskSlug).catch(() => undefined);
79
- }
80
- await reconcileAutoMemory(repoRoot, activeTask, preferences.autoTaskHarnessReviewEnabled ? "auto" : undefined);
81
- const memoryReadiness = await getTaskRetrospectiveMemoryReadiness(repoRoot, activeTask);
82
- if ((preferences.autoTaskHarnessReviewEnabled || memoryReadiness.trigger) && memoryReadiness.ready) {
83
- await maybeStartTaskHarnessRetrospective(repoRoot, activeTask, memoryReadiness.trigger ?? "auto");
84
- }
85
- return { activeTask, gatewayStatus };
86
- });
87
- }
88
+ reconcileProject
88
89
  };
89
90
  async function reconcileCurrentProject() {
90
91
  const project = await deps.projectService.getCurrentProject();
91
92
  if (!project) {
92
93
  return;
93
94
  }
94
- await withRepoLock(project.repoRoot, async () => {
95
- const activeTask = await resolveActiveTask(project.repoRoot);
96
- if (activeTask) {
97
- await deps.turnReconciler.reconcileTask(project.repoRoot, activeTask, await deps.getStateRoot(project.repoRoot));
98
- }
99
- return { activeTask, gatewayStatus: null };
100
- });
95
+ await reconcileProject(project.repoRoot);
101
96
  }
102
97
  async function resolveActiveTask(repoRoot, requestedTaskSlug) {
103
98
  const tasks = await deps.taskService.listTasks(repoRoot);
@@ -136,7 +131,7 @@ export function createRuntimeCoordinatorService(deps) {
136
131
  });
137
132
  }
138
133
  function shouldAutoEnsureTaskToolSession(session) {
139
- return Boolean(session && (session.status === "running" || session.claudeSessionId));
134
+ return !session || session.status === "running" || Boolean(session.claudeSessionId);
140
135
  }
141
136
  async function ensureTaskToolRoleSession(repoRoot, taskSlug, role, input) {
142
137
  const existing = await deps.sessionService.getRoleSession(repoRoot, taskSlug, role);
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import path from "node:path";
2
3
  import { ROLE_NAMES, isDispatchableRole } from "../../shared/constants.js";
3
4
  import { CCR_GPT_SESSION_MODEL, isCcrSessionModel } from "../../shared/types/session.js";
@@ -55,6 +56,9 @@ export function createSessionService(deps) {
55
56
  const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
56
57
  const model = normalizeClaudeModel(input.model ?? persisted?.model);
57
58
  const effort = normalizeClaudeEffort(input.effort ?? persisted?.effort);
59
+ if (launchMode === "resume" && persisted) {
60
+ assertResumeProviderCompatible(persisted, model);
61
+ }
58
62
  const [modelEnvironment, modelSettingsOverride] = await Promise.all([
59
63
  getModelLaunchEnvironment(model),
60
64
  getModelLaunchSettingsOverride(model)
@@ -74,10 +78,10 @@ export function createSessionService(deps) {
74
78
  const transcriptPath = launchMode === "resume" && persisted?.transcriptPath
75
79
  ? persisted.transcriptPath
76
80
  : resumeClaudeSessionId
77
- ? claudeTranscriptPath(taskRepoRoot, resumeClaudeSessionId)
81
+ ? claudeTranscriptPath(taskRepoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
78
82
  : undefined;
79
83
  const startCommand = {
80
- ...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride),
84
+ ...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride, input.appendSystemPrompt),
81
85
  cwd: taskRepoRoot
82
86
  };
83
87
  const runtimeSession = await deps.runtime.createSession({
@@ -94,7 +98,7 @@ export function createSessionService(deps) {
94
98
  VCM_TASK_SLUG: taskSlug,
95
99
  VCM_ROLE: role,
96
100
  VCM_SESSION_ID: claudeSessionId || undefined
97
- }, modelEnvironment),
101
+ }, modelEnvironment, buildUsageTelemetryEnvironment(deps.apiUrl, role, model)),
98
102
  cols: input.cols,
99
103
  rows: input.rows
100
104
  });
@@ -113,6 +117,7 @@ export function createSessionService(deps) {
113
117
  model,
114
118
  effort,
115
119
  cwd: startCommand.cwd,
120
+ claudeConfigDir: readClaudeConfigDir(modelEnvironment),
116
121
  terminalBackend: "node-pty",
117
122
  pid: runtimeSession.pid,
118
123
  roleCommandPath: isDispatchableRole(role)
@@ -163,6 +168,9 @@ export function createSessionService(deps) {
163
168
  const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
164
169
  const model = normalizeClaudeModel(input.model ?? persisted?.model);
165
170
  const effort = normalizeClaudeEffort(input.effort ?? persisted?.effort ?? "medium");
171
+ if (launchMode === "resume" && persisted) {
172
+ assertResumeProviderCompatible(persisted, model);
173
+ }
166
174
  const [modelEnvironment, modelSettingsOverride] = await Promise.all([
167
175
  getModelLaunchEnvironment(model),
168
176
  getModelLaunchSettingsOverride(model)
@@ -194,7 +202,7 @@ export function createSessionService(deps) {
194
202
  const sessionCwd = launchMode === "resume" ? persisted?.cwd ?? launchCwd : launchCwd;
195
203
  const claudeSessionId = resumeClaudeSessionId ?? "";
196
204
  const transcriptPath = resumeClaudeSessionId
197
- ? claudeTranscriptPath(repoRoot, resumeClaudeSessionId)
205
+ ? claudeTranscriptPath(repoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
198
206
  : undefined;
199
207
  const startCommand = {
200
208
  ...deps.claude.buildRoleStartCommand(TRANSLATOR_ROLE, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride),
@@ -217,7 +225,7 @@ export function createSessionService(deps) {
217
225
  VCM_TASK_SLUG: PROJECT_TRANSLATOR_SCOPE,
218
226
  VCM_ROLE: TRANSLATOR_ROLE,
219
227
  VCM_SESSION_ID: claudeSessionId || undefined
220
- }, modelEnvironment),
228
+ }, modelEnvironment, buildUsageTelemetryEnvironment(deps.apiUrl, TRANSLATOR_ROLE, model)),
221
229
  cols: input.cols,
222
230
  rows: input.rows
223
231
  });
@@ -236,6 +244,7 @@ export function createSessionService(deps) {
236
244
  model,
237
245
  effort,
238
246
  cwd: sessionCwd,
247
+ claudeConfigDir: readClaudeConfigDir(modelEnvironment),
239
248
  terminalBackend: "node-pty",
240
249
  pid: runtimeSession.pid,
241
250
  startedAt: runtimeSession.startedAt,
@@ -276,6 +285,9 @@ export function createSessionService(deps) {
276
285
  const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
277
286
  const model = normalizeClaudeModel(input.model ?? persisted?.model);
278
287
  const effort = normalizeClaudeEffort(input.effort ?? persisted?.effort ?? "medium");
288
+ if (launchMode === "resume" && persisted) {
289
+ assertResumeProviderCompatible(persisted, model);
290
+ }
279
291
  const [modelEnvironment, modelSettingsOverride] = await Promise.all([
280
292
  getModelLaunchEnvironment(model),
281
293
  getModelLaunchSettingsOverride(model)
@@ -303,7 +315,7 @@ export function createSessionService(deps) {
303
315
  const sessionCwd = launchMode === "resume" ? persisted?.cwd ?? launchCwd : launchCwd;
304
316
  const claudeSessionId = resumeClaudeSessionId ?? "";
305
317
  const transcriptPath = resumeClaudeSessionId
306
- ? claudeTranscriptPath(repoRoot, resumeClaudeSessionId)
318
+ ? claudeTranscriptPath(repoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
307
319
  : undefined;
308
320
  const startCommand = {
309
321
  ...deps.claude.buildRoleStartCommand(HARNESS_ENGINEER_ROLE, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride),
@@ -326,7 +338,7 @@ export function createSessionService(deps) {
326
338
  VCM_TASK_SLUG: PROJECT_HARNESS_ENGINEER_SCOPE,
327
339
  VCM_ROLE: HARNESS_ENGINEER_ROLE,
328
340
  VCM_SESSION_ID: claudeSessionId || undefined
329
- }, modelEnvironment),
341
+ }, modelEnvironment, buildUsageTelemetryEnvironment(deps.apiUrl, HARNESS_ENGINEER_ROLE, model)),
330
342
  cols: input.cols,
331
343
  rows: input.rows
332
344
  });
@@ -345,6 +357,7 @@ export function createSessionService(deps) {
345
357
  model,
346
358
  effort,
347
359
  cwd: sessionCwd,
360
+ claudeConfigDir: readClaudeConfigDir(modelEnvironment),
348
361
  terminalBackend: "node-pty",
349
362
  pid: runtimeSession.pid,
350
363
  startedAt: runtimeSession.startedAt,
@@ -489,6 +502,7 @@ export function createSessionService(deps) {
489
502
  const permissionMode = normalizeClaudePermissionMode(session.permissionMode);
490
503
  const model = normalizeClaudeModel(session.model);
491
504
  const effort = normalizeClaudeEffort(session.effort);
505
+ assertResumeProviderCompatible(session, model);
492
506
  const [modelEnvironment, modelSettingsOverride] = await Promise.all([
493
507
  getModelLaunchEnvironment(model),
494
508
  getModelLaunchSettingsOverride(model)
@@ -516,7 +530,7 @@ export function createSessionService(deps) {
516
530
  VCM_TASK_SLUG: normalizeProjectScopedRecordForPersistence(session).taskSlug,
517
531
  VCM_ROLE: session.role,
518
532
  VCM_SESSION_ID: session.claudeSessionId
519
- }, modelEnvironment)
533
+ }, modelEnvironment, buildUsageTelemetryEnvironment(deps.apiUrl, session.role, model))
520
534
  });
521
535
  if ((await waitForSessionInputReady(runtimeSession.id)) === "exited") {
522
536
  deps.registry.remove(runtimeSession.id);
@@ -546,13 +560,14 @@ export function createSessionService(deps) {
546
560
  permissionMode,
547
561
  model,
548
562
  effort,
563
+ claudeConfigDir: readClaudeConfigDir(modelEnvironment),
549
564
  pid: runtimeSession.pid,
550
565
  startedAt: runtimeSession.startedAt,
551
566
  updatedAt: timestamp,
552
567
  lastOutputAt: runtimeSession.lastOutputAt,
553
568
  exitCode: runtimeSession.exitCode,
554
569
  transcriptPath: session.claudeSessionId
555
- ? claudeTranscriptPath(repoRoot, session.claudeSessionId)
570
+ ? claudeTranscriptPath(repoRoot, session.claudeSessionId, session.claudeConfigDir)
556
571
  : session.transcriptPath
557
572
  };
558
573
  deps.registry.upsert(normalizeProjectScopedRecordForPersistence(resumed));
@@ -560,16 +575,16 @@ export function createSessionService(deps) {
560
575
  return migrateRunningProjectToolSessionCwd(repoRoot, resumed, targetCwd);
561
576
  }
562
577
  async function getModelLaunchEnvironment(model) {
563
- if (!isCcrSessionModel(model)) {
564
- return {};
565
- }
566
578
  if (!deps.ccrIntegration) {
567
- throw new VcmError({
568
- code: "CCR_UNAVAILABLE",
569
- message: "CCR integration is not available in this VCM runtime.",
570
- statusCode: 409,
571
- hint: "Enable and configure CCR GPT models before starting this session."
572
- });
579
+ if (isCcrSessionModel(model)) {
580
+ throw new VcmError({
581
+ code: "CCR_UNAVAILABLE",
582
+ message: "CCR integration is not available in this VCM runtime.",
583
+ statusCode: 409,
584
+ hint: "Enable and configure CCR GPT models before starting this session."
585
+ });
586
+ }
587
+ return {};
573
588
  }
574
589
  return deps.ccrIntegration.getLaunchEnvironment(model);
575
590
  }
@@ -1490,6 +1505,28 @@ function normalizeClaudeEffort(value) {
1490
1505
  }
1491
1506
  return "default";
1492
1507
  }
1508
+ function assertResumeProviderCompatible(session, requestedModel) {
1509
+ const persistedModel = normalizeClaudeModel(session.model);
1510
+ if (isCcrSessionModel(persistedModel) !== isCcrSessionModel(requestedModel)) {
1511
+ throw new VcmError({
1512
+ code: "SESSION_PROVIDER_SWITCH_REQUIRES_RESTART",
1513
+ message: `Cannot resume ${session.role} with a different model provider.`,
1514
+ statusCode: 409,
1515
+ hint: "Use Restart to switch between native Claude and CCR models."
1516
+ });
1517
+ }
1518
+ if (isCcrSessionModel(requestedModel) && !session.claudeConfigDir) {
1519
+ throw new VcmError({
1520
+ code: "CCR_SESSION_CONFIG_MISSING",
1521
+ message: `${session.role} was created before isolated CCR session storage was enabled.`,
1522
+ statusCode: 409,
1523
+ hint: "Restart this role once to create an isolated CCR session."
1524
+ });
1525
+ }
1526
+ }
1527
+ function readClaudeConfigDir(environment) {
1528
+ return environment.CLAUDE_CONFIG_DIR?.trim() || undefined;
1529
+ }
1493
1530
  function formatClaudeCdCommand(targetCwd) {
1494
1531
  // Claude Code's `/cd` slash command takes the literal remainder of the line as
1495
1532
  // the path, so the target must NOT be wrapped in quotes (quotes are taken as part
@@ -1500,13 +1537,40 @@ function formatClaudeCdCommand(targetCwd) {
1500
1537
  function isExitedStatus(status) {
1501
1538
  return status === "exited" || status === "crashed" || status === "missing";
1502
1539
  }
1503
- function withClaudeCodeRuntimeEnv(env, modelEnvironment = {}) {
1540
+ function withClaudeCodeRuntimeEnv(env, modelEnvironment = {}, telemetryEnvironment = {}) {
1504
1541
  return {
1505
1542
  ...env,
1506
1543
  ...modelEnvironment,
1544
+ ...telemetryEnvironment,
1507
1545
  CLAUDE_CODE_DISABLE_AUTO_MEMORY
1508
1546
  };
1509
1547
  }
1548
+ function buildUsageTelemetryEnvironment(apiUrl, role, model) {
1549
+ const disabled = {
1550
+ CLAUDE_CODE_ENABLE_TELEMETRY: undefined,
1551
+ OTEL_LOGS_EXPORTER: "none",
1552
+ OTEL_METRICS_EXPORTER: "none",
1553
+ OTEL_TRACES_EXPORTER: "none",
1554
+ OTEL_EXPORTER_OTLP_LOGS_PROTOCOL: undefined,
1555
+ OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: undefined,
1556
+ OTEL_RESOURCE_ATTRIBUTES: undefined,
1557
+ OTEL_LOG_USER_PROMPTS: "0",
1558
+ OTEL_LOG_ASSISTANT_RESPONSES: "0",
1559
+ OTEL_LOG_TOOL_DETAILS: "0",
1560
+ OTEL_LOG_RAW_API_BODIES: "0"
1561
+ };
1562
+ if (!apiUrl || isCcrSessionModel(model)) {
1563
+ return disabled;
1564
+ }
1565
+ return {
1566
+ ...disabled,
1567
+ CLAUDE_CODE_ENABLE_TELEMETRY: "1",
1568
+ OTEL_LOGS_EXPORTER: "otlp",
1569
+ OTEL_EXPORTER_OTLP_LOGS_PROTOCOL: "http/json",
1570
+ OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: `${apiUrl.replace(/\/+$/, "")}/api/telemetry/v1/logs`,
1571
+ OTEL_RESOURCE_ATTRIBUTES: `vcm.role=${role},vcm.launch_id=${randomUUID()}`
1572
+ };
1573
+ }
1510
1574
  function delay(ms) {
1511
1575
  if (ms <= 0) {
1512
1576
  return Promise.resolve();
@@ -5,6 +5,7 @@ export function createTaskCloseService(deps) {
5
5
  async closeTask(repoRoot, taskSlug) {
6
6
  const task = await deps.taskService.markTaskCleaned(repoRoot, taskSlug);
7
7
  const warnings = [];
8
+ deps.architectRestartService?.clear(repoRoot, taskSlug);
8
9
  await stopTaskRoleSessions(repoRoot, taskSlug, warnings);
9
10
  await bestEffort("Unable to stop task translation runtime", () => deps.translationService.stopTask(getTaskRuntimeRepoRoot(task), taskSlug, { clearCache: true }), warnings);
10
11
  await bestEffort("Unable to clear task round runtime", () => deps.roundService.stopTask(taskSlug), warnings);