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.
- package/README.md +30 -9
- package/dist/backend/adapters/claude-adapter.js +4 -1
- package/dist/backend/api/session-routes.js +5 -0
- package/dist/backend/api/usage-analytics-routes.js +31 -0
- package/dist/backend/cli/install-vcm-harness.js +51 -2
- package/dist/backend/runtime/session-registry.js +7 -0
- package/dist/backend/server.js +26 -9
- package/dist/backend/services/architect-restart-service.js +136 -0
- package/dist/backend/services/ccr-integration-service.js +70 -11
- package/dist/backend/services/claude-hook-service.js +6 -0
- package/dist/backend/services/claude-transcript-service.js +10 -10
- package/dist/backend/services/gate-review-service.js +40 -0
- package/dist/backend/services/harness-service.js +51 -4
- package/dist/backend/services/message-service.js +5 -0
- package/dist/backend/services/runtime-coordinator-service.js +37 -42
- package/dist/backend/services/session-service.js +83 -19
- package/dist/backend/services/task-close-service.js +1 -0
- package/dist/backend/services/usage-analytics-service.js +346 -0
- package/dist/backend/templates/harness/architect-agent.js +18 -10
- package/dist/backend/templates/harness/architect-scaffold-worker-agent.js +23 -0
- package/dist/backend/templates/harness/claude-root.js +1 -1
- package/dist/backend/templates/harness/gate-review.js +7 -2
- package/dist/backend/templates/harness/project-manager-agent.js +1 -1
- package/dist/backend/templates/harness/restart-architect-skill.js +75 -0
- package/dist/backend/templates/harness/vcm-architecture-interview-skill.js +31 -5
- package/dist/backend/templates/harness/vcm-route-message-skill.js +3 -3
- package/dist/shared/types/usage-analytics.js +1 -0
- package/dist-frontend/assets/index-CStWyouh.js +97 -0
- package/dist-frontend/assets/{index-CiEUp9Si.css → index-Ci7z8tW3.css} +1 -1
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/dist/backend/adapters/claude-settings-adapter.js +0 -79
- 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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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);
|