vibe-coding-master 0.0.11 → 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,25 @@ 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
+ }
41
+ if (!shouldCreateWorktree) {
42
+ const activeInlineTask = await findActiveInlineTask(deps.fs, repoRoot, config.stateRoot);
43
+ if (activeInlineTask) {
44
+ throw new VcmError({
45
+ code: "INLINE_TASK_EXISTS",
46
+ message: `An inline task already exists: ${activeInlineTask.taskSlug}`,
47
+ statusCode: 409,
48
+ hint: "Close the existing inline task first, or enable Create worktree and branch for this task."
49
+ });
50
+ }
51
+ }
33
52
  if (shouldCreateWorktree && worktreePath) {
34
53
  if (await deps.git.branchExists(repoRoot, taskBranch)) {
35
54
  throw new VcmError({
@@ -75,7 +94,7 @@ export function createTaskService(deps) {
75
94
  repoRoot,
76
95
  worktreePath,
77
96
  branch: taskBranch,
78
- handoffDir: path.posix.join(config.handoffRoot, input.taskSlug),
97
+ handoffDir: config.handoffRoot,
79
98
  status: "created",
80
99
  specPath: input.specPath,
81
100
  cleanupStatus: "active"
@@ -146,11 +165,11 @@ export function createTaskService(deps) {
146
165
  const config = await deps.projectService.loadConfig(repoRoot);
147
166
  const task = await this.loadTask(repoRoot, taskSlug);
148
167
  const taskRepoRoot = getTaskRuntimeRepoRoot(task);
149
- const statePaths = getTaskStatePaths(repoRoot, taskRepoRoot, config.stateRoot, taskSlug);
168
+ const statePaths = getTaskStatePaths(repoRoot, taskRepoRoot, config.stateRoot, config.handoffRoot, taskSlug);
150
169
  const removedStatePaths = [];
151
170
  const cleanedAt = now();
152
171
  if (task.worktreePath) {
153
- assertTaskWorktreePath(repoRoot, config.stateRoot, task.worktreePath);
172
+ assertTaskWorktreePath(repoRoot, task.worktreePath);
154
173
  await deps.git.removeWorktree(repoRoot, task.worktreePath, { force: options.force ?? true });
155
174
  }
156
175
  let deletedBranch;
@@ -178,8 +197,8 @@ export function getTaskRuntimeRepoRoot(task) {
178
197
  function getTaskPath(repoRoot, stateRoot, taskSlug) {
179
198
  return path.join(repoRoot, stateRoot, "tasks", `${taskSlug}.json`);
180
199
  }
181
- function getTaskWorktreePath(repoRoot, stateRoot, taskSlug) {
182
- return path.join(repoRoot, stateRoot, "worktrees", taskSlug);
200
+ function getTaskWorktreePath(repoRoot, taskSlug) {
201
+ return path.join(repoRoot, ".claude", "worktrees", taskSlug);
183
202
  }
184
203
  async function ensureTaskRuntimeStateDirs(fs, taskRepoRoot, stateRoot) {
185
204
  await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "sessions"));
@@ -187,17 +206,32 @@ async function ensureTaskRuntimeStateDirs(fs, taskRepoRoot, stateRoot) {
187
206
  await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "orchestration"));
188
207
  await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "translation"));
189
208
  }
190
- function getTaskStatePaths(baseRepoRoot, taskRepoRoot, stateRoot, taskSlug) {
209
+ async function findActiveInlineTask(fs, repoRoot, stateRoot) {
210
+ const tasksDir = path.join(repoRoot, stateRoot, "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 = 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 getTaskStatePaths(baseRepoRoot, taskRepoRoot, stateRoot, handoffRoot, taskSlug) {
191
224
  return [
192
225
  path.join(baseRepoRoot, stateRoot, "tasks", `${taskSlug}.json`),
193
226
  path.join(taskRepoRoot, stateRoot, "sessions", `${taskSlug}.json`),
194
227
  path.join(taskRepoRoot, stateRoot, "messages", `${taskSlug}.jsonl`),
195
228
  path.join(taskRepoRoot, stateRoot, "orchestration", `${taskSlug}.json`),
196
- path.join(taskRepoRoot, stateRoot, "translation", taskSlug)
229
+ path.join(taskRepoRoot, stateRoot, "translation", taskSlug),
230
+ path.join(taskRepoRoot, handoffRoot)
197
231
  ];
198
232
  }
199
- function assertTaskWorktreePath(repoRoot, stateRoot, worktreePath) {
200
- const worktreeRoot = path.resolve(repoRoot, stateRoot, "worktrees");
233
+ function assertTaskWorktreePath(repoRoot, worktreePath) {
234
+ const worktreeRoot = path.resolve(repoRoot, ".claude", "worktrees");
201
235
  const resolvedWorktreePath = path.resolve(worktreePath);
202
236
  const relative = path.relative(worktreeRoot, resolvedWorktreePath);
203
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
  }
@@ -3,11 +3,14 @@ export function renderRootClaudeHarnessRules() {
3
3
 
4
4
  - This repository uses VibeCodingMaster for multi-session Claude Code work.
5
5
  - User-facing work starts with the project-manager role.
6
- - Canonical task handoffs live under .ai/handoffs/<task-slug>/.
6
+ - Canonical task handoffs live under .ai/vcm/handoffs/ inside the current task runtime repo.
7
7
  - Use only the current task's handoff directory for task-specific artifacts.
8
- - Do not create or write .ai/handoffs/<other-task>/ for the current task.
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));
@@ -131,7 +131,7 @@ function printHelp() {
131
131
  Usage:
132
132
  vcmctl send --to coder --type task --body-file /tmp/message.md
133
133
  vcmctl reply --type blocked --body "Need clarification."
134
- vcmctl result --body-file /tmp/result.md --artifact .ai/handoffs/task/implementation-log.md
134
+ vcmctl result --body-file /tmp/result.md --artifact .ai/vcm/handoffs/implementation-log.md
135
135
  vcmctl inbox
136
136
  `);
137
137
  }
@@ -0,0 +1 @@
1
+ export {};