vibe-coding-master 0.0.1
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/LICENSE +201 -0
- package/README.md +226 -0
- package/dist/backend/adapters/claude-adapter.js +38 -0
- package/dist/backend/adapters/command-runner.js +33 -0
- package/dist/backend/adapters/filesystem.js +60 -0
- package/dist/backend/adapters/git-adapter.js +33 -0
- package/dist/backend/api/artifact-routes.js +109 -0
- package/dist/backend/api/message-routes.js +90 -0
- package/dist/backend/api/project-routes.js +17 -0
- package/dist/backend/api/session-routes.js +64 -0
- package/dist/backend/api/task-routes.js +30 -0
- package/dist/backend/errors.js +29 -0
- package/dist/backend/runtime/node-pty-runtime.js +162 -0
- package/dist/backend/runtime/session-registry.js +36 -0
- package/dist/backend/runtime/terminal-runtime.js +1 -0
- package/dist/backend/server.js +159 -0
- package/dist/backend/services/artifact-service.js +170 -0
- package/dist/backend/services/command-dispatcher.js +37 -0
- package/dist/backend/services/message-service.js +217 -0
- package/dist/backend/services/project-service.js +71 -0
- package/dist/backend/services/session-service.js +221 -0
- package/dist/backend/services/status-service.js +21 -0
- package/dist/backend/services/task-service.js +88 -0
- package/dist/backend/templates/handoff.js +76 -0
- package/dist/backend/templates/message-envelope.js +27 -0
- package/dist/backend/templates/role-command.js +21 -0
- package/dist/backend/templates/role-messaging-context.js +44 -0
- package/dist/backend/ws/terminal-ws.js +60 -0
- package/dist/cli/vcmctl.js +141 -0
- package/dist/main.js +63 -0
- package/dist/shared/constants.js +45 -0
- package/dist/shared/types/api.js +1 -0
- package/dist/shared/types/artifact.js +1 -0
- package/dist/shared/types/message.js +1 -0
- package/dist/shared/types/project.js +1 -0
- package/dist/shared/types/role.js +1 -0
- package/dist/shared/types/session.js +1 -0
- package/dist/shared/types/task.js +1 -0
- package/dist/shared/types/terminal.js +1 -0
- package/dist/shared/validation/artifact-check.js +64 -0
- package/dist/shared/validation/slug-check.js +22 -0
- package/dist-frontend/assets/index-Bah6k-Ix.css +32 -0
- package/dist-frontend/assets/index-EMaQuIB6.js +58 -0
- package/dist-frontend/index.html +13 -0
- package/docs/cc-best-practices.md +2142 -0
- package/docs/product-design.md +1597 -0
- package/docs/v1-architecture-design.md +1431 -0
- package/docs/v1-implementation-plan.md +1949 -0
- package/docs/v1-message-bus-orchestration-design.md +534 -0
- package/package.json +60 -0
- package/scripts/clean-build.mjs +12 -0
- package/scripts/fix-node-pty-spawn-helper.mjs +31 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ROLE_NAMES, isDispatchableRole } from "../../shared/constants.js";
|
|
4
|
+
import { VcmError } from "../errors.js";
|
|
5
|
+
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
6
|
+
import { renderRoleMessagingContext } from "../templates/role-messaging-context.js";
|
|
7
|
+
export function createSessionService(deps) {
|
|
8
|
+
const now = deps.now ?? (() => new Date().toISOString());
|
|
9
|
+
async function launchRoleSession(repoRoot, taskSlug, role, input, launchMode) {
|
|
10
|
+
const live = deps.registry.getByRole(taskSlug, role);
|
|
11
|
+
if (live && live.status === "running") {
|
|
12
|
+
return live;
|
|
13
|
+
}
|
|
14
|
+
const config = await deps.projectService.loadConfig(repoRoot);
|
|
15
|
+
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
16
|
+
const paths = deps.artifactService.getHandoffPaths(repoRoot, task.handoffDir);
|
|
17
|
+
const persisted = await loadPersistedRoleRecord(deps.fs, repoRoot, config.stateRoot, taskSlug, role);
|
|
18
|
+
const permissionMode = input.permissionMode ?? persisted?.permissionMode ?? "default";
|
|
19
|
+
const claudeSessionId = launchMode === "resume"
|
|
20
|
+
? persisted?.claudeSessionId
|
|
21
|
+
: randomUUID();
|
|
22
|
+
if (!claudeSessionId) {
|
|
23
|
+
throw new VcmError({
|
|
24
|
+
code: "CLAUDE_SESSION_MISSING",
|
|
25
|
+
message: `${role} does not have a Claude session id to resume.`,
|
|
26
|
+
statusCode: 409,
|
|
27
|
+
hint: "Start the role once before using Resume."
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const startCommand = deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, claudeSessionId, launchMode === "resume");
|
|
31
|
+
const runtimeSession = await deps.runtime.createSession({
|
|
32
|
+
taskSlug,
|
|
33
|
+
role,
|
|
34
|
+
command: startCommand.command,
|
|
35
|
+
args: startCommand.args,
|
|
36
|
+
cwd: repoRoot,
|
|
37
|
+
env: {
|
|
38
|
+
VCM_API_URL: deps.apiUrl,
|
|
39
|
+
VCM_CTL_COMMAND: deps.vcmctlCommand,
|
|
40
|
+
VCM_TASK_SLUG: taskSlug,
|
|
41
|
+
VCM_ROLE: role
|
|
42
|
+
},
|
|
43
|
+
cols: input.cols,
|
|
44
|
+
rows: input.rows,
|
|
45
|
+
logPath: resolveRepoPath(repoRoot, paths.roleLogPaths[role])
|
|
46
|
+
});
|
|
47
|
+
const timestamp = now();
|
|
48
|
+
const record = {
|
|
49
|
+
id: runtimeSession.id,
|
|
50
|
+
claudeSessionId,
|
|
51
|
+
taskSlug,
|
|
52
|
+
role,
|
|
53
|
+
status: runtimeSession.status,
|
|
54
|
+
command: startCommand.display,
|
|
55
|
+
permissionMode,
|
|
56
|
+
cwd: repoRoot,
|
|
57
|
+
terminalBackend: "node-pty",
|
|
58
|
+
pid: runtimeSession.pid,
|
|
59
|
+
logPath: paths.roleLogPaths[role],
|
|
60
|
+
roleCommandPath: isDispatchableRole(role)
|
|
61
|
+
? paths.roleCommandPaths[role]
|
|
62
|
+
: undefined,
|
|
63
|
+
handoffArtifactPath: getHandoffArtifactPath(paths, role),
|
|
64
|
+
startedAt: runtimeSession.startedAt,
|
|
65
|
+
updatedAt: timestamp,
|
|
66
|
+
lastOutputAt: runtimeSession.lastOutputAt,
|
|
67
|
+
exitCode: runtimeSession.exitCode
|
|
68
|
+
};
|
|
69
|
+
deps.registry.upsert(record);
|
|
70
|
+
await persistTaskSession(deps.fs, repoRoot, config.stateRoot, record);
|
|
71
|
+
await deps.taskService.updateTaskStatus(repoRoot, taskSlug, "running");
|
|
72
|
+
deps.runtime.write(runtimeSession.id, `${renderRoleMessagingContext(task, paths, role, deps.vcmctlCommand)}\r`);
|
|
73
|
+
return record;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
startRoleSession(repoRoot, taskSlug, role, input = {}) {
|
|
77
|
+
return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
|
|
78
|
+
},
|
|
79
|
+
resumeRoleSession(repoRoot, taskSlug, role, input = {}) {
|
|
80
|
+
return launchRoleSession(repoRoot, taskSlug, role, input, "resume");
|
|
81
|
+
},
|
|
82
|
+
async stopRoleSession(repoRoot, taskSlug, role) {
|
|
83
|
+
const existing = await this.getRoleSession(repoRoot, taskSlug, role);
|
|
84
|
+
if (!existing) {
|
|
85
|
+
throw new VcmError({
|
|
86
|
+
code: "SESSION_MISSING",
|
|
87
|
+
message: `${role} session has not been started.`,
|
|
88
|
+
statusCode: 404
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (deps.runtime.getSession(existing.id)) {
|
|
92
|
+
await deps.runtime.stop(existing.id);
|
|
93
|
+
}
|
|
94
|
+
const updated = {
|
|
95
|
+
...existing,
|
|
96
|
+
status: "exited",
|
|
97
|
+
updatedAt: now()
|
|
98
|
+
};
|
|
99
|
+
deps.registry.upsert(updated);
|
|
100
|
+
const config = await deps.projectService.loadConfig(repoRoot);
|
|
101
|
+
await persistTaskSession(deps.fs, repoRoot, config.stateRoot, updated);
|
|
102
|
+
return updated;
|
|
103
|
+
},
|
|
104
|
+
async restartRoleSession(repoRoot, taskSlug, role, input = {}) {
|
|
105
|
+
const existing = await this.getRoleSession(repoRoot, taskSlug, role);
|
|
106
|
+
if (!existing) {
|
|
107
|
+
return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
|
|
108
|
+
}
|
|
109
|
+
if (deps.runtime.getSession(existing.id)) {
|
|
110
|
+
await deps.runtime.stop(existing.id);
|
|
111
|
+
}
|
|
112
|
+
deps.registry.remove(existing.id);
|
|
113
|
+
return existing.claudeSessionId
|
|
114
|
+
? launchRoleSession(repoRoot, taskSlug, role, input, "resume")
|
|
115
|
+
: launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
|
|
116
|
+
},
|
|
117
|
+
async getRoleSession(repoRoot, taskSlug, role) {
|
|
118
|
+
const config = await deps.projectService.loadConfig(repoRoot);
|
|
119
|
+
const record = deps.registry.getByRole(taskSlug, role)
|
|
120
|
+
?? await loadPersistedRoleRecord(deps.fs, repoRoot, config.stateRoot, taskSlug, role);
|
|
121
|
+
if (!record) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
const runtimeSession = deps.runtime.getSession(record.id);
|
|
125
|
+
if (!runtimeSession) {
|
|
126
|
+
return {
|
|
127
|
+
...record,
|
|
128
|
+
status: getRecoverableStatus(record),
|
|
129
|
+
pid: undefined,
|
|
130
|
+
exitCode: record.exitCode ?? null
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
...record,
|
|
135
|
+
status: runtimeSession.status,
|
|
136
|
+
pid: runtimeSession.pid,
|
|
137
|
+
lastOutputAt: runtimeSession.lastOutputAt,
|
|
138
|
+
exitCode: runtimeSession.exitCode
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
async listRoleSessions(repoRoot, taskSlug) {
|
|
142
|
+
const sessions = [];
|
|
143
|
+
for (const role of ROLE_NAMES) {
|
|
144
|
+
const session = await this.getRoleSession(repoRoot, taskSlug, role);
|
|
145
|
+
if (session) {
|
|
146
|
+
sessions.push(session);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return sessions;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function getRecoverableStatus(record) {
|
|
154
|
+
if (!record.claudeSessionId) {
|
|
155
|
+
return record.status === "running" ? "missing" : record.status;
|
|
156
|
+
}
|
|
157
|
+
if (record.status === "done") {
|
|
158
|
+
return "done";
|
|
159
|
+
}
|
|
160
|
+
return "resumable";
|
|
161
|
+
}
|
|
162
|
+
function getHandoffArtifactPath(paths, role) {
|
|
163
|
+
if (role === "architect") {
|
|
164
|
+
return paths.architecturePlanPath;
|
|
165
|
+
}
|
|
166
|
+
if (role === "coder") {
|
|
167
|
+
return paths.implementationLogPath;
|
|
168
|
+
}
|
|
169
|
+
if (role === "reviewer") {
|
|
170
|
+
return paths.reviewReportPath;
|
|
171
|
+
}
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
async function loadPersistedRoleRecord(fs, repoRoot, stateRoot, taskSlug, role) {
|
|
175
|
+
const sessionPath = getTaskSessionPath(repoRoot, stateRoot, taskSlug);
|
|
176
|
+
if (!(await fs.pathExists(sessionPath))) {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
const current = await fs.readJson(sessionPath);
|
|
180
|
+
return current.roles[role]?.record;
|
|
181
|
+
}
|
|
182
|
+
async function persistTaskSession(fs, repoRoot, stateRoot, session) {
|
|
183
|
+
const sessionPath = getTaskSessionPath(repoRoot, stateRoot, session.taskSlug);
|
|
184
|
+
const empty = createEmptyTaskSessionRecord(session.taskSlug, session.updatedAt);
|
|
185
|
+
const current = await fs.pathExists(sessionPath)
|
|
186
|
+
? await fs.readJson(sessionPath)
|
|
187
|
+
: empty;
|
|
188
|
+
const record = {
|
|
189
|
+
...session,
|
|
190
|
+
updatedAt: session.updatedAt
|
|
191
|
+
};
|
|
192
|
+
await fs.writeJsonAtomic(sessionPath, {
|
|
193
|
+
...current,
|
|
194
|
+
updatedAt: session.updatedAt,
|
|
195
|
+
roles: {
|
|
196
|
+
...current.roles,
|
|
197
|
+
[session.role]: {
|
|
198
|
+
id: session.id,
|
|
199
|
+
claudeSessionId: session.claudeSessionId,
|
|
200
|
+
status: session.status,
|
|
201
|
+
record
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
function createEmptyTaskSessionRecord(taskSlug, updatedAt) {
|
|
207
|
+
return {
|
|
208
|
+
version: 1,
|
|
209
|
+
taskSlug,
|
|
210
|
+
updatedAt,
|
|
211
|
+
roles: {
|
|
212
|
+
"project-manager": { id: null, status: "not_started" },
|
|
213
|
+
architect: { id: null, status: "not_started" },
|
|
214
|
+
coder: { id: null, status: "not_started" },
|
|
215
|
+
reviewer: { id: null, status: "not_started" }
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function getTaskSessionPath(repoRoot, stateRoot, taskSlug) {
|
|
220
|
+
return path.join(repoRoot, stateRoot, "sessions", `${taskSlug}.json`);
|
|
221
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function createStatusService(deps) {
|
|
2
|
+
return {
|
|
3
|
+
async getTaskStatus(repoRoot, taskSlug) {
|
|
4
|
+
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
5
|
+
const artifacts = await deps.artifactService.listArtifacts({
|
|
6
|
+
repoRoot,
|
|
7
|
+
handoffDir: task.handoffDir
|
|
8
|
+
});
|
|
9
|
+
const sessions = await deps.sessionService.listRoleSessions(repoRoot, taskSlug);
|
|
10
|
+
const warnings = artifacts.checks
|
|
11
|
+
.filter((check) => check.status !== "ok")
|
|
12
|
+
.map((check) => `${check.path}: ${check.status}`);
|
|
13
|
+
return {
|
|
14
|
+
task,
|
|
15
|
+
sessions,
|
|
16
|
+
artifacts,
|
|
17
|
+
warnings
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { assertValidTaskSlug } from "../../shared/validation/slug-check.js";
|
|
3
|
+
import { VcmError } from "../errors.js";
|
|
4
|
+
export function createTaskService(deps) {
|
|
5
|
+
const now = deps.now ?? (() => new Date().toISOString());
|
|
6
|
+
return {
|
|
7
|
+
async createTask(repoRoot, input) {
|
|
8
|
+
assertValidTaskSlug(input.taskSlug);
|
|
9
|
+
const config = await deps.projectService.loadConfig(repoRoot);
|
|
10
|
+
const taskPath = getTaskPath(repoRoot, config.stateRoot, input.taskSlug);
|
|
11
|
+
if (await deps.fs.pathExists(taskPath)) {
|
|
12
|
+
throw new VcmError({
|
|
13
|
+
code: "TASK_EXISTS",
|
|
14
|
+
message: `Task already exists: ${input.taskSlug}`,
|
|
15
|
+
statusCode: 409
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const timestamp = now();
|
|
19
|
+
const task = {
|
|
20
|
+
version: 1,
|
|
21
|
+
taskSlug: input.taskSlug,
|
|
22
|
+
title: input.title,
|
|
23
|
+
createdAt: timestamp,
|
|
24
|
+
updatedAt: timestamp,
|
|
25
|
+
repoRoot,
|
|
26
|
+
branch: await deps.git.getCurrentBranch(repoRoot),
|
|
27
|
+
handoffDir: path.posix.join(config.handoffRoot, input.taskSlug),
|
|
28
|
+
status: "created",
|
|
29
|
+
specPath: input.specPath
|
|
30
|
+
};
|
|
31
|
+
await deps.artifactService.ensureHandoffStructure({
|
|
32
|
+
repoRoot,
|
|
33
|
+
taskSlug: input.taskSlug,
|
|
34
|
+
handoffDir: task.handoffDir
|
|
35
|
+
});
|
|
36
|
+
await deps.artifactService.createArtifactTemplates({
|
|
37
|
+
repoRoot,
|
|
38
|
+
taskSlug: input.taskSlug,
|
|
39
|
+
handoffDir: task.handoffDir
|
|
40
|
+
});
|
|
41
|
+
await this.saveTask(repoRoot, task);
|
|
42
|
+
return task;
|
|
43
|
+
},
|
|
44
|
+
async listTasks(repoRoot) {
|
|
45
|
+
const config = await deps.projectService.loadConfig(repoRoot);
|
|
46
|
+
const tasksDir = path.join(repoRoot, config.stateRoot, "tasks");
|
|
47
|
+
if (!(await deps.fs.pathExists(tasksDir))) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
const entries = await deps.fs.readDir(tasksDir);
|
|
51
|
+
const tasks = [];
|
|
52
|
+
for (const entry of entries.filter((candidate) => candidate.endsWith(".json"))) {
|
|
53
|
+
tasks.push(await deps.fs.readJson(path.join(tasksDir, entry)));
|
|
54
|
+
}
|
|
55
|
+
return tasks.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
56
|
+
},
|
|
57
|
+
async loadTask(repoRoot, taskSlug) {
|
|
58
|
+
assertValidTaskSlug(taskSlug);
|
|
59
|
+
const config = await deps.projectService.loadConfig(repoRoot);
|
|
60
|
+
const taskPath = getTaskPath(repoRoot, config.stateRoot, taskSlug);
|
|
61
|
+
if (!(await deps.fs.pathExists(taskPath))) {
|
|
62
|
+
throw new VcmError({
|
|
63
|
+
code: "TASK_MISSING",
|
|
64
|
+
message: `Task does not exist: ${taskSlug}`,
|
|
65
|
+
statusCode: 404
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return deps.fs.readJson(taskPath);
|
|
69
|
+
},
|
|
70
|
+
async saveTask(repoRoot, task) {
|
|
71
|
+
const config = await deps.projectService.loadConfig(repoRoot);
|
|
72
|
+
await deps.fs.writeJsonAtomic(getTaskPath(repoRoot, config.stateRoot, task.taskSlug), task);
|
|
73
|
+
},
|
|
74
|
+
async updateTaskStatus(repoRoot, taskSlug, status) {
|
|
75
|
+
const task = await this.loadTask(repoRoot, taskSlug);
|
|
76
|
+
const updated = {
|
|
77
|
+
...task,
|
|
78
|
+
status,
|
|
79
|
+
updatedAt: now()
|
|
80
|
+
};
|
|
81
|
+
await this.saveTask(repoRoot, updated);
|
|
82
|
+
return updated;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function getTaskPath(repoRoot, stateRoot, taskSlug) {
|
|
87
|
+
return path.join(repoRoot, stateRoot, "tasks", `${taskSlug}.json`);
|
|
88
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export function renderArchitecturePlanTemplate(taskSlug) {
|
|
2
|
+
return `# Architecture Plan: ${taskSlug}
|
|
3
|
+
|
|
4
|
+
## Context
|
|
5
|
+
|
|
6
|
+
TBD
|
|
7
|
+
|
|
8
|
+
## Architecture Decision
|
|
9
|
+
|
|
10
|
+
TBD
|
|
11
|
+
|
|
12
|
+
## Implementation Plan
|
|
13
|
+
|
|
14
|
+
TBD
|
|
15
|
+
|
|
16
|
+
## Risks
|
|
17
|
+
|
|
18
|
+
TBD
|
|
19
|
+
|
|
20
|
+
## Stop Conditions
|
|
21
|
+
|
|
22
|
+
TBD
|
|
23
|
+
`;
|
|
24
|
+
}
|
|
25
|
+
export function renderImplementationLogTemplate(taskSlug) {
|
|
26
|
+
return `# Implementation Log: ${taskSlug}
|
|
27
|
+
|
|
28
|
+
## Summary
|
|
29
|
+
|
|
30
|
+
TBD
|
|
31
|
+
|
|
32
|
+
## Files Changed
|
|
33
|
+
|
|
34
|
+
TBD
|
|
35
|
+
|
|
36
|
+
## Validation
|
|
37
|
+
|
|
38
|
+
TBD
|
|
39
|
+
|
|
40
|
+
## Deviations From Architecture Plan
|
|
41
|
+
|
|
42
|
+
TBD
|
|
43
|
+
|
|
44
|
+
## Follow-ups
|
|
45
|
+
|
|
46
|
+
TBD
|
|
47
|
+
`;
|
|
48
|
+
}
|
|
49
|
+
export function renderValidationLogTemplate(taskSlug) {
|
|
50
|
+
return `# Validation Log: ${taskSlug}
|
|
51
|
+
|
|
52
|
+
## Validation
|
|
53
|
+
|
|
54
|
+
Not run yet.
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
export function renderReviewReportTemplate(taskSlug) {
|
|
58
|
+
return `# Review Report: ${taskSlug}
|
|
59
|
+
|
|
60
|
+
## Summary
|
|
61
|
+
|
|
62
|
+
TBD
|
|
63
|
+
|
|
64
|
+
## Findings
|
|
65
|
+
|
|
66
|
+
TBD
|
|
67
|
+
|
|
68
|
+
## Validation
|
|
69
|
+
|
|
70
|
+
TBD
|
|
71
|
+
|
|
72
|
+
## Decision
|
|
73
|
+
|
|
74
|
+
TBD
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function renderMessageEnvelope(message) {
|
|
2
|
+
const artifactRefs = message.artifactRefs.length > 0
|
|
3
|
+
? message.artifactRefs.map((artifact) => `- ${artifact}`).join("\n")
|
|
4
|
+
: "- none";
|
|
5
|
+
return `
|
|
6
|
+
[VCM MESSAGE]
|
|
7
|
+
id: ${message.id}
|
|
8
|
+
task: ${message.taskSlug}
|
|
9
|
+
from: ${message.fromRole}
|
|
10
|
+
to: ${message.toRole}
|
|
11
|
+
type: ${message.type}
|
|
12
|
+
|
|
13
|
+
${message.body}
|
|
14
|
+
|
|
15
|
+
Artifact refs:
|
|
16
|
+
${artifactRefs}
|
|
17
|
+
|
|
18
|
+
Instructions:
|
|
19
|
+
- Read the message and execute only within this VCM task.
|
|
20
|
+
- Reply to project-manager with vcmctl reply when complete, blocked, or unclear.
|
|
21
|
+
[/VCM MESSAGE]
|
|
22
|
+
`;
|
|
23
|
+
}
|
|
24
|
+
export function renderManualStagePrompt(message) {
|
|
25
|
+
const target = message.bodyPath ?? `VCM message ${message.id}`;
|
|
26
|
+
return `Read and handle VCM message ${message.id} at ${target}`;
|
|
27
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function renderRoleCommandTemplate(taskSlug, role) {
|
|
2
|
+
return `# ${role} command for ${taskSlug}
|
|
3
|
+
|
|
4
|
+
## Objective
|
|
5
|
+
|
|
6
|
+
TBD
|
|
7
|
+
|
|
8
|
+
## Inputs
|
|
9
|
+
|
|
10
|
+
- Task slug: ${taskSlug}
|
|
11
|
+
|
|
12
|
+
## Expected Output Artifact
|
|
13
|
+
|
|
14
|
+
TBD
|
|
15
|
+
|
|
16
|
+
## Stop Conditions
|
|
17
|
+
|
|
18
|
+
- Stop and ask the user if the task scope is unclear.
|
|
19
|
+
- Stop before making high-risk changes without explicit user approval.
|
|
20
|
+
`;
|
|
21
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function renderRoleMessagingContext(task, paths, role, vcmctlCommand = "vcmctl") {
|
|
2
|
+
if (role === "project-manager") {
|
|
3
|
+
return `VCM messaging context
|
|
4
|
+
|
|
5
|
+
Task slug: ${task.taskSlug}
|
|
6
|
+
Canonical handoff directory: ${task.handoffDir}
|
|
7
|
+
|
|
8
|
+
You are the orchestration hub for this task.
|
|
9
|
+
|
|
10
|
+
Use VCM messaging instead of asking the user to copy commands:
|
|
11
|
+
- Send work to architect/coder/reviewer with: ${vcmctlCommand} send --to <role> --type task --body-file <file>
|
|
12
|
+
- Ask a question with: ${vcmctlCommand} send --to <role> --type question --body "..."
|
|
13
|
+
- Check pending messages with: ${vcmctlCommand} inbox
|
|
14
|
+
|
|
15
|
+
Canonical role command files still exist for durable handoff:
|
|
16
|
+
- architect: ${paths.roleCommandPaths.architect}
|
|
17
|
+
- coder: ${paths.roleCommandPaths.coder}
|
|
18
|
+
- reviewer: ${paths.roleCommandPaths.reviewer}
|
|
19
|
+
|
|
20
|
+
Hard rules:
|
|
21
|
+
- Use only ${task.handoffDir} for this task.
|
|
22
|
+
- Do not create or write .ai/handoffs/<other-task>/ for this task.
|
|
23
|
+
- Non-trivial blockers or high-risk decisions must be escalated to the user.
|
|
24
|
+
- In manual orchestration mode, sent messages wait for user approval before the target role executes them.
|
|
25
|
+
`;
|
|
26
|
+
}
|
|
27
|
+
return `VCM messaging context
|
|
28
|
+
|
|
29
|
+
Task slug: ${task.taskSlug}
|
|
30
|
+
Canonical handoff directory: ${task.handoffDir}
|
|
31
|
+
Current role: ${role}
|
|
32
|
+
|
|
33
|
+
When complete, blocked, or unclear, reply to project-manager through VCM:
|
|
34
|
+
- ${vcmctlCommand} reply --type result --body-file <file>
|
|
35
|
+
- ${vcmctlCommand} reply --type blocked --body "..."
|
|
36
|
+
- ${vcmctlCommand} reply --type question --body "..."
|
|
37
|
+
|
|
38
|
+
Hard rules:
|
|
39
|
+
- Only reply to project-manager. Do not message other roles directly.
|
|
40
|
+
- Use only ${task.handoffDir} for this task.
|
|
41
|
+
- Do not create or write .ai/handoffs/<other-task>/ for this task.
|
|
42
|
+
- In manual orchestration mode, your replies wait for user approval before project-manager receives them.
|
|
43
|
+
`;
|
|
44
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { WebSocketServer } from "ws";
|
|
2
|
+
import { toVcmError } from "../errors.js";
|
|
3
|
+
export function registerTerminalWs(app, deps) {
|
|
4
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
5
|
+
app.server.on("upgrade", (request, socket, head) => {
|
|
6
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
7
|
+
const match = /^\/ws\/terminal\/([^/]+)$/.exec(url.pathname);
|
|
8
|
+
if (!match) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
12
|
+
bindTerminalSocket(ws, decodeURIComponent(match[1] ?? ""), deps.runtime);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function bindTerminalSocket(ws, sessionId, runtime) {
|
|
17
|
+
let unsubscribe = () => { };
|
|
18
|
+
try {
|
|
19
|
+
unsubscribe = runtime.subscribe(sessionId, (event) => {
|
|
20
|
+
if (event.type === "output") {
|
|
21
|
+
send(ws, { type: "output", data: event.data ?? "" });
|
|
22
|
+
}
|
|
23
|
+
else if (event.type === "exit") {
|
|
24
|
+
send(ws, { type: "exit", exitCode: event.exitCode ?? null });
|
|
25
|
+
}
|
|
26
|
+
else if (event.type === "status" && event.status) {
|
|
27
|
+
send(ws, { type: "status", status: event.status });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
const vcmError = toVcmError(error);
|
|
33
|
+
send(ws, { type: "error", message: vcmError.message });
|
|
34
|
+
ws.close();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
ws.on("message", (raw) => {
|
|
38
|
+
try {
|
|
39
|
+
const message = JSON.parse(raw.toString());
|
|
40
|
+
if (message.type === "input") {
|
|
41
|
+
runtime.write(sessionId, message.data);
|
|
42
|
+
}
|
|
43
|
+
else if (message.type === "resize") {
|
|
44
|
+
runtime.resize(sessionId, message.cols, message.rows);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
const vcmError = toVcmError(error);
|
|
49
|
+
send(ws, { type: "error", message: vcmError.message });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
ws.on("close", () => {
|
|
53
|
+
unsubscribe();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function send(ws, message) {
|
|
57
|
+
if (ws.readyState === ws.OPEN) {
|
|
58
|
+
ws.send(JSON.stringify(message));
|
|
59
|
+
}
|
|
60
|
+
}
|