vibe-coding-master 0.3.22 → 0.3.24
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 +2 -2
- 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 +24 -0
- 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 +5 -2
- package/dist/backend/services/artifact-service.js +1 -30
- package/dist/backend/services/codex-hook-service.js +30 -9
- package/dist/backend/services/codex-translation-service.js +17 -40
- package/dist/backend/services/harness-service.js +106 -0
- package/dist/backend/services/session-service.js +189 -15
- package/dist/backend/services/status-service.js +1 -6
- package/dist/backend/services/task-service.js +42 -110
- package/dist/backend/services/translation-service.js +15 -4
- package/dist/backend/templates/handoff.js +3 -3
- package/dist/backend/templates/harness/architect-agent.js +3 -2
- package/dist/backend/templates/harness/coder-agent.js +6 -1
- 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/shared/types/app-settings.js +3 -2
- package/dist-frontend/assets/index-CKWy15WL.js +94 -0
- package/dist-frontend/assets/index-Cfum1Prr.css +32 -0
- package/dist-frontend/index.html +2 -2
- package/docs/cc-best-practices.md +1 -1
- package/docs/codex-translation-plan.md +41 -37
- package/docs/full-harness-baseline.md +0 -1
- package/docs/gateway-design.md +9 -13
- package/docs/product-design.md +23 -44
- package/docs/v0.4-custom-workflow-plan.md +785 -0
- package/docs/vcm-cc-best-practices.md +15 -13
- package/package.json +1 -1
- package/dist-frontend/assets/index-CEB6Bssn.js +0 -95
- package/dist-frontend/assets/index-Dmefx9m7.css +0 -32
|
@@ -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) {
|
|
@@ -12,8 +12,9 @@ const CODEX_REVIEW_DIR = ".ai/vcm/codex-reviews";
|
|
|
12
12
|
const CODEX_CONFIG_PATH = ".ai/codex/config.toml";
|
|
13
13
|
const CODEX_TRANSLATOR_DIR = ".ai/codex-translator";
|
|
14
14
|
const CODEX_TRANSLATION_DIR = ".ai/vcm/translations";
|
|
15
|
-
const CODEX_TRANSLATOR_SESSION_PATH = ".ai/vcm/translations/
|
|
15
|
+
const CODEX_TRANSLATOR_SESSION_PATH = ".ai/vcm/translations/session.json";
|
|
16
16
|
const CODEX_TRANSLATOR_CONFIG_PATH = ".ai/codex-translator/config.toml";
|
|
17
|
+
const PROJECT_TRANSLATOR_SCOPE = "__project__";
|
|
17
18
|
export function createSessionService(deps) {
|
|
18
19
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
19
20
|
async function launchRoleSession(repoRoot, taskSlug, role, input, launchMode) {
|
|
@@ -29,7 +30,6 @@ export function createSessionService(deps) {
|
|
|
29
30
|
const isCodexRole = isCodexRoleName(role);
|
|
30
31
|
const isTranslator = role === CODEX_TRANSLATOR_ROLE;
|
|
31
32
|
const sessionRepoRoot = isTranslator ? repoRoot : taskRepoRoot;
|
|
32
|
-
const sessionLogPath = isTranslator ? undefined : paths.roleLogPaths[role];
|
|
33
33
|
const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
|
|
34
34
|
const model = isCodexRole
|
|
35
35
|
? normalizeCodexModel(input.model ?? persisted?.model)
|
|
@@ -71,8 +71,7 @@ export function createSessionService(deps) {
|
|
|
71
71
|
VCM_SESSION_ID: claudeSessionId
|
|
72
72
|
},
|
|
73
73
|
cols: input.cols,
|
|
74
|
-
rows: input.rows
|
|
75
|
-
...(sessionLogPath ? { logPath: resolveRepoPath(sessionRepoRoot, sessionLogPath) } : {})
|
|
74
|
+
rows: input.rows
|
|
76
75
|
});
|
|
77
76
|
const timestamp = now();
|
|
78
77
|
const record = {
|
|
@@ -90,7 +89,6 @@ export function createSessionService(deps) {
|
|
|
90
89
|
cwd: startCommand.cwd,
|
|
91
90
|
terminalBackend: "node-pty",
|
|
92
91
|
pid: runtimeSession.pid,
|
|
93
|
-
...(sessionLogPath ? { logPath: sessionLogPath } : {}),
|
|
94
92
|
roleCommandPath: isDispatchableRole(role)
|
|
95
93
|
? paths.roleCommandPaths[role]
|
|
96
94
|
: undefined,
|
|
@@ -104,14 +102,167 @@ export function createSessionService(deps) {
|
|
|
104
102
|
await persistRoleSessionRecord(deps.fs, repoRoot, taskRepoRoot, config.stateRoot, record);
|
|
105
103
|
return record;
|
|
106
104
|
}
|
|
105
|
+
async function launchProjectTranslatorSession(repoRoot, input, launchMode) {
|
|
106
|
+
const live = toRoleSessionRecordView(getRegisteredProjectTranslatorSession(deps.registry, deps.runtime), deps.runtime);
|
|
107
|
+
if (live && live.status === "running") {
|
|
108
|
+
return live;
|
|
109
|
+
}
|
|
110
|
+
const persisted = await loadPersistedCodexTranslatorSession(deps.fs, repoRoot);
|
|
111
|
+
const model = normalizeCodexModel(input.model ?? persisted?.model);
|
|
112
|
+
const effort = normalizeCodexEffort(input.effort ?? persisted?.effort);
|
|
113
|
+
const claudeSessionId = launchMode === "resume"
|
|
114
|
+
? persisted?.claudeSessionId
|
|
115
|
+
: randomUUID();
|
|
116
|
+
if (!claudeSessionId) {
|
|
117
|
+
throw new VcmError({
|
|
118
|
+
code: "CODEX_TRANSLATOR_SESSION_MISSING",
|
|
119
|
+
message: "Codex Translator does not have a session id to resume.",
|
|
120
|
+
statusCode: 409,
|
|
121
|
+
hint: "Start the translator once before using Resume."
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
const startCommand = await buildCodexStartCommand(deps.fs, repoRoot, repoRoot, CODEX_TRANSLATOR_ROLE, launchMode, model, effort, deps.sandboxMode, launchMode === "resume" && hasCapturedCodexSession(persisted) ? claudeSessionId : undefined);
|
|
125
|
+
const runtimeSession = await deps.runtime.createSession({
|
|
126
|
+
taskSlug: PROJECT_TRANSLATOR_SCOPE,
|
|
127
|
+
role: CODEX_TRANSLATOR_ROLE,
|
|
128
|
+
command: startCommand.command,
|
|
129
|
+
args: startCommand.args,
|
|
130
|
+
cwd: startCommand.cwd,
|
|
131
|
+
env: {
|
|
132
|
+
VCM_API_URL: deps.apiUrl,
|
|
133
|
+
VCM_TASK_REPO_ROOT: repoRoot,
|
|
134
|
+
VCM_TASK_SLUG: PROJECT_TRANSLATOR_SCOPE,
|
|
135
|
+
VCM_ROLE: CODEX_TRANSLATOR_ROLE,
|
|
136
|
+
VCM_SESSION_ID: claudeSessionId
|
|
137
|
+
},
|
|
138
|
+
cols: input.cols,
|
|
139
|
+
rows: input.rows
|
|
140
|
+
});
|
|
141
|
+
const timestamp = now();
|
|
142
|
+
const record = {
|
|
143
|
+
id: runtimeSession.id,
|
|
144
|
+
claudeSessionId,
|
|
145
|
+
taskSlug: PROJECT_TRANSLATOR_SCOPE,
|
|
146
|
+
role: CODEX_TRANSLATOR_ROLE,
|
|
147
|
+
status: runtimeSession.status,
|
|
148
|
+
activityStatus: "idle",
|
|
149
|
+
command: startCommand.display,
|
|
150
|
+
permissionMode: "default",
|
|
151
|
+
model,
|
|
152
|
+
effort,
|
|
153
|
+
cwd: startCommand.cwd,
|
|
154
|
+
terminalBackend: "node-pty",
|
|
155
|
+
pid: runtimeSession.pid,
|
|
156
|
+
startedAt: runtimeSession.startedAt,
|
|
157
|
+
updatedAt: timestamp,
|
|
158
|
+
lastOutputAt: runtimeSession.lastOutputAt,
|
|
159
|
+
exitCode: runtimeSession.exitCode
|
|
160
|
+
};
|
|
161
|
+
deps.registry.upsert(record);
|
|
162
|
+
await persistCodexTranslatorSession(deps.fs, repoRoot, record);
|
|
163
|
+
return record;
|
|
164
|
+
}
|
|
107
165
|
return {
|
|
166
|
+
startProjectTranslatorSession(repoRoot, input = {}) {
|
|
167
|
+
return launchProjectTranslatorSession(repoRoot, input, "fresh");
|
|
168
|
+
},
|
|
169
|
+
resumeProjectTranslatorSession(repoRoot, input = {}) {
|
|
170
|
+
return launchProjectTranslatorSession(repoRoot, input, "resume");
|
|
171
|
+
},
|
|
172
|
+
async stopProjectTranslatorSession(repoRoot) {
|
|
173
|
+
const existing = await this.getProjectTranslatorSession(repoRoot);
|
|
174
|
+
if (!existing) {
|
|
175
|
+
throw new VcmError({
|
|
176
|
+
code: "SESSION_MISSING",
|
|
177
|
+
message: "Codex Translator session has not been started.",
|
|
178
|
+
statusCode: 404
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
if (deps.runtime.getSession(existing.id)) {
|
|
182
|
+
await deps.runtime.stop(existing.id);
|
|
183
|
+
}
|
|
184
|
+
const updated = {
|
|
185
|
+
...existing,
|
|
186
|
+
status: "exited",
|
|
187
|
+
activityStatus: "idle",
|
|
188
|
+
updatedAt: now()
|
|
189
|
+
};
|
|
190
|
+
deps.registry.upsert(updated);
|
|
191
|
+
await persistCodexTranslatorSession(deps.fs, repoRoot, updated);
|
|
192
|
+
return updated;
|
|
193
|
+
},
|
|
194
|
+
async restartProjectTranslatorSession(repoRoot, input = {}) {
|
|
195
|
+
const existing = await this.getProjectTranslatorSession(repoRoot);
|
|
196
|
+
if (!existing) {
|
|
197
|
+
return launchProjectTranslatorSession(repoRoot, input, "fresh");
|
|
198
|
+
}
|
|
199
|
+
if (deps.runtime.getSession(existing.id)) {
|
|
200
|
+
await deps.runtime.stop(existing.id);
|
|
201
|
+
}
|
|
202
|
+
deps.registry.remove(existing.id);
|
|
203
|
+
return launchProjectTranslatorSession(repoRoot, input, "fresh");
|
|
204
|
+
},
|
|
205
|
+
async getProjectTranslatorSession(repoRoot) {
|
|
206
|
+
const record = getRegisteredProjectTranslatorSession(deps.registry, deps.runtime)
|
|
207
|
+
?? await loadPersistedCodexTranslatorSession(deps.fs, repoRoot);
|
|
208
|
+
return toRoleSessionRecordView(record, deps.runtime);
|
|
209
|
+
},
|
|
210
|
+
async ensureProjectTranslatorSession(repoRoot, input = {}) {
|
|
211
|
+
const existing = await this.getProjectTranslatorSession(repoRoot);
|
|
212
|
+
if (existing?.status === "running") {
|
|
213
|
+
return existing;
|
|
214
|
+
}
|
|
215
|
+
if (existing?.claudeSessionId) {
|
|
216
|
+
return this.resumeProjectTranslatorSession(repoRoot, {
|
|
217
|
+
model: input.model ?? existing.model,
|
|
218
|
+
effort: input.effort ?? existing.effort,
|
|
219
|
+
cols: input.cols,
|
|
220
|
+
rows: input.rows
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return this.startProjectTranslatorSession(repoRoot, input);
|
|
224
|
+
},
|
|
225
|
+
async recordProjectTranslatorHookEvent(repoRoot, input) {
|
|
226
|
+
const current = await this.getProjectTranslatorSession(repoRoot);
|
|
227
|
+
if (!current) {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
const timestamp = now();
|
|
231
|
+
const isStop = input.eventName === "Stop";
|
|
232
|
+
const updated = {
|
|
233
|
+
...current,
|
|
234
|
+
claudeSessionId: input.sessionId ?? current.claudeSessionId,
|
|
235
|
+
transcriptPath: input.transcriptPath ?? current.transcriptPath,
|
|
236
|
+
cwd: input.cwd ?? current.cwd,
|
|
237
|
+
activityStatus: isStop ? "idle" : "running",
|
|
238
|
+
lastHookEventAt: timestamp,
|
|
239
|
+
lastTurnEndedAt: isStop ? timestamp : current.lastTurnEndedAt,
|
|
240
|
+
lastTurnStartedAt: isStop ? current.lastTurnStartedAt : timestamp,
|
|
241
|
+
updatedAt: timestamp
|
|
242
|
+
};
|
|
243
|
+
deps.registry.upsert(updated);
|
|
244
|
+
await persistCodexTranslatorSession(deps.fs, repoRoot, updated);
|
|
245
|
+
return updated;
|
|
246
|
+
},
|
|
108
247
|
startRoleSession(repoRoot, taskSlug, role, input = {}) {
|
|
248
|
+
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
249
|
+
void taskSlug;
|
|
250
|
+
return this.startProjectTranslatorSession(repoRoot, input);
|
|
251
|
+
}
|
|
109
252
|
return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
|
|
110
253
|
},
|
|
111
254
|
resumeRoleSession(repoRoot, taskSlug, role, input = {}) {
|
|
255
|
+
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
256
|
+
void taskSlug;
|
|
257
|
+
return this.resumeProjectTranslatorSession(repoRoot, input);
|
|
258
|
+
}
|
|
112
259
|
return launchRoleSession(repoRoot, taskSlug, role, input, "resume");
|
|
113
260
|
},
|
|
114
261
|
async stopRoleSession(repoRoot, taskSlug, role) {
|
|
262
|
+
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
263
|
+
void taskSlug;
|
|
264
|
+
return this.stopProjectTranslatorSession(repoRoot);
|
|
265
|
+
}
|
|
115
266
|
const existing = await this.getRoleSession(repoRoot, taskSlug, role);
|
|
116
267
|
if (!existing) {
|
|
117
268
|
throw new VcmError({
|
|
@@ -136,6 +287,10 @@ export function createSessionService(deps) {
|
|
|
136
287
|
return updated;
|
|
137
288
|
},
|
|
138
289
|
async restartRoleSession(repoRoot, taskSlug, role, input = {}) {
|
|
290
|
+
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
291
|
+
void taskSlug;
|
|
292
|
+
return this.restartProjectTranslatorSession(repoRoot, input);
|
|
293
|
+
}
|
|
139
294
|
const existing = await this.getRoleSession(repoRoot, taskSlug, role);
|
|
140
295
|
if (!existing) {
|
|
141
296
|
return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
|
|
@@ -147,6 +302,10 @@ export function createSessionService(deps) {
|
|
|
147
302
|
return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
|
|
148
303
|
},
|
|
149
304
|
async getRoleSession(repoRoot, taskSlug, role) {
|
|
305
|
+
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
306
|
+
void taskSlug;
|
|
307
|
+
return this.getProjectTranslatorSession(repoRoot);
|
|
308
|
+
}
|
|
150
309
|
const config = await deps.projectService.loadConfig(repoRoot);
|
|
151
310
|
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
152
311
|
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
@@ -163,12 +322,9 @@ export function createSessionService(deps) {
|
|
|
163
322
|
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
164
323
|
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
165
324
|
const persistedTaskSession = await loadPersistedTaskSessionRecord(deps.fs, taskRepoRoot, config.stateRoot, taskSlug);
|
|
166
|
-
for (const role of ROLE_NAMES) {
|
|
167
|
-
const record = role
|
|
168
|
-
|
|
169
|
-
?? await loadPersistedCodexTranslatorSession(deps.fs, repoRoot, taskSlug)
|
|
170
|
-
: deps.registry.getByRole(taskSlug, role)
|
|
171
|
-
?? normalizePersistedRoleRecord(persistedTaskSession?.roles[role]?.record);
|
|
325
|
+
for (const role of ROLE_NAMES.filter((candidate) => candidate !== CODEX_TRANSLATOR_ROLE)) {
|
|
326
|
+
const record = deps.registry.getByRole(taskSlug, role)
|
|
327
|
+
?? normalizePersistedRoleRecord(persistedTaskSession?.roles[role]?.record);
|
|
172
328
|
const session = toRoleSessionRecordView(record, deps.runtime);
|
|
173
329
|
if (session) {
|
|
174
330
|
sessions.push(session);
|
|
@@ -177,6 +333,15 @@ export function createSessionService(deps) {
|
|
|
177
333
|
return sessions;
|
|
178
334
|
},
|
|
179
335
|
async recordRoleHookEvent(repoRoot, input) {
|
|
336
|
+
if (input.role === CODEX_TRANSLATOR_ROLE) {
|
|
337
|
+
void input.taskSlug;
|
|
338
|
+
return this.recordProjectTranslatorHookEvent(repoRoot, {
|
|
339
|
+
eventName: input.eventName,
|
|
340
|
+
sessionId: input.sessionId,
|
|
341
|
+
transcriptPath: input.transcriptPath,
|
|
342
|
+
cwd: input.cwd
|
|
343
|
+
});
|
|
344
|
+
}
|
|
180
345
|
const current = await this.getRoleSession(repoRoot, input.taskSlug, input.role);
|
|
181
346
|
if (!current || (!input.allowSessionMismatch && !matchesRoleHookSession(current, input))) {
|
|
182
347
|
return undefined;
|
|
@@ -385,6 +550,11 @@ function getRegisteredRoleSession(registry, runtime, taskSlug, role) {
|
|
|
385
550
|
?? candidates.sort(compareSessionUpdatedAtDesc)[0];
|
|
386
551
|
return scopeProjectRoleSession(scoped, taskSlug);
|
|
387
552
|
}
|
|
553
|
+
function getRegisteredProjectTranslatorSession(registry, runtime) {
|
|
554
|
+
const candidates = registry.list().filter((session) => session.role === CODEX_TRANSLATOR_ROLE);
|
|
555
|
+
const live = candidates.find((session) => runtime.getSession(session.id)?.status === "running");
|
|
556
|
+
return live ?? candidates.sort(compareSessionUpdatedAtDesc)[0];
|
|
557
|
+
}
|
|
388
558
|
function compareSessionUpdatedAtDesc(left, right) {
|
|
389
559
|
return (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "");
|
|
390
560
|
}
|
|
@@ -399,11 +569,12 @@ function scopeProjectRoleSession(record, taskSlug) {
|
|
|
399
569
|
}
|
|
400
570
|
async function loadPersistedRoleRecordForRole(fs, baseRepoRoot, taskRepoRoot, stateRoot, taskSlug, role) {
|
|
401
571
|
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
402
|
-
|
|
572
|
+
void taskSlug;
|
|
573
|
+
return loadPersistedCodexTranslatorSession(fs, baseRepoRoot);
|
|
403
574
|
}
|
|
404
575
|
return loadPersistedRoleRecord(fs, taskRepoRoot, stateRoot, taskSlug, role);
|
|
405
576
|
}
|
|
406
|
-
async function loadPersistedCodexTranslatorSession(fs, repoRoot
|
|
577
|
+
async function loadPersistedCodexTranslatorSession(fs, repoRoot) {
|
|
407
578
|
const sessionPath = resolveRepoPath(repoRoot, CODEX_TRANSLATOR_SESSION_PATH);
|
|
408
579
|
if (!(await fs.pathExists(sessionPath))) {
|
|
409
580
|
return undefined;
|
|
@@ -413,7 +584,10 @@ async function loadPersistedCodexTranslatorSession(fs, repoRoot, taskSlug) {
|
|
|
413
584
|
if (!record) {
|
|
414
585
|
return undefined;
|
|
415
586
|
}
|
|
416
|
-
return
|
|
587
|
+
return {
|
|
588
|
+
...record,
|
|
589
|
+
taskSlug: PROJECT_TRANSLATOR_SCOPE
|
|
590
|
+
};
|
|
417
591
|
}
|
|
418
592
|
async function loadPersistedRoleRecord(fs, repoRoot, stateRoot, taskSlug, role) {
|
|
419
593
|
const current = await loadPersistedTaskSessionRecord(fs, repoRoot, stateRoot, taskSlug);
|
|
@@ -486,7 +660,7 @@ async function persistCodexTranslatorSession(fs, repoRoot, session) {
|
|
|
486
660
|
updatedAt: session.updatedAt,
|
|
487
661
|
record: {
|
|
488
662
|
...session,
|
|
489
|
-
taskSlug:
|
|
663
|
+
taskSlug: PROJECT_TRANSLATOR_SCOPE
|
|
490
664
|
}
|
|
491
665
|
});
|
|
492
666
|
}
|
|
@@ -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`,
|
|
@@ -9,13 +9,8 @@ export function createTaskService(deps) {
|
|
|
9
9
|
const config = await deps.projectService.loadConfig(repoRoot);
|
|
10
10
|
const taskStoreRoot = deps.projectService.getProjectDataRoot(repoRoot);
|
|
11
11
|
const taskPath = getTaskPath(taskStoreRoot, input.taskSlug);
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
? `feature/${input.taskSlug}`
|
|
15
|
-
: await deps.git.getCurrentBranch(repoRoot);
|
|
16
|
-
const worktreePath = shouldCreateWorktree
|
|
17
|
-
? getTaskWorktreePath(repoRoot, input.taskSlug)
|
|
18
|
-
: undefined;
|
|
12
|
+
const taskBranch = `feature/${input.taskSlug}`;
|
|
13
|
+
const worktreePath = getTaskWorktreePath(repoRoot, input.taskSlug);
|
|
19
14
|
if (await deps.fs.pathExists(taskPath)) {
|
|
20
15
|
throw new VcmError({
|
|
21
16
|
code: "TASK_EXISTS",
|
|
@@ -31,7 +26,7 @@ export function createTaskService(deps) {
|
|
|
31
26
|
hint: "Apply VCM Harness first so .gitignore contains the VCM managed block."
|
|
32
27
|
});
|
|
33
28
|
}
|
|
34
|
-
if (
|
|
29
|
+
if (!(await deps.git.isIgnored(repoRoot, ".claude/worktrees/.probe"))) {
|
|
35
30
|
throw new VcmError({
|
|
36
31
|
code: "VCM_WORKTREES_NOT_IGNORED",
|
|
37
32
|
message: ".claude/worktrees/ is not ignored by Git.",
|
|
@@ -39,53 +34,39 @@ export function createTaskService(deps) {
|
|
|
39
34
|
hint: "Apply VCM Harness first so .gitignore ignores Claude-compatible task worktrees."
|
|
40
35
|
});
|
|
41
36
|
}
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
37
|
+
if (await deps.git.branchExists(repoRoot, taskBranch)) {
|
|
38
|
+
throw new VcmError({
|
|
39
|
+
code: "TASK_BRANCH_EXISTS",
|
|
40
|
+
message: `Task branch already exists: ${taskBranch}`,
|
|
41
|
+
statusCode: 409,
|
|
42
|
+
hint: "Choose a different task name or clean up the existing branch."
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (await deps.fs.pathExists(worktreePath)) {
|
|
46
|
+
throw new VcmError({
|
|
47
|
+
code: "TASK_WORKTREE_EXISTS",
|
|
48
|
+
message: `Task worktree already exists: ${worktreePath}`,
|
|
49
|
+
statusCode: 409,
|
|
50
|
+
hint: "Choose a different task name or clean up the existing worktree."
|
|
51
|
+
});
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
if (await deps.fs.pathExists(worktreePath)) {
|
|
63
|
-
throw new VcmError({
|
|
64
|
-
code: "TASK_WORKTREE_EXISTS",
|
|
65
|
-
message: `Task worktree already exists: ${worktreePath}`,
|
|
66
|
-
statusCode: 409,
|
|
67
|
-
hint: "Choose a different task name or clean up the existing worktree."
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
const baseStatus = await deps.git.getStatusPorcelain(repoRoot);
|
|
71
|
-
if (baseStatus.trim()) {
|
|
72
|
-
throw new VcmError({
|
|
73
|
-
code: "BASE_REPO_DIRTY",
|
|
74
|
-
message: "The connected repository has uncommitted changes.",
|
|
75
|
-
statusCode: 409,
|
|
76
|
-
hint: "Commit, stash, or discard base repository changes before creating a task worktree."
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
await deps.fs.ensureDir(path.dirname(worktreePath));
|
|
80
|
-
await deps.git.createWorktree({
|
|
81
|
-
repoRoot,
|
|
82
|
-
branch: taskBranch,
|
|
83
|
-
worktreePath,
|
|
84
|
-
baseRef: "HEAD"
|
|
53
|
+
const baseStatus = await deps.git.getStatusPorcelain(repoRoot);
|
|
54
|
+
if (baseStatus.trim()) {
|
|
55
|
+
throw new VcmError({
|
|
56
|
+
code: "BASE_REPO_DIRTY",
|
|
57
|
+
message: "The connected repository has uncommitted changes.",
|
|
58
|
+
statusCode: 409,
|
|
59
|
+
hint: "Commit, stash, or discard base repository changes before creating a task worktree."
|
|
85
60
|
});
|
|
86
61
|
}
|
|
87
62
|
const timestamp = now();
|
|
88
|
-
|
|
63
|
+
await deps.fs.ensureDir(path.dirname(worktreePath));
|
|
64
|
+
await deps.git.createWorktree({
|
|
65
|
+
repoRoot,
|
|
66
|
+
branch: taskBranch,
|
|
67
|
+
worktreePath,
|
|
68
|
+
baseRef: "HEAD"
|
|
69
|
+
});
|
|
89
70
|
const task = {
|
|
90
71
|
version: 1,
|
|
91
72
|
taskSlug: input.taskSlug,
|
|
@@ -100,14 +81,14 @@ export function createTaskService(deps) {
|
|
|
100
81
|
specPath: input.specPath,
|
|
101
82
|
cleanupStatus: "active"
|
|
102
83
|
};
|
|
103
|
-
await ensureTaskRuntimeStateDirs(deps.fs,
|
|
84
|
+
await ensureTaskRuntimeStateDirs(deps.fs, worktreePath, config.stateRoot);
|
|
104
85
|
await deps.artifactService.ensureHandoffStructure({
|
|
105
|
-
repoRoot:
|
|
86
|
+
repoRoot: worktreePath,
|
|
106
87
|
taskSlug: input.taskSlug,
|
|
107
88
|
handoffDir: task.handoffDir
|
|
108
89
|
});
|
|
109
90
|
await deps.artifactService.createArtifactTemplates({
|
|
110
|
-
repoRoot:
|
|
91
|
+
repoRoot: worktreePath,
|
|
111
92
|
taskSlug: input.taskSlug,
|
|
112
93
|
handoffDir: task.handoffDir,
|
|
113
94
|
branch: task.branch
|
|
@@ -123,7 +104,7 @@ export function createTaskService(deps) {
|
|
|
123
104
|
const entries = await deps.fs.readDir(tasksDir);
|
|
124
105
|
const tasks = [];
|
|
125
106
|
for (const entry of entries.filter((candidate) => candidate.endsWith(".json"))) {
|
|
126
|
-
tasks.push(
|
|
107
|
+
tasks.push(await deps.fs.readJson(path.join(tasksDir, entry)));
|
|
127
108
|
}
|
|
128
109
|
return tasks.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
129
110
|
},
|
|
@@ -137,7 +118,7 @@ export function createTaskService(deps) {
|
|
|
137
118
|
statusCode: 404
|
|
138
119
|
});
|
|
139
120
|
}
|
|
140
|
-
return
|
|
121
|
+
return deps.fs.readJson(taskPath);
|
|
141
122
|
},
|
|
142
123
|
async saveTask(repoRoot, task) {
|
|
143
124
|
await deps.fs.writeJsonAtomic(getTaskPath(deps.projectService.getProjectDataRoot(repoRoot), task.taskSlug), task);
|
|
@@ -167,15 +148,9 @@ export function createTaskService(deps) {
|
|
|
167
148
|
const statePaths = getTaskStatePaths(deps.projectService.getProjectDataRoot(repoRoot), taskRepoRoot, config.stateRoot, config.handoffRoot, taskSlug);
|
|
168
149
|
const removedStatePaths = [];
|
|
169
150
|
const cleanedAt = now();
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
let deletedBranch;
|
|
175
|
-
if (task.worktreePath && (options.deleteBranch ?? true)) {
|
|
176
|
-
await deps.git.deleteBranch(repoRoot, task.branch, { force: options.forceDeleteBranch ?? true });
|
|
177
|
-
deletedBranch = task.branch;
|
|
178
|
-
}
|
|
151
|
+
assertTaskWorktreePath(repoRoot, task.worktreePath);
|
|
152
|
+
await deps.git.removeWorktree(repoRoot, task.worktreePath, { force: options.force ?? true });
|
|
153
|
+
await deps.git.deleteBranch(repoRoot, task.branch, { force: options.forceDeleteBranch ?? true });
|
|
179
154
|
for (const statePath of statePaths) {
|
|
180
155
|
await deps.fs.removePath(statePath, { recursive: true, force: true });
|
|
181
156
|
removedStatePaths.push(statePath);
|
|
@@ -184,14 +159,14 @@ export function createTaskService(deps) {
|
|
|
184
159
|
taskSlug,
|
|
185
160
|
removedWorktreePath: task.worktreePath,
|
|
186
161
|
removedStatePaths,
|
|
187
|
-
deletedBranch,
|
|
162
|
+
deletedBranch: task.branch,
|
|
188
163
|
cleanedAt
|
|
189
164
|
};
|
|
190
165
|
}
|
|
191
166
|
};
|
|
192
167
|
}
|
|
193
168
|
export function getTaskRuntimeRepoRoot(task) {
|
|
194
|
-
return task.worktreePath
|
|
169
|
+
return task.worktreePath;
|
|
195
170
|
}
|
|
196
171
|
function getTaskPath(taskStoreRoot, taskSlug) {
|
|
197
172
|
return path.join(taskStoreRoot, "tasks", `${taskSlug}.json`);
|
|
@@ -206,49 +181,6 @@ async function ensureTaskRuntimeStateDirs(fs, taskRepoRoot, stateRoot) {
|
|
|
206
181
|
await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "translation"));
|
|
207
182
|
await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "codex-reviews"));
|
|
208
183
|
}
|
|
209
|
-
async function findActiveInlineTask(fs, taskStoreRoot) {
|
|
210
|
-
const tasksDir = path.join(taskStoreRoot, "tasks");
|
|
211
|
-
if (!(await fs.pathExists(tasksDir))) {
|
|
212
|
-
return undefined;
|
|
213
|
-
}
|
|
214
|
-
const entries = await fs.readDir(tasksDir);
|
|
215
|
-
for (const entry of entries.filter((candidate) => candidate.endsWith(".json"))) {
|
|
216
|
-
const task = normalizeTaskRecord(await fs.readJson(path.join(tasksDir, entry)));
|
|
217
|
-
if (!task.worktreePath && task.cleanupStatus !== "cleaned") {
|
|
218
|
-
return task;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return undefined;
|
|
222
|
-
}
|
|
223
|
-
function normalizeTaskRecord(input) {
|
|
224
|
-
return {
|
|
225
|
-
version: 1,
|
|
226
|
-
taskSlug: input.taskSlug ?? "",
|
|
227
|
-
title: input.title,
|
|
228
|
-
createdAt: input.createdAt ?? "",
|
|
229
|
-
updatedAt: input.updatedAt ?? input.createdAt ?? "",
|
|
230
|
-
repoRoot: input.repoRoot ?? "",
|
|
231
|
-
worktreePath: input.worktreePath,
|
|
232
|
-
branch: input.branch ?? "",
|
|
233
|
-
handoffDir: input.handoffDir ?? ".ai/vcm/handoffs",
|
|
234
|
-
status: normalizeTaskStatus(input.status),
|
|
235
|
-
specPath: input.specPath,
|
|
236
|
-
cleanupStatus: input.cleanupStatus,
|
|
237
|
-
cleanedAt: input.cleanedAt
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
function normalizeTaskStatus(value) {
|
|
241
|
-
if (value === "created" || value === "running" || value === "stopped") {
|
|
242
|
-
return value;
|
|
243
|
-
}
|
|
244
|
-
if (value === undefined || value === null) {
|
|
245
|
-
return "created";
|
|
246
|
-
}
|
|
247
|
-
if (value === "planning") {
|
|
248
|
-
return "created";
|
|
249
|
-
}
|
|
250
|
-
return "stopped";
|
|
251
|
-
}
|
|
252
184
|
function getTaskStatePaths(taskStoreRoot, taskRepoRoot, stateRoot, handoffRoot, taskSlug) {
|
|
253
185
|
return [
|
|
254
186
|
getTaskPath(taskStoreRoot, taskSlug),
|