vibe-coding-master 0.2.1 → 0.2.2
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 +15 -8
- package/dist/backend/adapters/claude-adapter.js +0 -3
- package/dist/backend/adapters/filesystem.js +14 -0
- package/dist/backend/api/round-routes.js +1 -10
- package/dist/backend/api/translation-routes.js +8 -0
- package/dist/backend/runtime/node-pty-runtime.js +21 -1
- package/dist/backend/server.js +9 -9
- package/dist/backend/services/app-settings-service.js +4 -1
- package/dist/backend/services/claude-hook-service.js +36 -0
- package/dist/backend/services/harness-service.js +0 -3
- package/dist/backend/services/round-service.js +309 -57
- package/dist/backend/services/session-service.js +14 -2
- package/dist/backend/services/translation-service.js +280 -13
- package/dist/backend/templates/harness/reviewer-agent.js +2 -0
- package/dist/shared/types/translation.js +1 -0
- package/dist-frontend/assets/index-CfH8XNnx.js +90 -0
- package/dist-frontend/assets/index-CfqduxVB.css +32 -0
- package/dist-frontend/index.html +2 -2
- package/docs/product-design.md +35 -16
- package/package.json +1 -1
- package/scripts/install-vcm-harness.mjs +2 -0
- package/dist-frontend/assets/index-BmpJxnNx.css +0 -32
- package/dist-frontend/assets/index-CGUkhVAP.js +0 -90
package/README.md
CHANGED
|
@@ -22,7 +22,6 @@ Each role runs as a real Claude Code process inside an embedded terminal. The GU
|
|
|
22
22
|
- Permission mode selection before start, resume, or restart:
|
|
23
23
|
- `default`
|
|
24
24
|
- `bypassPermissions`
|
|
25
|
-
- `--dangerously-skip-permissions`
|
|
26
25
|
- PM-mediated role messaging through VCM-dispatched route files.
|
|
27
26
|
- Manual and automatic orchestration modes.
|
|
28
27
|
- Two-stage VCM harness setup: deterministic fixed install plus AI-assisted bootstrap.
|
|
@@ -188,18 +187,20 @@ The left sidebar is intentionally compact and collapsible:
|
|
|
188
187
|
|
|
189
188
|
- `Repository Path`: path input on one row; `Recent` and `Connect` on the next row.
|
|
190
189
|
- `Repository`: connected path, branch, and working tree state. `Working tree: uncommitted changes` means `git status --porcelain` is not empty.
|
|
191
|
-
- `Settings`: `Theme`, `
|
|
190
|
+
- `Settings`: `Theme`, `Flow pause alert`, `Try alert`, `Messages`, and `Events`.
|
|
192
191
|
- `VCM Harness`: fixed-install status, bootstrap completion checks, and the bootstrap terminal when one is running.
|
|
193
192
|
- `New Task`: one `task name` input.
|
|
194
193
|
- `Tasks`: task list and task status.
|
|
195
194
|
|
|
196
195
|
All sidebar sections are collapsed by default. When no task is selected, `Repository Path` opens by default.
|
|
197
196
|
|
|
197
|
+
When VCM is connected to an active task, the bottom of the sidebar shows a task status dock. It stays outside the collapsible groups and shows the task title, task status, start time, total elapsed time, total flow-round count, and Claude Code active runtime. If a flow round is currently active or settling, the dock also shows that round's start time and Claude Code active runtime.
|
|
198
|
+
|
|
198
199
|
## Translation
|
|
199
200
|
|
|
200
201
|
The task header has a global `Translate` button next to `Close Task`. It opens a translation panel beside the embedded terminal for the role consoles and keeps the same on/off setting while switching roles. The terminal and translation panel split the available width evenly.
|
|
201
202
|
|
|
202
|
-
The task header does not include a manual `Refresh` button. Task status, role status, messages, orchestration state, and
|
|
203
|
+
The task header does not include a manual `Refresh` button. Task status, role status, messages, orchestration state, and flow pause state refresh automatically. The remaining `Refresh` button lives only in the sidebar `VCM Harness` section and is for rechecking harness files.
|
|
203
204
|
|
|
204
205
|
Translation settings are local and stored in:
|
|
205
206
|
|
|
@@ -211,7 +212,7 @@ The same file stores recent repository paths. The translation API key is stored
|
|
|
211
212
|
|
|
212
213
|
The sidebar `Settings` section also stores the UI theme preference in this file. The default is `system`, which follows the OS/browser color-scheme preference; users can cycle between `System`, `Light`, and `Dark`.
|
|
213
214
|
|
|
214
|
-
The same sidebar also has a `
|
|
215
|
+
The same sidebar also has a `Flow pause alert` toggle. It is on by default and controls the local alert that fires when VCM detects that the current role flow has stopped advancing. Short flows use a weak reminder: the soft two-note chime plays 3 times, 1.4 seconds apart. Flows lasting 10 minutes or longer use a strong reminder: VCM shows an alert dialog and repeats the chime until the user confirms it. The `Try alert` button always triggers the strong reminder for testing.
|
|
215
216
|
|
|
216
217
|
Translation behavior:
|
|
217
218
|
|
|
@@ -222,6 +223,8 @@ Translation behavior:
|
|
|
222
223
|
- VCM tails those transcript files in the backend. Closing the translation panel does not stop capture; the tailer stops only when the role session is stopped/restarted or the task is closed.
|
|
223
224
|
- Translation events are cached under the task runtime repo at `.ai/vcm/translation/<task>/<role>/<session-id>.jsonl` and delivered to the frontend through HTTP polling.
|
|
224
225
|
- The polling cursor is the next expected seq: `after=18` acknowledges seq `1..17` and returns seq `18+`; there is no snapshot mismatch error.
|
|
226
|
+
- The translation panel retains the most recent 500 entries per role session in frontend/backend memory. Older entries are pruned from the live panel state and event cache to keep long sessions responsive.
|
|
227
|
+
- Failed output translations are tracked in a backend failure list. When failures exist, the panel shows `Ignore N` and `Retry N`; retry reuses the original entry id so the failed row is replaced by the normal translating/translated flow. If an old failed entry is pruned by the 500-entry cap, its failure-list item is removed too.
|
|
225
228
|
- Assistant prose is shown as English source while translating, then replaced by the translated Chinese result.
|
|
226
229
|
- Assistant prose renders Markdown in the panel, including headings, lists, code fences, tables, and links.
|
|
227
230
|
- Tool calls and tool results are preserved as dim one-line rows such as `● Bash({"command":"npm test"})`.
|
|
@@ -369,13 +372,15 @@ VCM Harness injects Claude Code `UserPromptSubmit` and `Stop` hooks into `.claud
|
|
|
369
372
|
|
|
370
373
|
The implementation keeps only the active manual/auto orchestration mode. It does not expose pause/resume, stage/approve/reject, or a separate agent-facing message CLI.
|
|
371
374
|
|
|
372
|
-
##
|
|
375
|
+
## Flow Pause Alerts
|
|
376
|
+
|
|
377
|
+
VCM detects flow pauses from Claude Code hook events, not PTY silence, message history, or pending route files. `UserPromptSubmit` starts a new flow round when none is active, or continues the current round when it fires within the settle window.
|
|
373
378
|
|
|
374
|
-
|
|
379
|
+
When a `Stop` hook fires, VCM marks the round as settling for 10 seconds. If another `UserPromptSubmit` fires inside that window, the same round continues and VCM increments the prompt-submit count. If no new prompt is accepted before the deadline, VCM marks the round as `paused`.
|
|
375
380
|
|
|
376
|
-
|
|
381
|
+
The normal path is timer-driven: `Stop` starts a backend settle timer, and `UserPromptSubmit` cancels it. Reading the round state also checks expired deadlines as a recovery fallback after process restarts, sleep, or missed timers.
|
|
377
382
|
|
|
378
|
-
When `
|
|
383
|
+
When `Flow pause alert` is enabled, the frontend polls the task round state and deduplicates each pause id. Flow duration is measured from the first `UserPromptSubmit` to the last `Stop`; the 10 second settle window is not counted. If the paused flow lasted less than 10 minutes, it plays the local chime 3 times at 1.4 second intervals. If the paused flow lasted 10 minutes or longer, it shows a modal `Flow paused` alert and repeats the local chime until the user clicks `Confirm`. A pause can mean normal completion, user decision needed, dispatch failure, or another workflow interruption; the point is to get the user to look with the right amount of urgency.
|
|
379
384
|
|
|
380
385
|
## Resume Behavior
|
|
381
386
|
|
|
@@ -392,6 +397,8 @@ Session buttons behave as follows:
|
|
|
392
397
|
- `Restart`: stops the current process if needed, creates a new UUID, and starts a fresh Claude session.
|
|
393
398
|
- `Stop`: stops the embedded terminal process and leaves the persisted Claude session id resumable.
|
|
394
399
|
|
|
400
|
+
Embedded terminal output is still written to raw role log files under `.ai/vcm/handoffs/logs/`. When a browser reconnects to a running terminal, VCM replays only the tail of that log, capped at 2 MB, rather than streaming the entire historical log back into xterm. This keeps long sessions responsive while preserving full logs on disk until task cleanup.
|
|
401
|
+
|
|
395
402
|
## Local Project Files
|
|
396
403
|
|
|
397
404
|
For a connected repository, VCM uses:
|
|
@@ -25,9 +25,6 @@ export function createClaudeAdapter(runner) {
|
|
|
25
25
|
if (permissionMode === "bypassPermissions") {
|
|
26
26
|
args.push("--permission-mode", "bypassPermissions");
|
|
27
27
|
}
|
|
28
|
-
else if (permissionMode === "dangerously-skip-permissions") {
|
|
29
|
-
args.push("--dangerously-skip-permissions");
|
|
30
|
-
}
|
|
31
28
|
return {
|
|
32
29
|
command,
|
|
33
30
|
args,
|
|
@@ -20,6 +20,20 @@ export function createNodeFileSystemAdapter() {
|
|
|
20
20
|
async readText(targetPath) {
|
|
21
21
|
return fs.readFile(targetPath, "utf8");
|
|
22
22
|
},
|
|
23
|
+
async readTextTail(targetPath, maxBytes) {
|
|
24
|
+
const handle = await fs.open(targetPath, "r");
|
|
25
|
+
try {
|
|
26
|
+
const stat = await handle.stat();
|
|
27
|
+
const length = Math.min(Math.max(0, Math.floor(maxBytes)), stat.size);
|
|
28
|
+
const position = stat.size - length;
|
|
29
|
+
const buffer = Buffer.alloc(length);
|
|
30
|
+
await handle.read(buffer, 0, length, position);
|
|
31
|
+
return buffer.toString("utf8");
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
await handle.close();
|
|
35
|
+
}
|
|
36
|
+
},
|
|
23
37
|
async writeText(targetPath, content) {
|
|
24
38
|
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
25
39
|
await fs.writeFile(targetPath, content, "utf8");
|
|
@@ -6,20 +6,11 @@ export function registerRoundRoutes(app, deps) {
|
|
|
6
6
|
const config = await deps.projectService.loadConfig(project.repoRoot);
|
|
7
7
|
const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
8
8
|
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
9
|
-
|
|
10
|
-
const pendingRoutes = await deps.messageService.listPendingRouteFiles({
|
|
11
|
-
repoRoot: project.repoRoot,
|
|
12
|
-
taskRepoRoot,
|
|
9
|
+
return deps.roundService.getTaskRoundState({
|
|
13
10
|
stateRepoRoot: taskRepoRoot,
|
|
14
11
|
stateRoot: config.stateRoot,
|
|
15
|
-
handoffDir: task.handoffDir,
|
|
16
12
|
taskSlug: request.params.taskSlug
|
|
17
13
|
});
|
|
18
|
-
return deps.roundService.getTaskRoundState({
|
|
19
|
-
taskSlug: request.params.taskSlug,
|
|
20
|
-
sessions,
|
|
21
|
-
pendingRouteCount: pendingRoutes.length
|
|
22
|
-
});
|
|
23
14
|
});
|
|
24
15
|
}
|
|
25
16
|
async function requireCurrentProject(projectService) {
|
|
@@ -64,6 +64,14 @@ export function registerTranslationRoutes(app, deps) {
|
|
|
64
64
|
await requireCurrentProject(deps.projectService);
|
|
65
65
|
return deps.translationService.retryTranslation(request.params.sessionId, request.params.translationId);
|
|
66
66
|
});
|
|
67
|
+
app.post("/api/translation/sessions/:sessionId/failures/ignore", async (request) => {
|
|
68
|
+
await requireCurrentProject(deps.projectService);
|
|
69
|
+
return deps.translationService.ignoreTranslationFailures(request.params.sessionId);
|
|
70
|
+
});
|
|
71
|
+
app.post("/api/translation/sessions/:sessionId/failures/retry", async (request) => {
|
|
72
|
+
await requireCurrentProject(deps.projectService);
|
|
73
|
+
return deps.translationService.retryFailedTranslations(request.params.sessionId);
|
|
74
|
+
});
|
|
67
75
|
}
|
|
68
76
|
function parseRole(role) {
|
|
69
77
|
if (!isRoleName(role)) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as pty from "node-pty";
|
|
2
2
|
import { VcmError } from "../errors.js";
|
|
3
|
+
export const TERMINAL_REPLAY_TAIL_LIMIT_BYTES = 2 * 1024 * 1024;
|
|
3
4
|
export function createNodePtyTerminalRuntime(deps) {
|
|
4
5
|
const entries = new Map();
|
|
5
6
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
@@ -113,7 +114,7 @@ export function createNodePtyTerminalRuntime(deps) {
|
|
|
113
114
|
const entry = getEntry(entries, sessionId);
|
|
114
115
|
entry.listeners.add(listener);
|
|
115
116
|
if (options.replay !== false) {
|
|
116
|
-
void deps.fs
|
|
117
|
+
void readTerminalReplayText(deps.fs, entry.input.logPath)
|
|
117
118
|
.then((data) => {
|
|
118
119
|
if (!data || !entry.listeners.has(listener)) {
|
|
119
120
|
return;
|
|
@@ -138,6 +139,25 @@ export function createNodePtyTerminalRuntime(deps) {
|
|
|
138
139
|
}
|
|
139
140
|
};
|
|
140
141
|
}
|
|
142
|
+
async function readTerminalReplayText(fs, logPath) {
|
|
143
|
+
const data = fs.readTextTail
|
|
144
|
+
? await fs.readTextTail(logPath, TERMINAL_REPLAY_TAIL_LIMIT_BYTES)
|
|
145
|
+
: tailTerminalReplay(await fs.readText(logPath));
|
|
146
|
+
return tailTerminalReplay(data);
|
|
147
|
+
}
|
|
148
|
+
export function tailTerminalReplay(data, limitBytes = TERMINAL_REPLAY_TAIL_LIMIT_BYTES) {
|
|
149
|
+
if (limitBytes <= 0 || Buffer.byteLength(data, "utf8") <= limitBytes) {
|
|
150
|
+
return data;
|
|
151
|
+
}
|
|
152
|
+
let start = Math.max(0, data.length - limitBytes);
|
|
153
|
+
let tail = data.slice(start);
|
|
154
|
+
while (Buffer.byteLength(tail, "utf8") > limitBytes && start < data.length) {
|
|
155
|
+
start += Math.max(1, Math.ceil((Buffer.byteLength(tail, "utf8") - limitBytes) / 4));
|
|
156
|
+
tail = data.slice(start);
|
|
157
|
+
}
|
|
158
|
+
const firstLineBreak = tail.indexOf("\n");
|
|
159
|
+
return firstLineBreak >= 0 ? tail.slice(firstLineBreak + 1) : tail;
|
|
160
|
+
}
|
|
141
161
|
export function buildPtyEnvironment(baseEnv, inputEnv = {}) {
|
|
142
162
|
const env = {
|
|
143
163
|
...baseEnv,
|
package/dist/backend/server.js
CHANGED
|
@@ -83,8 +83,6 @@ export async function createServer(deps, options = {}) {
|
|
|
83
83
|
registerRoundRoutes(app, {
|
|
84
84
|
projectService: deps.projectService,
|
|
85
85
|
taskService: deps.taskService,
|
|
86
|
-
sessionService: deps.sessionService,
|
|
87
|
-
messageService: deps.messageService,
|
|
88
86
|
roundService: deps.roundService
|
|
89
87
|
});
|
|
90
88
|
registerTranslationRoutes(app, {
|
|
@@ -164,14 +162,8 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
164
162
|
sessionService,
|
|
165
163
|
taskService
|
|
166
164
|
});
|
|
167
|
-
const
|
|
168
|
-
projectService,
|
|
169
|
-
taskService,
|
|
170
|
-
sessionService,
|
|
171
|
-
messageService
|
|
172
|
-
});
|
|
165
|
+
const roundService = createRoundService({ fs });
|
|
173
166
|
const transcripts = createClaudeTranscriptService();
|
|
174
|
-
const roundService = createRoundService();
|
|
175
167
|
const translationService = createTranslationService({
|
|
176
168
|
runtime,
|
|
177
169
|
sessionRegistry: registry,
|
|
@@ -182,6 +174,14 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
182
174
|
appSettings,
|
|
183
175
|
provider: createOpenAiCompatibleTranslationProvider()
|
|
184
176
|
});
|
|
177
|
+
const claudeHookService = createClaudeHookService({
|
|
178
|
+
projectService,
|
|
179
|
+
taskService,
|
|
180
|
+
sessionService,
|
|
181
|
+
messageService,
|
|
182
|
+
roundService,
|
|
183
|
+
translationService
|
|
184
|
+
});
|
|
185
185
|
return {
|
|
186
186
|
appSettings,
|
|
187
187
|
projectService,
|
|
@@ -179,9 +179,12 @@ function normalizeSettingsFile(input) {
|
|
|
179
179
|
}
|
|
180
180
|
function normalizePreferences(input) {
|
|
181
181
|
const candidate = isObject(input) ? input : {};
|
|
182
|
+
const rawFlowPauseAlerts = "flowPauseAlerts" in candidate
|
|
183
|
+
? candidate.flowPauseAlerts
|
|
184
|
+
: candidate.roundCompletionAlerts;
|
|
182
185
|
return {
|
|
183
186
|
themeMode: normalizeThemeMode(candidate.themeMode),
|
|
184
|
-
|
|
187
|
+
flowPauseAlerts: rawFlowPauseAlerts !== false
|
|
185
188
|
};
|
|
186
189
|
}
|
|
187
190
|
function normalizeThemeMode(input) {
|
|
@@ -42,6 +42,24 @@ export function createClaudeHookService(deps) {
|
|
|
42
42
|
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
43
43
|
cwd: stringOrUndefined(input.event.cwd)
|
|
44
44
|
});
|
|
45
|
+
await deps.roundService.recordClaudeHookEvent({
|
|
46
|
+
stateRepoRoot: context.taskRepoRoot,
|
|
47
|
+
stateRoot: context.config.stateRoot,
|
|
48
|
+
taskSlug: input.taskSlug,
|
|
49
|
+
role: input.role,
|
|
50
|
+
eventName
|
|
51
|
+
});
|
|
52
|
+
if (session) {
|
|
53
|
+
await deps.translationService.recordConversationBoundary({
|
|
54
|
+
repoRoot: context.project.repoRoot,
|
|
55
|
+
taskRepoRoot: context.taskRepoRoot,
|
|
56
|
+
taskSlug: input.taskSlug,
|
|
57
|
+
role: input.role,
|
|
58
|
+
sessionId: session.id,
|
|
59
|
+
boundaryKind: "start",
|
|
60
|
+
occurredAt: session.lastPromptSubmittedAt ?? session.updatedAt
|
|
61
|
+
});
|
|
62
|
+
}
|
|
45
63
|
const submitted = await deps.messageService.confirmPromptSubmitted({
|
|
46
64
|
repoRoot: context.project.repoRoot,
|
|
47
65
|
taskRepoRoot: context.taskRepoRoot,
|
|
@@ -76,6 +94,24 @@ export function createClaudeHookService(deps) {
|
|
|
76
94
|
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
77
95
|
cwd: stringOrUndefined(input.event.cwd)
|
|
78
96
|
});
|
|
97
|
+
await deps.roundService.recordClaudeHookEvent({
|
|
98
|
+
stateRepoRoot: context.taskRepoRoot,
|
|
99
|
+
stateRoot: context.config.stateRoot,
|
|
100
|
+
taskSlug: input.taskSlug,
|
|
101
|
+
role: input.role,
|
|
102
|
+
eventName
|
|
103
|
+
});
|
|
104
|
+
if (session) {
|
|
105
|
+
await deps.translationService.recordConversationBoundary({
|
|
106
|
+
repoRoot: context.project.repoRoot,
|
|
107
|
+
taskRepoRoot: context.taskRepoRoot,
|
|
108
|
+
taskSlug: input.taskSlug,
|
|
109
|
+
role: input.role,
|
|
110
|
+
sessionId: session.id,
|
|
111
|
+
boundaryKind: "end",
|
|
112
|
+
occurredAt: session.lastStopAt ?? session.updatedAt
|
|
113
|
+
});
|
|
114
|
+
}
|
|
79
115
|
const dispatched = await deps.messageService.scanAndDispatchPendingRouteFiles({
|
|
80
116
|
repoRoot: context.project.repoRoot,
|
|
81
117
|
taskRepoRoot: context.taskRepoRoot,
|
|
@@ -817,9 +817,6 @@ function buildClaudeStartCommand(command = "claude", permissionMode = "default",
|
|
|
817
817
|
if (permissionMode === "bypassPermissions") {
|
|
818
818
|
args.push("--permission-mode", "bypassPermissions");
|
|
819
819
|
}
|
|
820
|
-
else if (permissionMode === "dangerously-skip-permissions") {
|
|
821
|
-
args.push("--dangerously-skip-permissions");
|
|
822
|
-
}
|
|
823
820
|
return {
|
|
824
821
|
command,
|
|
825
822
|
args,
|