vibe-coding-master 0.3.23 → 0.3.25
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/dist/backend/adapters/filesystem.js +16 -1
- package/dist/backend/adapters/git-adapter.js +53 -0
- package/dist/backend/api/artifact-routes.js +1 -28
- package/dist/backend/api/codex-translation-routes.js +3 -1
- package/dist/backend/api/harness-routes.js +10 -0
- package/dist/backend/api/task-routes.js +2 -6
- package/dist/backend/gateway/gateway-service.js +3 -16
- package/dist/backend/server.js +3 -1
- package/dist/backend/services/artifact-service.js +1 -30
- package/dist/backend/services/codex-translation-service.js +23 -2
- package/dist/backend/services/harness-service.js +106 -0
- package/dist/backend/services/session-service.js +1 -4
- package/dist/backend/services/status-service.js +1 -6
- package/dist/backend/services/task-service.js +42 -110
- package/dist/backend/templates/harness/project-manager-agent.js +10 -1
- package/dist/backend/templates/harness/vcm-route-message-skill.js +8 -2
- package/dist-frontend/assets/{index-C2xTNLmb.js → index-CKWy15WL.js} +46 -47
- package/dist-frontend/index.html +1 -1
- package/docs/cc-best-practices.md +1 -1
- package/docs/full-harness-baseline.md +0 -1
- package/docs/gateway-design.md +9 -13
- package/docs/product-design.md +18 -39
- package/docs/vcm-cc-best-practices.md +0 -1
- package/package.json +1 -1
|
@@ -54,7 +54,21 @@ export function createNodeFileSystemAdapter() {
|
|
|
54
54
|
});
|
|
55
55
|
},
|
|
56
56
|
async readJson(targetPath) {
|
|
57
|
-
|
|
57
|
+
let attempt = 0;
|
|
58
|
+
for (;;) {
|
|
59
|
+
const content = await runFileOperation(() => fs.readFile(targetPath, "utf8"));
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(content);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
const delayMs = JSON_READ_RETRY_DELAYS_MS[attempt];
|
|
65
|
+
if (!(error instanceof SyntaxError) || delayMs === undefined) {
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
attempt += 1;
|
|
69
|
+
await delay(delayMs);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
58
72
|
},
|
|
59
73
|
async writeJson(targetPath, value) {
|
|
60
74
|
await this.writeText(targetPath, `${JSON.stringify(value, null, 2)}\n`);
|
|
@@ -84,6 +98,7 @@ export function createNodeFileSystemAdapter() {
|
|
|
84
98
|
}
|
|
85
99
|
const DEFAULT_FILE_OPERATION_CONCURRENCY = 8;
|
|
86
100
|
const OPEN_FILE_RETRY_DELAYS_MS = [25, 50, 100, 200, 400, 800];
|
|
101
|
+
const JSON_READ_RETRY_DELAYS_MS = [25, 50, 100, 200];
|
|
87
102
|
function createFileOperationRunner(maxConcurrent = DEFAULT_FILE_OPERATION_CONCURRENCY) {
|
|
88
103
|
let active = 0;
|
|
89
104
|
const waiters = [];
|
|
@@ -81,6 +81,18 @@ export function createGitAdapter(runner) {
|
|
|
81
81
|
}
|
|
82
82
|
return result.stdout;
|
|
83
83
|
},
|
|
84
|
+
async getStagedStatus(repoRoot) {
|
|
85
|
+
const result = await runGit(runner, repoRoot, ["diff", "--cached", "--name-status"]);
|
|
86
|
+
if (result.exitCode !== 0) {
|
|
87
|
+
throw new VcmError({
|
|
88
|
+
code: "GIT_ERROR",
|
|
89
|
+
message: "Unable to read staged Git changes.",
|
|
90
|
+
statusCode: 400,
|
|
91
|
+
hint: result.stderr
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return result.stdout;
|
|
95
|
+
},
|
|
84
96
|
async isIgnored(repoRoot, repoRelativePath) {
|
|
85
97
|
const result = await runGit(runner, repoRoot, ["check-ignore", "-q", "--", repoRelativePath]);
|
|
86
98
|
if (result.exitCode === 0) {
|
|
@@ -111,6 +123,47 @@ export function createGitAdapter(runner) {
|
|
|
111
123
|
hint: result.stderr
|
|
112
124
|
});
|
|
113
125
|
},
|
|
126
|
+
async addPaths(repoRoot, paths) {
|
|
127
|
+
if (paths.length === 0) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const result = await runGit(runner, repoRoot, ["add", "--", ...paths]);
|
|
131
|
+
if (result.exitCode !== 0) {
|
|
132
|
+
throw new VcmError({
|
|
133
|
+
code: "GIT_ADD_FAILED",
|
|
134
|
+
message: "Unable to stage harness changes.",
|
|
135
|
+
statusCode: 409,
|
|
136
|
+
hint: result.stderr || result.stdout
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
async commit(repoRoot, message) {
|
|
141
|
+
const result = await runGit(runner, repoRoot, ["commit", "-m", message]);
|
|
142
|
+
if (result.exitCode !== 0) {
|
|
143
|
+
throw new VcmError({
|
|
144
|
+
code: "GIT_COMMIT_FAILED",
|
|
145
|
+
message: "Unable to commit harness changes.",
|
|
146
|
+
statusCode: 409,
|
|
147
|
+
hint: result.stderr || result.stdout
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return this.getHeadCommit(repoRoot);
|
|
151
|
+
},
|
|
152
|
+
async rebase(repoRoot, upstream) {
|
|
153
|
+
const result = await runGit(runner, repoRoot, ["rebase", upstream]);
|
|
154
|
+
if (result.exitCode !== 0) {
|
|
155
|
+
throw new VcmError({
|
|
156
|
+
code: "GIT_REBASE_FAILED",
|
|
157
|
+
message: "Unable to rebase task branch onto the harness commit.",
|
|
158
|
+
statusCode: 409,
|
|
159
|
+
hint: result.stderr || result.stdout
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
stdout: result.stdout,
|
|
164
|
+
stderr: result.stderr
|
|
165
|
+
};
|
|
166
|
+
},
|
|
114
167
|
async createWorktree(input) {
|
|
115
168
|
const result = await runGit(runner, input.repoRoot, [
|
|
116
169
|
"worktree",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isDispatchableRole
|
|
1
|
+
import { isDispatchableRole } from "../../shared/constants.js";
|
|
2
2
|
import { VcmError } from "../errors.js";
|
|
3
3
|
import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
|
|
4
4
|
export function registerArtifactRoutes(app, deps) {
|
|
@@ -52,33 +52,6 @@ export function registerArtifactRoutes(app, deps) {
|
|
|
52
52
|
});
|
|
53
53
|
return { ok: true };
|
|
54
54
|
});
|
|
55
|
-
app.get("/api/tasks/:taskSlug/logs/:role", async (request) => {
|
|
56
|
-
const project = await requireCurrentProject(deps.projectService);
|
|
57
|
-
if (!isRoleName(request.params.role)) {
|
|
58
|
-
throw new VcmError({
|
|
59
|
-
code: "UNKNOWN_ROLE",
|
|
60
|
-
message: `Unknown role: ${request.params.role}`,
|
|
61
|
-
statusCode: 400
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
65
|
-
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
66
|
-
const paths = deps.artifactService.getHandoffPaths(taskRepoRoot, task.handoffDir);
|
|
67
|
-
const artifactPath = paths.roleLogPaths[request.params.role];
|
|
68
|
-
if (!artifactPath) {
|
|
69
|
-
return {
|
|
70
|
-
role: request.params.role,
|
|
71
|
-
content: ""
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
return {
|
|
75
|
-
role: request.params.role,
|
|
76
|
-
content: await deps.artifactService.readArtifact({
|
|
77
|
-
repoRoot: taskRepoRoot,
|
|
78
|
-
artifactPath
|
|
79
|
-
})
|
|
80
|
-
};
|
|
81
|
-
});
|
|
82
55
|
}
|
|
83
56
|
function parseDispatchableRole(role) {
|
|
84
57
|
if (!isDispatchableRole(role)) {
|
|
@@ -2,7 +2,9 @@ import { VcmError } from "../errors.js";
|
|
|
2
2
|
export function registerCodexTranslationRoutes(app, deps) {
|
|
3
3
|
app.get("/api/translation/codex/state", async () => {
|
|
4
4
|
const project = await requireCurrentProject(deps.projectService);
|
|
5
|
-
return deps.codexTranslationService.getState(project.repoRoot
|
|
5
|
+
return deps.codexTranslationService.getState(project.repoRoot, {
|
|
6
|
+
visibility: "public"
|
|
7
|
+
});
|
|
6
8
|
});
|
|
7
9
|
app.get("/api/translation/codex/session", async () => {
|
|
8
10
|
const project = await requireCurrentProject(deps.projectService);
|
|
@@ -16,6 +16,16 @@ export function registerHarnessRoutes(app, deps) {
|
|
|
16
16
|
const project = await requireCurrentProject(deps.projectService);
|
|
17
17
|
return deps.harnessService.applyHarness(project.repoRoot);
|
|
18
18
|
});
|
|
19
|
+
app.post("/api/projects/harness/tasks/:taskSlug/commit-and-rebase", async (request) => {
|
|
20
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
21
|
+
const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
22
|
+
return deps.harnessService.commitAndRebaseTask(project.repoRoot, {
|
|
23
|
+
taskSlug: task.taskSlug,
|
|
24
|
+
branch: task.branch,
|
|
25
|
+
worktreePath: task.worktreePath,
|
|
26
|
+
changedFiles: request.body?.changedFiles ?? []
|
|
27
|
+
});
|
|
28
|
+
});
|
|
19
29
|
app.get("/api/projects/harness/bootstrap", async () => {
|
|
20
30
|
const project = await requireCurrentProject(deps.projectService);
|
|
21
31
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DISPATCHABLE_ROLES
|
|
1
|
+
import { DISPATCHABLE_ROLES } from "../../shared/constants.js";
|
|
2
2
|
import { isOpenFileLimitError, VcmError } from "../errors.js";
|
|
3
3
|
import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
|
|
4
4
|
export function registerTaskRoutes(app, deps) {
|
|
@@ -66,6 +66,7 @@ function degradedTaskStatus(repoRoot, taskSlug, error) {
|
|
|
66
66
|
createdAt: timestamp,
|
|
67
67
|
updatedAt: timestamp,
|
|
68
68
|
repoRoot,
|
|
69
|
+
worktreePath: `${repoRoot}/.claude/worktrees/${taskSlug}`,
|
|
69
70
|
branch: "unknown",
|
|
70
71
|
handoffDir,
|
|
71
72
|
status: "stopped"
|
|
@@ -79,18 +80,13 @@ function degradedTaskStatus(repoRoot, taskSlug, error) {
|
|
|
79
80
|
}
|
|
80
81
|
function degradedArtifactSummary(handoffDir) {
|
|
81
82
|
const roleCommandsDir = `${handoffDir}/role-commands`;
|
|
82
|
-
const logsDir = `${handoffDir}/logs`;
|
|
83
83
|
const messagesDir = `${handoffDir}/messages`;
|
|
84
84
|
return {
|
|
85
85
|
paths: {
|
|
86
86
|
handoffDir,
|
|
87
87
|
roleCommandsDir,
|
|
88
|
-
logsDir,
|
|
89
88
|
messagesDir,
|
|
90
89
|
roleCommandPaths: Object.fromEntries(DISPATCHABLE_ROLES.map((role) => [role, `${roleCommandsDir}/${role}.md`])),
|
|
91
|
-
roleLogPaths: Object.fromEntries(ROLE_NAMES
|
|
92
|
-
.filter((role) => role !== "codex-translator")
|
|
93
|
-
.map((role) => [role, `${logsDir}/${role}.log`])),
|
|
94
90
|
messageRoutePaths: {},
|
|
95
91
|
architecturePlanPath: `${handoffDir}/architecture-plan.md`,
|
|
96
92
|
knownIssuesPath: `${handoffDir}/known-issues.md`,
|
|
@@ -352,17 +352,6 @@ export function createGatewayService(deps) {
|
|
|
352
352
|
}
|
|
353
353
|
async function pullCurrent() {
|
|
354
354
|
const project = await ensureProject();
|
|
355
|
-
const settings = await syncDesktopContext(await deps.settings.loadSettings());
|
|
356
|
-
if (settings.currentTaskSlug) {
|
|
357
|
-
const task = await deps.taskService.loadTask(project.repoRoot, settings.currentTaskSlug);
|
|
358
|
-
if (!task.worktreePath && task.cleanupStatus !== "cleaned") {
|
|
359
|
-
throw new VcmError({
|
|
360
|
-
code: "GATEWAY_PULL_BLOCKED_BY_INLINE_TASK",
|
|
361
|
-
message: `Inline task "${task.taskSlug}" uses the base repository.`,
|
|
362
|
-
statusCode: 409
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
355
|
const pulled = await deps.projectService.pullCurrentProject();
|
|
367
356
|
return [
|
|
368
357
|
"Connected repository updated.",
|
|
@@ -377,7 +366,7 @@ export function createGatewayService(deps) {
|
|
|
377
366
|
if (tasks.length === 0) {
|
|
378
367
|
return "No tasks. Use /create-task <task-slug> [title] to create one.";
|
|
379
368
|
}
|
|
380
|
-
return tasks.map((task, index) => `${index + 1}. ${task.taskSlug} [${task.status}] ${task.
|
|
369
|
+
return tasks.map((task, index) => `${index + 1}. ${task.taskSlug} [${task.status}] ${task.branch}`).join("\n");
|
|
381
370
|
}
|
|
382
371
|
async function useTask(selector) {
|
|
383
372
|
const project = await ensureProject();
|
|
@@ -405,8 +394,7 @@ export function createGatewayService(deps) {
|
|
|
405
394
|
const project = await ensureProject();
|
|
406
395
|
const task = await deps.taskService.createTask(project.repoRoot, {
|
|
407
396
|
taskSlug,
|
|
408
|
-
title
|
|
409
|
-
createWorktree: true
|
|
397
|
+
title
|
|
410
398
|
});
|
|
411
399
|
const config = await deps.projectService.loadConfig(project.repoRoot);
|
|
412
400
|
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
@@ -460,7 +448,7 @@ export function createGatewayService(deps) {
|
|
|
460
448
|
return [
|
|
461
449
|
`Task created and initialized: ${task.taskSlug}`,
|
|
462
450
|
`branch: ${task.branch}`,
|
|
463
|
-
`worktree: ${task.worktreePath
|
|
451
|
+
`worktree: ${task.worktreePath}`,
|
|
464
452
|
`orchestration: ${template.autoOrchestration ? "auto" : "manual"}`,
|
|
465
453
|
`translation: ${preferences.translationEnabled ? "on" : "off"}`,
|
|
466
454
|
`sessions: ${startedRoles.join(", ")}`
|
|
@@ -523,7 +511,6 @@ export function createGatewayService(deps) {
|
|
|
523
511
|
deps.roundService.stopTask(taskSlug);
|
|
524
512
|
const result = await deps.taskService.cleanupTask(project.repoRoot, taskSlug, {
|
|
525
513
|
force: true,
|
|
526
|
-
deleteBranch: Boolean(task.worktreePath),
|
|
527
514
|
forceDeleteBranch: true
|
|
528
515
|
});
|
|
529
516
|
clearFailedTranslation(project.repoRoot, taskSlug);
|
package/dist/backend/server.js
CHANGED
|
@@ -82,7 +82,8 @@ export async function createServer(deps, options = {}) {
|
|
|
82
82
|
});
|
|
83
83
|
registerHarnessRoutes(app, {
|
|
84
84
|
projectService: deps.projectService,
|
|
85
|
-
harnessService: deps.harnessService
|
|
85
|
+
harnessService: deps.harnessService,
|
|
86
|
+
taskService: deps.taskService
|
|
86
87
|
});
|
|
87
88
|
registerTaskRoutes(app, {
|
|
88
89
|
projectService: deps.projectService,
|
|
@@ -170,6 +171,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
170
171
|
const projectService = createProjectService({ fs, git, appSettings });
|
|
171
172
|
const harnessService = createHarnessService({
|
|
172
173
|
fs,
|
|
174
|
+
git,
|
|
173
175
|
runtime,
|
|
174
176
|
projectService,
|
|
175
177
|
apiUrl: options.apiUrl,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { DISPATCHABLE_ROLES
|
|
2
|
+
import { DISPATCHABLE_ROLES } from "../../shared/constants.js";
|
|
3
3
|
import { checkMarkdownArtifact } from "../../shared/validation/artifact-check.js";
|
|
4
4
|
import { VcmError } from "../errors.js";
|
|
5
5
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
@@ -25,25 +25,16 @@ export function createArtifactService(fs) {
|
|
|
25
25
|
return {
|
|
26
26
|
getHandoffPaths(_repoRoot, handoffDir) {
|
|
27
27
|
const roleCommandsDir = path.posix.join(handoffDir, "role-commands");
|
|
28
|
-
const logsDir = path.posix.join(handoffDir, "logs");
|
|
29
28
|
const messagesDir = path.posix.join(handoffDir, "messages");
|
|
30
29
|
return {
|
|
31
30
|
handoffDir,
|
|
32
31
|
roleCommandsDir,
|
|
33
|
-
logsDir,
|
|
34
32
|
messagesDir,
|
|
35
33
|
roleCommandPaths: {
|
|
36
34
|
architect: path.posix.join(roleCommandsDir, "architect.md"),
|
|
37
35
|
coder: path.posix.join(roleCommandsDir, "coder.md"),
|
|
38
36
|
reviewer: path.posix.join(roleCommandsDir, "reviewer.md")
|
|
39
37
|
},
|
|
40
|
-
roleLogPaths: {
|
|
41
|
-
"project-manager": path.posix.join(logsDir, "project-manager.log"),
|
|
42
|
-
architect: path.posix.join(logsDir, "architect.log"),
|
|
43
|
-
coder: path.posix.join(logsDir, "coder.log"),
|
|
44
|
-
reviewer: path.posix.join(logsDir, "reviewer.log"),
|
|
45
|
-
"codex-reviewer": path.posix.join(logsDir, "codex-reviewer.log")
|
|
46
|
-
},
|
|
47
38
|
messageRoutePaths: getDefaultMessageRoutePaths(messagesDir),
|
|
48
39
|
architecturePlanPath: path.posix.join(handoffDir, "architecture-plan.md"),
|
|
49
40
|
knownIssuesPath: path.posix.join(handoffDir, "known-issues.md"),
|
|
@@ -59,7 +50,6 @@ export function createArtifactService(fs) {
|
|
|
59
50
|
const paths = this.getHandoffPaths(input.repoRoot, input.handoffDir);
|
|
60
51
|
await fs.ensureDir(resolveRepoPath(input.repoRoot, paths.handoffDir));
|
|
61
52
|
await fs.ensureDir(resolveRepoPath(input.repoRoot, paths.roleCommandsDir));
|
|
62
|
-
await fs.ensureDir(resolveRepoPath(input.repoRoot, paths.logsDir));
|
|
63
53
|
await fs.ensureDir(resolveRepoPath(input.repoRoot, paths.messagesDir));
|
|
64
54
|
return paths;
|
|
65
55
|
},
|
|
@@ -168,25 +158,6 @@ export function createArtifactService(fs) {
|
|
|
168
158
|
async saveRoleCommand(input) {
|
|
169
159
|
const paths = this.getHandoffPaths(input.repoRoot, input.handoffDir);
|
|
170
160
|
await fs.writeText(resolveRepoPath(input.repoRoot, paths.roleCommandPaths[input.role]), input.content);
|
|
171
|
-
},
|
|
172
|
-
async appendRoleLog(input) {
|
|
173
|
-
if (!ROLE_NAMES.includes(input.role)) {
|
|
174
|
-
throw new VcmError({
|
|
175
|
-
code: "UNKNOWN_ROLE",
|
|
176
|
-
message: `Unknown role: ${input.role}`,
|
|
177
|
-
statusCode: 400
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
const paths = this.getHandoffPaths(input.repoRoot, input.handoffDir);
|
|
181
|
-
const roleLogPath = paths.roleLogPaths[input.role];
|
|
182
|
-
if (!roleLogPath) {
|
|
183
|
-
throw new VcmError({
|
|
184
|
-
code: "ROLE_LOG_UNAVAILABLE",
|
|
185
|
-
message: `${input.role} does not write a handoff log.`,
|
|
186
|
-
statusCode: 409
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
await fs.appendText(resolveRepoPath(input.repoRoot, roleLogPath), input.content);
|
|
190
161
|
}
|
|
191
162
|
};
|
|
192
163
|
}
|
|
@@ -812,7 +812,7 @@ export function createCodexTranslationService(deps) {
|
|
|
812
812
|
await cleanupStartupFileIndex(repoRoot);
|
|
813
813
|
await cleanupStartupBootstrapIndex(repoRoot);
|
|
814
814
|
},
|
|
815
|
-
async getState(repoRoot) {
|
|
815
|
+
async getState(repoRoot, options = {}) {
|
|
816
816
|
await ensureLayout(repoRoot);
|
|
817
817
|
await cleanupCompletedRuntime(repoRoot);
|
|
818
818
|
const [queue, fileIndex, bootstrapIndex, memoryInitialized] = await Promise.all([
|
|
@@ -822,7 +822,7 @@ export function createCodexTranslationService(deps) {
|
|
|
822
822
|
isMemoryInitialized(repoRoot, deps.fs)
|
|
823
823
|
]);
|
|
824
824
|
const runtimeFileJobs = await loadRuntimeFileJobs(repoRoot, queue);
|
|
825
|
-
|
|
825
|
+
const state = {
|
|
826
826
|
queue,
|
|
827
827
|
fileIndex: visibleFileTranslationIndex({
|
|
828
828
|
...fileIndex,
|
|
@@ -831,6 +831,7 @@ export function createCodexTranslationService(deps) {
|
|
|
831
831
|
bootstrapIndex,
|
|
832
832
|
memoryInitialized
|
|
833
833
|
};
|
|
834
|
+
return options.visibility === "public" ? toPublicTranslationState(state) : state;
|
|
834
835
|
},
|
|
835
836
|
async browseSourceFiles(repoRoot, input = {}) {
|
|
836
837
|
const currentPath = normalizeBrowserPath(input.path ?? "");
|
|
@@ -1742,6 +1743,26 @@ function isPrunableCompletedQueueItem(item) {
|
|
|
1742
1743
|
item.type === "bootstrap" ||
|
|
1743
1744
|
item.type === "memory-update");
|
|
1744
1745
|
}
|
|
1746
|
+
function toPublicTranslationState(state) {
|
|
1747
|
+
const items = state.queue.items
|
|
1748
|
+
.filter((item) => item.type !== "conversation")
|
|
1749
|
+
.map(toPublicQueueItem);
|
|
1750
|
+
const activeItemId = state.queue.activeItemId && items.some((item) => item.id === state.queue.activeItemId)
|
|
1751
|
+
? state.queue.activeItemId
|
|
1752
|
+
: undefined;
|
|
1753
|
+
return {
|
|
1754
|
+
...state,
|
|
1755
|
+
queue: {
|
|
1756
|
+
...state.queue,
|
|
1757
|
+
activeItemId,
|
|
1758
|
+
items
|
|
1759
|
+
}
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
function toPublicQueueItem(item) {
|
|
1763
|
+
const { translatedText: _translatedText, ...publicItem } = item;
|
|
1764
|
+
return publicItem;
|
|
1765
|
+
}
|
|
1745
1766
|
function isTranslationRuntimeDirectory(relativePath) {
|
|
1746
1767
|
const normalized = normalizeRepoRelative(relativePath);
|
|
1747
1768
|
return normalized.startsWith(`${TRANSLATIONS_RUNTIME_DIR}/`);
|
|
@@ -246,6 +246,73 @@ export function createHarnessService(deps) {
|
|
|
246
246
|
: "VCM Harness updated. Review these files and commit the harness changes before starting long-running work."
|
|
247
247
|
};
|
|
248
248
|
},
|
|
249
|
+
async commitAndRebaseTask(repoRoot, input) {
|
|
250
|
+
if (!deps.git) {
|
|
251
|
+
throw new VcmError({
|
|
252
|
+
code: "HARNESS_GIT_UNAVAILABLE",
|
|
253
|
+
message: "Git-backed harness sync is not available in this VCM runtime.",
|
|
254
|
+
statusCode: 501
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
const changedFiles = dedupeHarnessChanges(input.changedFiles);
|
|
258
|
+
const changedPaths = changedFiles.map((change) => change.path);
|
|
259
|
+
const taskBranch = await deps.git.getCurrentBranch(input.worktreePath);
|
|
260
|
+
if (taskBranch !== input.branch) {
|
|
261
|
+
throw new VcmError({
|
|
262
|
+
code: "HARNESS_TASK_BRANCH_MISMATCH",
|
|
263
|
+
message: "The selected task worktree is not on its recorded task branch.",
|
|
264
|
+
statusCode: 409,
|
|
265
|
+
hint: `Expected ${input.branch}, found ${taskBranch}.`
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
const taskStatus = await deps.git.getStatusPorcelain(input.worktreePath);
|
|
269
|
+
if (taskStatus.trim()) {
|
|
270
|
+
throw new VcmError({
|
|
271
|
+
code: "HARNESS_TASK_DIRTY",
|
|
272
|
+
message: "The selected task worktree has uncommitted changes.",
|
|
273
|
+
statusCode: 409,
|
|
274
|
+
hint: "Commit or clean the task worktree before rebasing it onto the harness commit."
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
const preExistingStaged = await deps.git.getStagedStatus(repoRoot);
|
|
278
|
+
if (preExistingStaged.trim()) {
|
|
279
|
+
throw new VcmError({
|
|
280
|
+
code: "HARNESS_BASE_STAGED_CHANGES",
|
|
281
|
+
message: "The connected repository already has staged changes.",
|
|
282
|
+
statusCode: 409,
|
|
283
|
+
hint: "Commit, unstage, or clean existing staged changes before using Commit & rebase task."
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const baseBranch = await deps.git.getCurrentBranch(repoRoot);
|
|
287
|
+
const baseCommitBefore = await deps.git.getHeadCommit(repoRoot);
|
|
288
|
+
let harnessCommit;
|
|
289
|
+
let committed = false;
|
|
290
|
+
if (changedPaths.length > 0) {
|
|
291
|
+
await deps.git.addPaths(repoRoot, changedPaths);
|
|
292
|
+
const stagedHarnessChanges = await deps.git.getStagedStatus(repoRoot);
|
|
293
|
+
if (stagedHarnessChanges.trim()) {
|
|
294
|
+
harnessCommit = await deps.git.commit(repoRoot, "chore: update VCM harness");
|
|
295
|
+
committed = true;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const baseCommitAfter = await deps.git.getHeadCommit(repoRoot);
|
|
299
|
+
await deps.git.rebase(input.worktreePath, baseCommitAfter);
|
|
300
|
+
return {
|
|
301
|
+
taskSlug: input.taskSlug,
|
|
302
|
+
branch: input.branch,
|
|
303
|
+
worktreePath: input.worktreePath,
|
|
304
|
+
baseBranch,
|
|
305
|
+
baseCommitBefore,
|
|
306
|
+
baseCommitAfter,
|
|
307
|
+
harnessCommit,
|
|
308
|
+
committed,
|
|
309
|
+
rebased: true,
|
|
310
|
+
changedFiles,
|
|
311
|
+
message: committed
|
|
312
|
+
? `Committed VCM harness update ${shortCommit(baseCommitAfter)} on ${baseBranch} and rebased ${input.branch}.`
|
|
313
|
+
: `No new harness commit was needed; rebased ${input.branch} onto ${shortCommit(baseCommitAfter)}.`
|
|
314
|
+
};
|
|
315
|
+
},
|
|
249
316
|
async getBootstrapStatus(repoRoot) {
|
|
250
317
|
return getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
251
318
|
},
|
|
@@ -320,6 +387,45 @@ export function createHarnessService(deps) {
|
|
|
320
387
|
}
|
|
321
388
|
};
|
|
322
389
|
}
|
|
390
|
+
function dedupeHarnessChanges(changedFiles) {
|
|
391
|
+
const seen = new Set();
|
|
392
|
+
const changes = [];
|
|
393
|
+
for (const change of changedFiles) {
|
|
394
|
+
const normalizedPath = normalizeHarnessGitPath(change.path);
|
|
395
|
+
if (seen.has(normalizedPath)) {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
seen.add(normalizedPath);
|
|
399
|
+
changes.push({
|
|
400
|
+
...change,
|
|
401
|
+
path: normalizedPath
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return changes;
|
|
405
|
+
}
|
|
406
|
+
function normalizeHarnessGitPath(value) {
|
|
407
|
+
if (!value || value.includes("\0") || path.posix.isAbsolute(value)) {
|
|
408
|
+
throw new VcmError({
|
|
409
|
+
code: "HARNESS_CHANGED_PATH_INVALID",
|
|
410
|
+
message: "Harness changed file path is invalid.",
|
|
411
|
+
statusCode: 400,
|
|
412
|
+
hint: value
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
const normalized = path.posix.normalize(value).replace(/^\.\//, "");
|
|
416
|
+
if (normalized === "." || normalized.startsWith("../")) {
|
|
417
|
+
throw new VcmError({
|
|
418
|
+
code: "HARNESS_CHANGED_PATH_INVALID",
|
|
419
|
+
message: "Harness changed file path must stay inside the repository.",
|
|
420
|
+
statusCode: 400,
|
|
421
|
+
hint: value
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
return normalized;
|
|
425
|
+
}
|
|
426
|
+
function shortCommit(commit) {
|
|
427
|
+
return commit.slice(0, 7);
|
|
428
|
+
}
|
|
323
429
|
async function analyzeHarnessFiles(fs, repoRoot) {
|
|
324
430
|
const analyses = [];
|
|
325
431
|
for (const definition of HARNESS_FILES) {
|
|
@@ -30,7 +30,6 @@ export function createSessionService(deps) {
|
|
|
30
30
|
const isCodexRole = isCodexRoleName(role);
|
|
31
31
|
const isTranslator = role === CODEX_TRANSLATOR_ROLE;
|
|
32
32
|
const sessionRepoRoot = isTranslator ? repoRoot : taskRepoRoot;
|
|
33
|
-
const sessionLogPath = isTranslator ? undefined : paths.roleLogPaths[role];
|
|
34
33
|
const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
|
|
35
34
|
const model = isCodexRole
|
|
36
35
|
? normalizeCodexModel(input.model ?? persisted?.model)
|
|
@@ -72,8 +71,7 @@ export function createSessionService(deps) {
|
|
|
72
71
|
VCM_SESSION_ID: claudeSessionId
|
|
73
72
|
},
|
|
74
73
|
cols: input.cols,
|
|
75
|
-
rows: input.rows
|
|
76
|
-
...(sessionLogPath ? { logPath: resolveRepoPath(sessionRepoRoot, sessionLogPath) } : {})
|
|
74
|
+
rows: input.rows
|
|
77
75
|
});
|
|
78
76
|
const timestamp = now();
|
|
79
77
|
const record = {
|
|
@@ -91,7 +89,6 @@ export function createSessionService(deps) {
|
|
|
91
89
|
cwd: startCommand.cwd,
|
|
92
90
|
terminalBackend: "node-pty",
|
|
93
91
|
pid: runtimeSession.pid,
|
|
94
|
-
...(sessionLogPath ? { logPath: sessionLogPath } : {}),
|
|
95
92
|
roleCommandPath: isDispatchableRole(role)
|
|
96
93
|
? paths.roleCommandPaths[role]
|
|
97
94
|
: undefined,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DISPATCHABLE_ROLES
|
|
1
|
+
import { DISPATCHABLE_ROLES } from "../../shared/constants.js";
|
|
2
2
|
import { isOpenFileLimitError } from "../errors.js";
|
|
3
3
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
4
4
|
export function createStatusService(deps) {
|
|
@@ -45,18 +45,13 @@ export function createStatusService(deps) {
|
|
|
45
45
|
}
|
|
46
46
|
function degradedArtifactSummary(handoffDir) {
|
|
47
47
|
const roleCommandsDir = `${handoffDir}/role-commands`;
|
|
48
|
-
const logsDir = `${handoffDir}/logs`;
|
|
49
48
|
const messagesDir = `${handoffDir}/messages`;
|
|
50
49
|
return {
|
|
51
50
|
paths: {
|
|
52
51
|
handoffDir,
|
|
53
52
|
roleCommandsDir,
|
|
54
|
-
logsDir,
|
|
55
53
|
messagesDir,
|
|
56
54
|
roleCommandPaths: Object.fromEntries(DISPATCHABLE_ROLES.map((role) => [role, `${roleCommandsDir}/${role}.md`])),
|
|
57
|
-
roleLogPaths: Object.fromEntries(ROLE_NAMES
|
|
58
|
-
.filter((role) => role !== "codex-translator")
|
|
59
|
-
.map((role) => [role, `${logsDir}/${role}.log`])),
|
|
60
55
|
messageRoutePaths: {},
|
|
61
56
|
architecturePlanPath: `${handoffDir}/architecture-plan.md`,
|
|
62
57
|
knownIssuesPath: `${handoffDir}/known-issues.md`,
|