vibe-coding-master 0.0.12 → 0.0.13

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.
@@ -0,0 +1,227 @@
1
+ import { ROLE_NAMES } from "../../shared/constants.js";
2
+ export function createRoundService(deps) {
3
+ const now = deps.now ?? (() => new Date().toISOString());
4
+ const debounceMs = deps.debounceMs ?? 1500;
5
+ const trackers = new Map();
6
+ function ensureTracker(session) {
7
+ if (session.status !== "running") {
8
+ stopSession(session.id);
9
+ return;
10
+ }
11
+ if (trackers.has(session.id)) {
12
+ return;
13
+ }
14
+ const tracker = {
15
+ role: session.role,
16
+ sessionId: session.id,
17
+ taskSlug: session.taskSlug,
18
+ status: "unknown",
19
+ pendingToolUseIds: new Set(),
20
+ unsubscribe: () => { }
21
+ };
22
+ tracker.unsubscribe = deps.transcripts.subscribeToRoleSession(session, (event) => {
23
+ handleTranscriptEvent(tracker, event);
24
+ }, {
25
+ onError(error) {
26
+ tracker.status = "unknown";
27
+ tracker.reason = error.message;
28
+ }
29
+ });
30
+ trackers.set(session.id, tracker);
31
+ }
32
+ function handleTranscriptEvent(tracker, event) {
33
+ tracker.lastActivityAt = event.timestamp;
34
+ clearIdleTimer(tracker);
35
+ if (event.kind === "tool_use") {
36
+ tracker.pendingToolUseIds.add(event.id);
37
+ tracker.status = "using_tools";
38
+ tracker.reason = undefined;
39
+ return;
40
+ }
41
+ if (event.kind === "tool_result") {
42
+ tracker.pendingToolUseIds.delete(event.toolResult.tool_use_id);
43
+ tracker.status = tracker.pendingToolUseIds.size > 0 ? "using_tools" : "answering";
44
+ tracker.reason = undefined;
45
+ return;
46
+ }
47
+ if (event.kind === "question") {
48
+ tracker.status = "waiting_user";
49
+ tracker.reason = "Claude Code is asking for user input.";
50
+ return;
51
+ }
52
+ if (event.kind === "agent" || event.kind === "todo") {
53
+ tracker.status = "answering";
54
+ tracker.reason = undefined;
55
+ return;
56
+ }
57
+ if (event.kind === "thinking") {
58
+ tracker.status = "answering";
59
+ tracker.reason = undefined;
60
+ return;
61
+ }
62
+ if (event.kind === "text") {
63
+ if (event.stopReason === "max_tokens" || event.stopReason === "stop_sequence" || event.stopReason === "refusal") {
64
+ tracker.status = "abnormal";
65
+ tracker.reason = `Claude Code stopped with ${event.stopReason}.`;
66
+ return;
67
+ }
68
+ tracker.status = "answering";
69
+ tracker.reason = undefined;
70
+ if (event.stopReason === "end_turn" && tracker.pendingToolUseIds.size === 0 && event.text.trim()) {
71
+ scheduleIdle(tracker, event.timestamp);
72
+ }
73
+ }
74
+ }
75
+ function scheduleIdle(tracker, endedAt) {
76
+ tracker.pendingAnswerEndedAt = endedAt;
77
+ tracker.idleTimer = setTimeout(() => {
78
+ if (tracker.pendingToolUseIds.size > 0 || tracker.pendingAnswerEndedAt !== endedAt) {
79
+ return;
80
+ }
81
+ tracker.status = "idle";
82
+ tracker.lastAnswerEndedAt = endedAt;
83
+ tracker.reason = undefined;
84
+ tracker.idleTimer = undefined;
85
+ }, debounceMs);
86
+ }
87
+ function clearIdleTimer(tracker) {
88
+ tracker.pendingAnswerEndedAt = undefined;
89
+ if (tracker.idleTimer) {
90
+ clearTimeout(tracker.idleTimer);
91
+ tracker.idleTimer = undefined;
92
+ }
93
+ }
94
+ function stopSession(sessionId) {
95
+ const tracker = trackers.get(sessionId);
96
+ if (!tracker) {
97
+ return;
98
+ }
99
+ clearIdleTimer(tracker);
100
+ tracker.unsubscribe();
101
+ trackers.delete(sessionId);
102
+ }
103
+ return {
104
+ getTaskRoundState(input) {
105
+ const liveSessionIds = new Set(input.sessions.filter((session) => session.status === "running").map((session) => session.id));
106
+ for (const session of input.sessions) {
107
+ ensureTracker(session);
108
+ }
109
+ for (const [sessionId, tracker] of trackers) {
110
+ if (tracker.taskSlug === input.taskSlug && !liveSessionIds.has(sessionId)) {
111
+ stopSession(sessionId);
112
+ }
113
+ }
114
+ const roleStates = ROLE_NAMES.map((role) => toRoleTurnState(role, input.sessions, trackers));
115
+ const latestDelivered = getLatestDeliveredMessage(input.messages);
116
+ const queuedMessageCount = input.messages.filter((message) => message.status === "queued").length;
117
+ const pendingMessageCount = input.messages.filter((message) => message.status === "pending_approval").length;
118
+ const state = evaluateTaskRoundState({
119
+ taskSlug: input.taskSlug,
120
+ messages: input.messages,
121
+ roleStates,
122
+ updatedAt: now()
123
+ });
124
+ return {
125
+ ...state,
126
+ latestMessageId: latestDelivered?.id,
127
+ latestMessageDeliveredAt: getMessageDeliveredAt(latestDelivered),
128
+ queuedMessageCount,
129
+ pendingMessageCount
130
+ };
131
+ },
132
+ stopSession,
133
+ stopTask(taskSlug) {
134
+ for (const [sessionId, tracker] of trackers) {
135
+ if (tracker.taskSlug === taskSlug) {
136
+ stopSession(sessionId);
137
+ }
138
+ }
139
+ }
140
+ };
141
+ }
142
+ export function evaluateTaskRoundState(input) {
143
+ const latestDelivered = getLatestDeliveredMessage(input.messages);
144
+ const hasQueuedOrPending = input.messages.some((message) => message.status === "queued" || message.status === "pending_approval");
145
+ if (latestDelivered) {
146
+ const deliveredAt = getMessageDeliveredAt(latestDelivered);
147
+ const targetState = input.roleStates.find((roleState) => roleState.role === latestDelivered.toRole);
148
+ const completedAt = targetState && deliveredAt
149
+ ? getCompletedAtForDelivery(targetState, deliveredAt)
150
+ : undefined;
151
+ if (completedAt && !hasQueuedOrPending) {
152
+ return {
153
+ taskSlug: input.taskSlug,
154
+ status: "completed",
155
+ activeRole: latestDelivered.toRole,
156
+ completionId: `${latestDelivered.id}:${completedAt}`,
157
+ completedAt,
158
+ roles: input.roleStates,
159
+ updatedAt: input.updatedAt
160
+ };
161
+ }
162
+ return {
163
+ taskSlug: input.taskSlug,
164
+ status: targetState?.status === "waiting_user" ? "waiting_user" : "active",
165
+ activeRole: latestDelivered.toRole,
166
+ roles: input.roleStates,
167
+ updatedAt: input.updatedAt
168
+ };
169
+ }
170
+ const latestIdleRole = !hasQueuedOrPending ? getLatestIdleRole(input.roleStates) : undefined;
171
+ if (latestIdleRole?.lastAnswerEndedAt) {
172
+ return {
173
+ taskSlug: input.taskSlug,
174
+ status: "completed",
175
+ activeRole: latestIdleRole.role,
176
+ completionId: `direct:${latestIdleRole.role}:${latestIdleRole.lastAnswerEndedAt}`,
177
+ completedAt: latestIdleRole.lastAnswerEndedAt,
178
+ roles: input.roleStates,
179
+ updatedAt: input.updatedAt
180
+ };
181
+ }
182
+ const activeRole = input.roleStates.find((roleState) => roleState.status === "answering" ||
183
+ roleState.status === "using_tools" ||
184
+ roleState.status === "waiting_user" ||
185
+ roleState.status === "abnormal");
186
+ return {
187
+ taskSlug: input.taskSlug,
188
+ status: activeRole?.status === "waiting_user" ? "waiting_user" : activeRole ? "active" : "idle",
189
+ activeRole: activeRole?.role,
190
+ roles: input.roleStates,
191
+ updatedAt: input.updatedAt
192
+ };
193
+ }
194
+ function toRoleTurnState(role, sessions, trackers) {
195
+ const session = sessions.find((candidate) => candidate.role === role && candidate.status === "running");
196
+ const tracker = session ? trackers.get(session.id) : undefined;
197
+ return {
198
+ role,
199
+ sessionId: session?.id,
200
+ status: tracker?.status ?? "unknown",
201
+ pendingToolUseCount: tracker?.pendingToolUseIds.size ?? 0,
202
+ lastActivityAt: tracker?.lastActivityAt,
203
+ lastAnswerEndedAt: tracker?.lastAnswerEndedAt,
204
+ reason: tracker?.reason
205
+ };
206
+ }
207
+ function getLatestDeliveredMessage(messages) {
208
+ return messages
209
+ .filter((message) => message.status === "delivered" || message.status === "staged")
210
+ .sort((left, right) => getMessageDeliveredAt(left).localeCompare(getMessageDeliveredAt(right)))
211
+ .at(-1);
212
+ }
213
+ function getMessageDeliveredAt(message) {
214
+ return message?.deliveredAt ?? message?.stagedAt ?? message?.createdAt ?? "";
215
+ }
216
+ function getCompletedAtForDelivery(roleState, deliveredAt) {
217
+ if (roleState.status !== "idle" || !roleState.lastAnswerEndedAt) {
218
+ return undefined;
219
+ }
220
+ return roleState.lastAnswerEndedAt >= deliveredAt ? roleState.lastAnswerEndedAt : undefined;
221
+ }
222
+ function getLatestIdleRole(roleStates) {
223
+ return roleStates
224
+ .filter((roleState) => roleState.status === "idle" && roleState.lastAnswerEndedAt)
225
+ .sort((left, right) => (left.lastAnswerEndedAt ?? "").localeCompare(right.lastAnswerEndedAt ?? ""))
226
+ .at(-1);
227
+ }
@@ -13,7 +13,7 @@ export function createTaskService(deps) {
13
13
  ? `feature/${input.taskSlug}`
14
14
  : await deps.git.getCurrentBranch(repoRoot);
15
15
  const worktreePath = shouldCreateWorktree
16
- ? getTaskWorktreePath(repoRoot, config.stateRoot, input.taskSlug)
16
+ ? getTaskWorktreePath(repoRoot, input.taskSlug)
17
17
  : undefined;
18
18
  if (await deps.fs.pathExists(taskPath)) {
19
19
  throw new VcmError({
@@ -30,6 +30,14 @@ export function createTaskService(deps) {
30
30
  hint: "Apply VCM Harness first so .gitignore contains the VCM managed block."
31
31
  });
32
32
  }
33
+ if (shouldCreateWorktree && !(await deps.git.isIgnored(repoRoot, ".claude/worktrees/.probe"))) {
34
+ throw new VcmError({
35
+ code: "VCM_WORKTREES_NOT_IGNORED",
36
+ message: ".claude/worktrees/ is not ignored by Git.",
37
+ statusCode: 409,
38
+ hint: "Apply VCM Harness first so .gitignore ignores Claude-compatible task worktrees."
39
+ });
40
+ }
33
41
  if (!shouldCreateWorktree) {
34
42
  const activeInlineTask = await findActiveInlineTask(deps.fs, repoRoot, config.stateRoot);
35
43
  if (activeInlineTask) {
@@ -161,7 +169,7 @@ export function createTaskService(deps) {
161
169
  const removedStatePaths = [];
162
170
  const cleanedAt = now();
163
171
  if (task.worktreePath) {
164
- assertTaskWorktreePath(repoRoot, config.stateRoot, task.worktreePath);
172
+ assertTaskWorktreePath(repoRoot, task.worktreePath);
165
173
  await deps.git.removeWorktree(repoRoot, task.worktreePath, { force: options.force ?? true });
166
174
  }
167
175
  let deletedBranch;
@@ -189,8 +197,8 @@ export function getTaskRuntimeRepoRoot(task) {
189
197
  function getTaskPath(repoRoot, stateRoot, taskSlug) {
190
198
  return path.join(repoRoot, stateRoot, "tasks", `${taskSlug}.json`);
191
199
  }
192
- function getTaskWorktreePath(repoRoot, stateRoot, taskSlug) {
193
- return path.join(repoRoot, stateRoot, "worktrees", taskSlug);
200
+ function getTaskWorktreePath(repoRoot, taskSlug) {
201
+ return path.join(repoRoot, ".claude", "worktrees", taskSlug);
194
202
  }
195
203
  async function ensureTaskRuntimeStateDirs(fs, taskRepoRoot, stateRoot) {
196
204
  await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "sessions"));
@@ -222,8 +230,8 @@ function getTaskStatePaths(baseRepoRoot, taskRepoRoot, stateRoot, handoffRoot, t
222
230
  path.join(taskRepoRoot, handoffRoot)
223
231
  ];
224
232
  }
225
- function assertTaskWorktreePath(repoRoot, stateRoot, worktreePath) {
226
- const worktreeRoot = path.resolve(repoRoot, stateRoot, "worktrees");
233
+ function assertTaskWorktreePath(repoRoot, worktreePath) {
234
+ const worktreeRoot = path.resolve(repoRoot, ".claude", "worktrees");
227
235
  const resolvedWorktreePath = path.resolve(worktreePath);
228
236
  const relative = path.relative(worktreeRoot, resolvedWorktreePath);
229
237
  if (relative.startsWith("..") || path.isAbsolute(relative) || relative === "") {
@@ -8,5 +8,6 @@ export function renderArchitectHarnessRules() {
8
8
  - Update stale architecture/module/testing/security/dependency docs when the final code made them stale.
9
9
  - Write docs-sync-report.md with docs changed, docs intentionally left unchanged, remaining documentation risks, and decision.
10
10
  - Stop and reply to project-manager if implementation drift changes architecture, public contracts, dependency direction, schema, auth, permission, payment, or design assumptions.
11
+ - Reply to project-manager once per received VCM message when complete, blocked, or unclear; do not send fragmented progress updates unless project-manager explicitly requested them.
11
12
  `;
12
13
  }
@@ -8,6 +8,9 @@ export function renderRootClaudeHarnessRules() {
8
8
  - Do not create or write task handoffs outside .ai/vcm/handoffs/ for the current task.
9
9
  - Use vcmctl for role-to-role messaging instead of asking the user to copy prompts.
10
10
  - Non-PM roles only reply to project-manager; they do not message other roles directly.
11
+ - Role messaging is turn-based: do not send more than one active message to the same target role.
12
+ - After sending a message to a role, wait for that role to reply with vcmctl reply or vcmctl result before sending another message to the same role.
13
+ - If new information arrives while a role is still processing, update the relevant handoff artifact or wait; do not spam the target role's terminal.
11
14
  - High-risk decisions involving schema, auth, permissions, payment, billing, security, data deletion, or unclear user intent must stop for project-manager/user approval.
12
15
  - Required workflow gates: architect plan -> coder implementation/validation -> reviewer review -> architect docs sync -> project-manager final acceptance/commit/PR.
13
16
  `;
@@ -7,5 +7,6 @@ export function renderCoderHarnessRules() {
7
7
  - Maintain implementation-log.md and validation-log.md under the current task handoff directory.
8
8
  - Do not change module boundaries, public contracts, dependency direction, or test strategy without project-manager/architect replan.
9
9
  - Stop and reply to project-manager when blocked, unclear, or when the plan no longer matches reality.
10
+ - Reply to project-manager once per received VCM message when complete, blocked, or unclear; do not send fragmented progress updates unless project-manager explicitly requested them.
10
11
  `;
11
12
  }
@@ -1,6 +1,7 @@
1
1
  export function renderGitignoreHarnessRules() {
2
2
  return [
3
3
  "# VCM local app state, task metadata, session records, and task worktrees.",
4
- ".ai/vcm/"
4
+ ".ai/vcm/",
5
+ ".claude/worktrees/"
5
6
  ].join("\n");
6
7
  }
@@ -5,6 +5,9 @@ export function renderProjectManagerHarnessRules() {
5
5
  - Clarify the user's request, classify task risk, and choose the role route.
6
6
  - Use vcmctl send to assign work to architect, coder, or reviewer.
7
7
  - Send role work as durable instructions with artifact refs when possible.
8
+ - Enforce per-role turn-taking: keep at most one in-flight message per target role.
9
+ - Before sending another task, question, revise, or review-request to the same role, wait for that role's vcmctl reply or vcmctl result.
10
+ - Use cancel only for urgent supersession; include what is superseded.
8
11
  - Track the workflow gates: architecture plan, implementation/validation, review, docs sync, final acceptance.
9
12
  - Request architect post-review docs sync after reviewer completes.
10
13
  - Prepare final acceptance, commit, and PR only after reviewer and docs-sync gates pass or an explicit exception is approved.
@@ -9,5 +9,6 @@ export function renderReviewerHarnessRules() {
9
9
  - Escalate larger implementation issues to project-manager for coder follow-up.
10
10
  - Escalate architecture, public contract, design, or documentation drift issues to project-manager for architect follow-up.
11
11
  - Do not take over broad implementation and do not weaken tests to pass validation.
12
+ - Reply to project-manager once per received VCM message when complete, blocked, or unclear; do not send fragmented progress updates unless project-manager explicitly requested them.
12
13
  `;
13
14
  }
@@ -41,7 +41,9 @@ function bindTerminalSocket(ws, sessionId, runtime) {
41
41
  runtime.write(sessionId, message.data);
42
42
  }
43
43
  else if (message.type === "resize") {
44
- runtime.resize(sessionId, message.cols, message.rows);
44
+ if (isSafeTerminalResize(message.cols, message.rows)) {
45
+ runtime.resize(sessionId, message.cols, message.rows);
46
+ }
45
47
  }
46
48
  }
47
49
  catch (error) {
@@ -53,6 +55,18 @@ function bindTerminalSocket(ws, sessionId, runtime) {
53
55
  unsubscribe();
54
56
  });
55
57
  }
58
+ export function isSafeTerminalResize(cols, rows) {
59
+ return (Number.isInteger(cols) &&
60
+ Number.isInteger(rows) &&
61
+ cols >= MIN_TERMINAL_COLS &&
62
+ rows >= MIN_TERMINAL_ROWS &&
63
+ cols <= MAX_TERMINAL_COLS &&
64
+ rows <= MAX_TERMINAL_ROWS);
65
+ }
66
+ const MIN_TERMINAL_COLS = 20;
67
+ const MIN_TERMINAL_ROWS = 5;
68
+ const MAX_TERMINAL_COLS = 1000;
69
+ const MAX_TERMINAL_ROWS = 200;
56
70
  function send(ws, message) {
57
71
  if (ws.readyState === ws.OPEN) {
58
72
  ws.send(JSON.stringify(message));
@@ -0,0 +1 @@
1
+ export {};