vibe-coding-master 0.7.14 → 0.7.16

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 CHANGED
@@ -235,14 +235,16 @@ a host-running [Claude Code Router](https://github.com/musistudio/claude-code-ro
235
235
  CCR must already be installed, authenticated, configured, and running on the
236
236
  host. VCM does not manage the CCR process.
237
237
 
238
- VCM uses this fixed Dev Container endpoint:
238
+ VCM automatically checks the local-host and DevContainer endpoints:
239
239
 
240
240
  ```text
241
+ http://127.0.0.1:3456
241
242
  http://host.docker.internal:3456
242
243
  ```
243
244
 
244
- Configure CCR to listen on port `3456` with an API key and make it reachable
245
- from the container. In the VCM `Settings` section:
245
+ Configure CCR to listen on port `3456` with an API key. When VCM runs in a
246
+ DevContainer, make the second endpoint reachable from the container. In the VCM
247
+ `Settings` section:
246
248
 
247
249
  1. enter and save the CCR API key;
248
250
  2. enable `CCR GPT models`;
@@ -250,13 +252,21 @@ from the container. In the VCM `Settings` section:
250
252
  4. select `GPT-5.6 Sol (CCR)` in any Session model control.
251
253
 
252
254
  The key is stored in global VCM state (`~/.vcm/settings.json`) with owner-only
253
- permissions, is used only to check CCR and discover models, and is never
254
- returned by the settings API. GPT sessions use the CCR-managed Claude Code
255
- `apiKeyHelper`; VCM removes inherited Anthropic token variables only from those
256
- child processes. Native Claude selections retain their existing launch and
257
- authentication behavior. If CCR is disabled, unreachable, rejects the key, or
258
- does not expose `Codex API/gpt-5.6-sol`, VCM blocks the new Start, Resume, or
259
- Restart and does not fall back to another model.
255
+ permissions and is never returned by the settings API. It is used for CCR
256
+ checks, model discovery, and the GPT-only `apiKeyHelper`. GPT sessions receive a
257
+ child-only `--settings` override and use the isolated Claude configuration root
258
+ `~/.vcm/claude/ccr`. VCM never edits `~/.claude/settings.json`. Native Claude
259
+ sessions keep their normal configuration and account authentication; VCM only
260
+ removes inherited environment variables that clearly point at the local CCR
261
+ gateway from the native child process. Configure CCR without enabling its
262
+ global Claude Code or Claude App takeover if those clients should remain on
263
+ Anthropic.
264
+
265
+ Resume keeps the provider recorded by the existing Session. Use Restart when
266
+ switching between a native Claude model and `GPT-5.6 Sol (CCR)`. If CCR is
267
+ disabled, unreachable, rejects the key, or does not expose
268
+ `Codex API/gpt-5.6-sol`, VCM blocks the new Start, Resume, or Restart and does
269
+ not fall back to another model.
260
270
 
261
271
  ## Launch Template
262
272
 
@@ -320,7 +330,9 @@ Conversation translation is controlled from the sidebar `Translation` section.
320
330
 
321
331
  VCM uses a task-scoped Translator role and Claude transcript JSONL files, not
322
332
  raw terminal text. Translation memory and completed file translations remain
323
- project-level durable data.
333
+ project-level durable data. When translation is enabled and the active task's
334
+ Harness is initialized, the backend automatically starts a fresh Translator for
335
+ the task or resumes its saved Session.
324
336
 
325
337
  Common controls:
326
338
 
@@ -424,9 +436,10 @@ Use it to:
424
436
  - merge task harness commits back to the connected repository branch when
425
437
  appropriate
426
438
 
427
- Harness Engineer is task-scoped and runs from the active task worktree. A new
428
- task receives its own Harness Engineer session. Durable memory is versioned with
429
- the project harness files.
439
+ Harness Engineer is task-scoped and runs from the active task worktree. The
440
+ backend automatically starts a fresh Harness Engineer for each active task or
441
+ resumes its saved Session. Durable memory is versioned with the project harness
442
+ files.
430
443
 
431
444
  ### Auto Memory
432
445
 
@@ -1,62 +1,84 @@
1
- import { CCR_GATEWAY_BASE_URL, CCR_GPT_MODEL_ID } from "../../shared/types/session.js";
1
+ import { CCR_GATEWAY_BASE_URLS, CCR_GPT_MODEL_ID } from "../../shared/types/session.js";
2
2
  const DEFAULT_TIMEOUT_MS = 3_000;
3
3
  const CCR_ENCODED_MODEL_PREFIX = "anthropic/claude-ccr-h";
4
4
  export function createCcrGatewayAdapter(deps = {}) {
5
- const baseUrl = (deps.baseUrl ?? CCR_GATEWAY_BASE_URL).replace(/\/+$/, "");
5
+ const baseUrls = (deps.baseUrl ? [deps.baseUrl] : CCR_GATEWAY_BASE_URLS)
6
+ .map((baseUrl) => baseUrl.replace(/\/+$/, ""));
6
7
  const fetchImpl = deps.fetch ?? globalThis.fetch;
7
8
  const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
8
9
  return {
9
10
  async probe(apiKey) {
10
- try {
11
- const root = await requestJson(fetchImpl, `${baseUrl}/`, timeoutMs);
12
- if (!root.response.ok) {
13
- return invalidResponse(`CCR gateway root returned HTTP ${root.response.status}.`);
11
+ const failures = [];
12
+ for (const baseUrl of baseUrls) {
13
+ const result = await probeEndpoint(fetchImpl, baseUrl, apiKey, timeoutMs);
14
+ if (result.baseUrl) {
15
+ return result;
14
16
  }
15
- if (!isCcrGateway(root.payload)) {
16
- return {
17
- connectionState: "not-ccr",
18
- modelAvailable: false,
19
- error: `The fixed CCR endpoint ${baseUrl} did not identify a Claude Code Router gateway.`
20
- };
21
- }
22
- const models = await requestJson(fetchImpl, `${baseUrl}/v1/models`, timeoutMs, {
23
- authorization: `Bearer ${apiKey}`,
24
- "user-agent": "Claude Code"
25
- });
26
- if (models.response.status === 401 || models.response.status === 403) {
27
- return {
28
- connectionState: "unauthorized",
29
- modelAvailable: false,
30
- error: "CCR rejected the configured API key."
31
- };
32
- }
33
- if (!models.response.ok) {
34
- return invalidResponse(`CCR model discovery returned HTTP ${models.response.status}.`);
35
- }
36
- const modelDescriptors = readModelDescriptors(models.payload);
37
- if (!modelDescriptors) {
38
- return invalidResponse("CCR returned an invalid /v1/models response.");
39
- }
40
- const modelAvailable = modelDescriptors.some(isRequiredModel);
41
- return {
42
- connectionState: "available",
43
- modelAvailable,
44
- ...(modelAvailable
45
- ? {}
46
- : { error: `CCR does not expose the required model ${CCR_GPT_MODEL_ID}.` })
47
- };
48
- }
49
- catch (error) {
50
- const timedOut = isAbortError(error);
51
- return {
52
- connectionState: "unreachable",
53
- modelAvailable: false,
54
- error: timedOut
55
- ? `CCR connection timed out after ${timeoutMs} ms at ${baseUrl}.`
56
- : `CCR could not be reached from the VCM container backend at ${baseUrl}. Reason: ${errorMessage(error)}`
57
- };
17
+ failures.push(result);
58
18
  }
19
+ return combineProbeFailures(baseUrls, failures);
20
+ }
21
+ };
22
+ }
23
+ async function probeEndpoint(fetchImpl, baseUrl, apiKey, timeoutMs) {
24
+ try {
25
+ const root = await requestJson(fetchImpl, `${baseUrl}/`, timeoutMs);
26
+ if (!root.response.ok) {
27
+ return invalidResponse(`Gateway root at ${baseUrl} returned HTTP ${root.response.status}.`);
28
+ }
29
+ if (!isCcrGateway(root.payload)) {
30
+ return {
31
+ connectionState: "not-ccr",
32
+ modelAvailable: false,
33
+ error: `${baseUrl} did not identify a Claude Code Router gateway.`
34
+ };
59
35
  }
36
+ const models = await requestJson(fetchImpl, `${baseUrl}/v1/models`, timeoutMs, {
37
+ authorization: `Bearer ${apiKey}`,
38
+ "user-agent": "Claude Code"
39
+ });
40
+ if (models.response.status === 401 || models.response.status === 403) {
41
+ return {
42
+ connectionState: "unauthorized",
43
+ modelAvailable: false,
44
+ baseUrl,
45
+ error: "CCR rejected the configured API key."
46
+ };
47
+ }
48
+ if (!models.response.ok) {
49
+ return invalidResponse(`CCR model discovery at ${baseUrl} returned HTTP ${models.response.status}.`, baseUrl);
50
+ }
51
+ const modelDescriptors = readModelDescriptors(models.payload);
52
+ if (!modelDescriptors) {
53
+ return invalidResponse("CCR returned an invalid /v1/models response.", baseUrl);
54
+ }
55
+ const modelAvailable = modelDescriptors.some(isRequiredModel);
56
+ return {
57
+ connectionState: "available",
58
+ modelAvailable,
59
+ baseUrl,
60
+ ...(modelAvailable
61
+ ? {}
62
+ : { error: `CCR does not expose the required model ${CCR_GPT_MODEL_ID}.` })
63
+ };
64
+ }
65
+ catch (error) {
66
+ const timedOut = isAbortError(error);
67
+ return {
68
+ connectionState: "unreachable",
69
+ modelAvailable: false,
70
+ error: timedOut
71
+ ? `CCR connection timed out after ${timeoutMs} ms at ${baseUrl}.`
72
+ : `CCR could not be reached from the VCM backend at ${baseUrl}. Reason: ${errorMessage(error)}`
73
+ };
74
+ }
75
+ }
76
+ function combineProbeFailures(baseUrls, failures) {
77
+ const lastFailure = failures.at(-1);
78
+ return {
79
+ connectionState: lastFailure?.connectionState ?? "unreachable",
80
+ modelAvailable: false,
81
+ error: `CCR was not found at ${baseUrls.join(" or ")}. ${failures.map((failure) => failure.error).filter(Boolean).join(" ")}`.trim()
60
82
  };
61
83
  }
62
84
  async function requestJson(fetchImpl, url, timeoutMs, headers = {}) {
@@ -131,10 +153,11 @@ function decodeCcrModelId(modelId) {
131
153
  function normalizeModelName(value) {
132
154
  return (value ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "");
133
155
  }
134
- function invalidResponse(error) {
156
+ function invalidResponse(error, baseUrl) {
135
157
  return {
136
158
  connectionState: "invalid-response",
137
159
  modelAvailable: false,
160
+ ...(baseUrl ? { baseUrl } : {}),
138
161
  error
139
162
  };
140
163
  }
@@ -18,8 +18,9 @@ export function createClaudeAdapter(runner) {
18
18
  }
19
19
  return result.stdout.trim();
20
20
  },
21
- buildRoleStartCommand(role, command = "claude", permissionMode = "default", claudeSessionId, resume = false, model = "default", effort = "default") {
21
+ buildRoleStartCommand(role, command = "claude", permissionMode = "default", claudeSessionId, resume = false, model = "default", effort = "default", settingsOverride) {
22
22
  const args = ["--agent", role];
23
+ const sessionSettings = { ...settingsOverride };
23
24
  if (claudeSessionId) {
24
25
  args.push(resume ? "--resume" : "--session-id", claudeSessionId);
25
26
  }
@@ -27,11 +28,14 @@ export function createClaudeAdapter(runner) {
27
28
  args.push("--model", model);
28
29
  }
29
30
  if (effort === "ultracode") {
30
- args.push("--settings", JSON.stringify({ ultracode: true }));
31
+ sessionSettings.ultracode = true;
31
32
  }
32
33
  else if (effort !== "default") {
33
34
  args.push("--effort", effort);
34
35
  }
36
+ if (Object.keys(sessionSettings).length > 0) {
37
+ args.push("--settings", JSON.stringify(sessionSettings));
38
+ }
35
39
  if (permissionMode !== "default") {
36
40
  args.push("--permission-mode", permissionMode);
37
41
  }
@@ -2,6 +2,13 @@ export function createSessionRegistry() {
2
2
  const sessions = new Map();
3
3
  return {
4
4
  upsert(session) {
5
+ for (const [sessionId, candidate] of sessions) {
6
+ if (sessionId !== session.id
7
+ && candidate.taskSlug === session.taskSlug
8
+ && candidate.role === session.role) {
9
+ sessions.delete(sessionId);
10
+ }
11
+ }
5
12
  sessions.set(session.id, session);
6
13
  },
7
14
  get(sessionId) {
@@ -1,9 +1,15 @@
1
1
  import { CCR_GATEWAY_BASE_URL, CCR_GPT_MODEL_ID, createSessionModelOptions, isCcrSessionModel } from "../../shared/types/session.js";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
2
4
  import { VcmError } from "../errors.js";
5
+ import { resolveVcmDataDir } from "../vcm-data-dir.js";
3
6
  const DEFAULT_CACHE_TTL_MS = 10_000;
7
+ const DEFAULT_API_KEY_HELPER_PATH = fileURLToPath(new URL("../../../scripts/ccr-api-key-helper.mjs", import.meta.url));
4
8
  export function createCcrIntegrationService(deps) {
5
9
  const now = deps.now ?? (() => new Date());
6
10
  const cacheTtlMs = deps.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
11
+ const baseEnv = deps.baseEnv ?? process.env;
12
+ const configDir = deps.configDir ?? path.join(resolveVcmDataDir(baseEnv), "claude", "ccr");
7
13
  let cachedProbe;
8
14
  let inFlight;
9
15
  async function probe(force = false) {
@@ -122,7 +128,7 @@ export function createCcrIntegrationService(deps) {
122
128
  },
123
129
  async getLaunchEnvironment(model) {
124
130
  if (!isCcrSessionModel(model)) {
125
- return {};
131
+ return buildNativeLaunchEnvironment(baseEnv);
126
132
  }
127
133
  const settings = await deps.settings.getCcrIntegrationSettings();
128
134
  if (!settings.enabled) {
@@ -142,11 +148,13 @@ export function createCcrIntegrationService(deps) {
142
148
  hint: "Check that host CCR is running on port 3456 and exposes GPT-5.6 Sol."
143
149
  });
144
150
  }
145
- const noProxy = mergeNoProxy(deps.baseEnv?.NO_PROXY ?? deps.baseEnv?.no_proxy ?? process.env.NO_PROXY ?? process.env.no_proxy, "host.docker.internal");
151
+ const gatewayBaseUrl = checked.result.baseUrl ?? CCR_GATEWAY_BASE_URL;
152
+ const gatewayHost = new URL(gatewayBaseUrl).hostname;
153
+ const noProxy = mergeNoProxy(deps.baseEnv?.NO_PROXY ?? deps.baseEnv?.no_proxy ?? process.env.NO_PROXY ?? process.env.no_proxy, gatewayHost);
146
154
  return {
147
- ANTHROPIC_BASE_URL: CCR_GATEWAY_BASE_URL,
148
- ANTHROPIC_API_BASE_URL: CCR_GATEWAY_BASE_URL,
149
- CLAUDE_AGENT_API_BASE_URL: CCR_GATEWAY_BASE_URL,
155
+ ANTHROPIC_BASE_URL: gatewayBaseUrl,
156
+ ANTHROPIC_API_BASE_URL: gatewayBaseUrl,
157
+ CLAUDE_AGENT_API_BASE_URL: gatewayBaseUrl,
150
158
  ANTHROPIC_AUTH_TOKEN: undefined,
151
159
  ANTHROPIC_API_KEY: undefined,
152
160
  ANTHROPIC_MODEL: CCR_GPT_MODEL_ID,
@@ -154,12 +162,85 @@ export function createCcrIntegrationService(deps) {
154
162
  CODEXL_CLAUDE_CODE_MODEL: CCR_GPT_MODEL_ID,
155
163
  ANTHROPIC_SMALL_FAST_MODEL: CCR_GPT_MODEL_ID,
156
164
  CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1",
165
+ CLAUDE_CONFIG_DIR: configDir,
157
166
  NO_PROXY: noProxy,
158
167
  no_proxy: noProxy
159
168
  };
169
+ },
170
+ async getLaunchSettingsOverride(model) {
171
+ if (!isCcrSessionModel(model)) {
172
+ return undefined;
173
+ }
174
+ return {
175
+ apiKeyHelper: buildApiKeyHelperCommand(deps.apiKeyHelperPath ?? DEFAULT_API_KEY_HELPER_PATH)
176
+ };
160
177
  }
161
178
  };
162
179
  }
180
+ const CCR_BASE_URL_KEYS = [
181
+ "ANTHROPIC_BASE_URL",
182
+ "ANTHROPIC_API_BASE_URL",
183
+ "CLAUDE_AGENT_API_BASE_URL"
184
+ ];
185
+ const CCR_MODEL_KEYS = [
186
+ "ANTHROPIC_MODEL",
187
+ "ANTHROPIC_SMALL_FAST_MODEL"
188
+ ];
189
+ const CCR_ONLY_ENV_KEYS = [
190
+ "CCR_CLAUDE_CODE_MODEL",
191
+ "CODEXL_CLAUDE_CODE_MODEL",
192
+ "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"
193
+ ];
194
+ export function buildNativeLaunchEnvironment(baseEnv) {
195
+ const environment = {};
196
+ let inheritedCcrTakeover = false;
197
+ if (baseEnv.CLAUDE_CONFIG_DIR?.trim()) {
198
+ environment.CLAUDE_CONFIG_DIR = baseEnv.CLAUDE_CONFIG_DIR.trim();
199
+ }
200
+ for (const key of CCR_BASE_URL_KEYS) {
201
+ if (isCcrGatewayUrl(baseEnv[key])) {
202
+ environment[key] = undefined;
203
+ inheritedCcrTakeover = true;
204
+ }
205
+ }
206
+ for (const key of CCR_MODEL_KEYS) {
207
+ if (isCcrModelName(baseEnv[key])) {
208
+ environment[key] = undefined;
209
+ inheritedCcrTakeover = true;
210
+ }
211
+ }
212
+ for (const key of CCR_ONLY_ENV_KEYS) {
213
+ if (baseEnv[key] !== undefined) {
214
+ environment[key] = undefined;
215
+ inheritedCcrTakeover = true;
216
+ }
217
+ }
218
+ if (inheritedCcrTakeover) {
219
+ environment.ANTHROPIC_AUTH_TOKEN = undefined;
220
+ environment.ANTHROPIC_API_KEY = undefined;
221
+ }
222
+ return environment;
223
+ }
224
+ function isCcrGatewayUrl(value) {
225
+ if (!value) {
226
+ return false;
227
+ }
228
+ try {
229
+ const url = new URL(value);
230
+ return url.port === "3456"
231
+ && ["127.0.0.1", "localhost", "host.docker.internal"].includes(url.hostname);
232
+ }
233
+ catch {
234
+ return false;
235
+ }
236
+ }
237
+ function isCcrModelName(value) {
238
+ if (!value) {
239
+ return false;
240
+ }
241
+ return value === CCR_GPT_MODEL_ID
242
+ || value.startsWith("anthropic/claude-ccr-h");
243
+ }
163
244
  function createStatus(input) {
164
245
  const reason = input.error
165
246
  ?? (input.enabled ? "CCR GPT-5.6 Sol is unavailable." : "Enable CCR GPT models in Settings.");
@@ -193,3 +274,12 @@ export function mergeNoProxy(current, host) {
193
274
  }
194
275
  return values.join(",");
195
276
  }
277
+ export function buildApiKeyHelperCommand(helperPath) {
278
+ if (process.platform === "win32") {
279
+ return `${JSON.stringify(process.execPath)} ${JSON.stringify(helperPath)}`;
280
+ }
281
+ return `${quotePosixShell(process.execPath)} ${quotePosixShell(helperPath)}`;
282
+ }
283
+ function quotePosixShell(value) {
284
+ return `'${value.replaceAll("'", `'\\''`)}'`;
285
+ }
@@ -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;
@@ -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);
@@ -55,7 +55,13 @@ export function createSessionService(deps) {
55
55
  const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
56
56
  const model = normalizeClaudeModel(input.model ?? persisted?.model);
57
57
  const effort = normalizeClaudeEffort(input.effort ?? persisted?.effort);
58
- const modelEnvironment = await getModelLaunchEnvironment(model);
58
+ if (launchMode === "resume" && persisted) {
59
+ assertResumeProviderCompatible(persisted, model);
60
+ }
61
+ const [modelEnvironment, modelSettingsOverride] = await Promise.all([
62
+ getModelLaunchEnvironment(model),
63
+ getModelLaunchSettingsOverride(model)
64
+ ]);
59
65
  const resumeClaudeSessionId = launchMode === "resume"
60
66
  ? persisted?.claudeSessionId
61
67
  : undefined;
@@ -71,10 +77,10 @@ export function createSessionService(deps) {
71
77
  const transcriptPath = launchMode === "resume" && persisted?.transcriptPath
72
78
  ? persisted.transcriptPath
73
79
  : resumeClaudeSessionId
74
- ? claudeTranscriptPath(taskRepoRoot, resumeClaudeSessionId)
80
+ ? claudeTranscriptPath(taskRepoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
75
81
  : undefined;
76
82
  const startCommand = {
77
- ...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort),
83
+ ...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride),
78
84
  cwd: taskRepoRoot
79
85
  };
80
86
  const runtimeSession = await deps.runtime.createSession({
@@ -110,6 +116,7 @@ export function createSessionService(deps) {
110
116
  model,
111
117
  effort,
112
118
  cwd: startCommand.cwd,
119
+ claudeConfigDir: readClaudeConfigDir(modelEnvironment),
113
120
  terminalBackend: "node-pty",
114
121
  pid: runtimeSession.pid,
115
122
  roleCommandPath: isDispatchableRole(role)
@@ -160,7 +167,13 @@ export function createSessionService(deps) {
160
167
  const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
161
168
  const model = normalizeClaudeModel(input.model ?? persisted?.model);
162
169
  const effort = normalizeClaudeEffort(input.effort ?? persisted?.effort ?? "medium");
163
- const modelEnvironment = await getModelLaunchEnvironment(model);
170
+ if (launchMode === "resume" && persisted) {
171
+ assertResumeProviderCompatible(persisted, model);
172
+ }
173
+ const [modelEnvironment, modelSettingsOverride] = await Promise.all([
174
+ getModelLaunchEnvironment(model),
175
+ getModelLaunchSettingsOverride(model)
176
+ ]);
164
177
  const resumeClaudeSessionId = launchMode === "resume"
165
178
  ? persisted?.claudeSessionId
166
179
  : undefined;
@@ -188,10 +201,10 @@ export function createSessionService(deps) {
188
201
  const sessionCwd = launchMode === "resume" ? persisted?.cwd ?? launchCwd : launchCwd;
189
202
  const claudeSessionId = resumeClaudeSessionId ?? "";
190
203
  const transcriptPath = resumeClaudeSessionId
191
- ? claudeTranscriptPath(repoRoot, resumeClaudeSessionId)
204
+ ? claudeTranscriptPath(repoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
192
205
  : undefined;
193
206
  const startCommand = {
194
- ...deps.claude.buildRoleStartCommand(TRANSLATOR_ROLE, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort),
207
+ ...deps.claude.buildRoleStartCommand(TRANSLATOR_ROLE, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride),
195
208
  cwd: launchCwd
196
209
  };
197
210
  const runtimeSession = await deps.runtime.createSession({
@@ -230,6 +243,7 @@ export function createSessionService(deps) {
230
243
  model,
231
244
  effort,
232
245
  cwd: sessionCwd,
246
+ claudeConfigDir: readClaudeConfigDir(modelEnvironment),
233
247
  terminalBackend: "node-pty",
234
248
  pid: runtimeSession.pid,
235
249
  startedAt: runtimeSession.startedAt,
@@ -270,7 +284,13 @@ export function createSessionService(deps) {
270
284
  const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
271
285
  const model = normalizeClaudeModel(input.model ?? persisted?.model);
272
286
  const effort = normalizeClaudeEffort(input.effort ?? persisted?.effort ?? "medium");
273
- const modelEnvironment = await getModelLaunchEnvironment(model);
287
+ if (launchMode === "resume" && persisted) {
288
+ assertResumeProviderCompatible(persisted, model);
289
+ }
290
+ const [modelEnvironment, modelSettingsOverride] = await Promise.all([
291
+ getModelLaunchEnvironment(model),
292
+ getModelLaunchSettingsOverride(model)
293
+ ]);
274
294
  const resumeClaudeSessionId = launchMode === "resume"
275
295
  ? persisted?.claudeSessionId
276
296
  : undefined;
@@ -294,10 +314,10 @@ export function createSessionService(deps) {
294
314
  const sessionCwd = launchMode === "resume" ? persisted?.cwd ?? launchCwd : launchCwd;
295
315
  const claudeSessionId = resumeClaudeSessionId ?? "";
296
316
  const transcriptPath = resumeClaudeSessionId
297
- ? claudeTranscriptPath(repoRoot, resumeClaudeSessionId)
317
+ ? claudeTranscriptPath(repoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
298
318
  : undefined;
299
319
  const startCommand = {
300
- ...deps.claude.buildRoleStartCommand(HARNESS_ENGINEER_ROLE, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort),
320
+ ...deps.claude.buildRoleStartCommand(HARNESS_ENGINEER_ROLE, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride),
301
321
  cwd: launchCwd
302
322
  };
303
323
  const runtimeSession = await deps.runtime.createSession({
@@ -336,6 +356,7 @@ export function createSessionService(deps) {
336
356
  model,
337
357
  effort,
338
358
  cwd: sessionCwd,
359
+ claudeConfigDir: readClaudeConfigDir(modelEnvironment),
339
360
  terminalBackend: "node-pty",
340
361
  pid: runtimeSession.pid,
341
362
  startedAt: runtimeSession.startedAt,
@@ -480,14 +501,18 @@ export function createSessionService(deps) {
480
501
  const permissionMode = normalizeClaudePermissionMode(session.permissionMode);
481
502
  const model = normalizeClaudeModel(session.model);
482
503
  const effort = normalizeClaudeEffort(session.effort);
483
- const modelEnvironment = await getModelLaunchEnvironment(model);
504
+ assertResumeProviderCompatible(session, model);
505
+ const [modelEnvironment, modelSettingsOverride] = await Promise.all([
506
+ getModelLaunchEnvironment(model),
507
+ getModelLaunchSettingsOverride(model)
508
+ ]);
484
509
  // Spawn (`claude --resume`) always anchors at the base repoRoot so resume works
485
510
  // even if the persisted task cwd was deleted. `--resume` then restores the
486
511
  // session's own last cwd (tracked on `session.cwd`), so the `/cd` migrate below
487
512
  // fires only when that restored cwd differs from the target worktree.
488
513
  const launchCwd = repoRoot;
489
514
  const startCommand = {
490
- ...deps.claude.buildRoleStartCommand(session.role, config.claudeCommand, permissionMode, session.claudeSessionId, true, model, effort),
515
+ ...deps.claude.buildRoleStartCommand(session.role, config.claudeCommand, permissionMode, session.claudeSessionId, true, model, effort, modelSettingsOverride),
491
516
  cwd: launchCwd
492
517
  };
493
518
  const runtimeSession = await deps.runtime.createSession({
@@ -534,13 +559,14 @@ export function createSessionService(deps) {
534
559
  permissionMode,
535
560
  model,
536
561
  effort,
562
+ claudeConfigDir: readClaudeConfigDir(modelEnvironment),
537
563
  pid: runtimeSession.pid,
538
564
  startedAt: runtimeSession.startedAt,
539
565
  updatedAt: timestamp,
540
566
  lastOutputAt: runtimeSession.lastOutputAt,
541
567
  exitCode: runtimeSession.exitCode,
542
568
  transcriptPath: session.claudeSessionId
543
- ? claudeTranscriptPath(repoRoot, session.claudeSessionId)
569
+ ? claudeTranscriptPath(repoRoot, session.claudeSessionId, session.claudeConfigDir)
544
570
  : session.transcriptPath
545
571
  };
546
572
  deps.registry.upsert(normalizeProjectScopedRecordForPersistence(resumed));
@@ -548,19 +574,22 @@ export function createSessionService(deps) {
548
574
  return migrateRunningProjectToolSessionCwd(repoRoot, resumed, targetCwd);
549
575
  }
550
576
  async function getModelLaunchEnvironment(model) {
551
- if (!isCcrSessionModel(model)) {
552
- return {};
553
- }
554
577
  if (!deps.ccrIntegration) {
555
- throw new VcmError({
556
- code: "CCR_UNAVAILABLE",
557
- message: "CCR integration is not available in this VCM runtime.",
558
- statusCode: 409,
559
- hint: "Enable and configure CCR GPT models before starting this session."
560
- });
578
+ if (isCcrSessionModel(model)) {
579
+ throw new VcmError({
580
+ code: "CCR_UNAVAILABLE",
581
+ message: "CCR integration is not available in this VCM runtime.",
582
+ statusCode: 409,
583
+ hint: "Enable and configure CCR GPT models before starting this session."
584
+ });
585
+ }
586
+ return {};
561
587
  }
562
588
  return deps.ccrIntegration.getLaunchEnvironment(model);
563
589
  }
590
+ async function getModelLaunchSettingsOverride(model) {
591
+ return deps.ccrIntegration?.getLaunchSettingsOverride(model);
592
+ }
564
593
  function isRuntimeSessionAlive(session) {
565
594
  if (!session || isExitedStatus(session.status) || session.pid === undefined) {
566
595
  return false;
@@ -1475,6 +1504,28 @@ function normalizeClaudeEffort(value) {
1475
1504
  }
1476
1505
  return "default";
1477
1506
  }
1507
+ function assertResumeProviderCompatible(session, requestedModel) {
1508
+ const persistedModel = normalizeClaudeModel(session.model);
1509
+ if (isCcrSessionModel(persistedModel) !== isCcrSessionModel(requestedModel)) {
1510
+ throw new VcmError({
1511
+ code: "SESSION_PROVIDER_SWITCH_REQUIRES_RESTART",
1512
+ message: `Cannot resume ${session.role} with a different model provider.`,
1513
+ statusCode: 409,
1514
+ hint: "Use Restart to switch between native Claude and CCR models."
1515
+ });
1516
+ }
1517
+ if (isCcrSessionModel(requestedModel) && !session.claudeConfigDir) {
1518
+ throw new VcmError({
1519
+ code: "CCR_SESSION_CONFIG_MISSING",
1520
+ message: `${session.role} was created before isolated CCR session storage was enabled.`,
1521
+ statusCode: 409,
1522
+ hint: "Restart this role once to create an isolated CCR session."
1523
+ });
1524
+ }
1525
+ }
1526
+ function readClaudeConfigDir(environment) {
1527
+ return environment.CLAUDE_CONFIG_DIR?.trim() || undefined;
1528
+ }
1478
1529
  function formatClaudeCdCommand(targetCwd) {
1479
1530
  // Claude Code's `/cd` slash command takes the literal remainder of the line as
1480
1531
  // the path, so the target must NOT be wrapped in quotes (quotes are taken as part
@@ -45,7 +45,13 @@ export const CLAUDE_MODEL_OPTIONS = [
45
45
  available: true
46
46
  }
47
47
  ];
48
- export const CCR_GATEWAY_BASE_URL = "http://host.docker.internal:3456";
48
+ export const CCR_LOCAL_GATEWAY_BASE_URL = "http://127.0.0.1:3456";
49
+ export const CCR_CONTAINER_GATEWAY_BASE_URL = "http://host.docker.internal:3456";
50
+ export const CCR_GATEWAY_BASE_URL = CCR_CONTAINER_GATEWAY_BASE_URL;
51
+ export const CCR_GATEWAY_BASE_URLS = [
52
+ CCR_LOCAL_GATEWAY_BASE_URL,
53
+ CCR_CONTAINER_GATEWAY_BASE_URL
54
+ ];
49
55
  export const CCR_GPT_MODEL_ID = "Codex API/gpt-5.6-sol";
50
56
  export const CCR_GPT_SESSION_MODEL = `ccr:${CCR_GPT_MODEL_ID}`;
51
57
  export function isCcrSessionModel(model) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-coding-master",
3
- "version": "0.7.14",
3
+ "version": "0.7.16",
4
4
  "description": "Local GUI session cockpit for Claude Code role sessions.",
5
5
  "type": "module",
6
6
  "files": [
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import path from "node:path";
6
+
7
+ const dataDir = process.env.VCM_DATA_DIR?.trim()
8
+ ? path.resolve(process.env.VCM_DATA_DIR)
9
+ : path.join(homedir(), ".vcm");
10
+
11
+ try {
12
+ const settings = JSON.parse(await readFile(path.join(dataDir, "settings.json"), "utf8"));
13
+ const apiKey = settings?.ccr?.apiKey;
14
+ if (typeof apiKey !== "string" || !apiKey.trim()) {
15
+ process.exitCode = 1;
16
+ } else {
17
+ process.stdout.write(`${apiKey.trim()}\n`);
18
+ }
19
+ } catch {
20
+ process.exitCode = 1;
21
+ }