vibe-coding-master 0.2.4 → 0.2.5

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.
@@ -17,19 +17,26 @@ export function createClaudeAdapter(runner) {
17
17
  }
18
18
  return result.stdout.trim();
19
19
  },
20
- buildRoleStartCommand(role, command = "claude", permissionMode = "default", claudeSessionId, resume = false) {
20
+ buildRoleStartCommand(role, command = "claude", permissionMode = "default", claudeSessionId, resume = false, model = "default") {
21
21
  const args = ["--agent", role];
22
22
  if (claudeSessionId) {
23
23
  args.push(resume ? "--resume" : "--session-id", claudeSessionId);
24
24
  }
25
+ args.push("--model", model);
25
26
  if (permissionMode === "bypassPermissions") {
26
27
  args.push("--permission-mode", "bypassPermissions");
27
28
  }
28
29
  return {
29
30
  command,
30
31
  args,
31
- display: `${command} ${args.join(" ")}`
32
+ display: [command, ...args].map(formatDisplayArg).join(" ")
32
33
  };
33
34
  }
34
35
  };
35
36
  }
37
+ function formatDisplayArg(value) {
38
+ if (/^[A-Za-z0-9_./:=@+-]+$/.test(value)) {
39
+ return value;
40
+ }
41
+ return `'${value.replaceAll("'", "'\\''")}'`;
42
+ }
@@ -5,4 +5,11 @@ export function registerClaudeHookRoutes(app, deps) {
5
5
  app.post("/api/hooks/claude-code/stop", async (request) => {
6
6
  return deps.claudeHookService.handleStopHook(request.body);
7
7
  });
8
+ app.post("/api/hooks/claude-code/permission-request", async (request, reply) => {
9
+ const result = await deps.claudeHookService.handlePermissionRequestHook(request.body);
10
+ if (!result) {
11
+ return reply.code(204).send();
12
+ }
13
+ return result;
14
+ });
8
15
  }
@@ -180,7 +180,8 @@ export function createDefaultServerDeps(options = {}) {
180
180
  sessionService,
181
181
  messageService,
182
182
  roundService,
183
- translationService
183
+ translationService,
184
+ appSettings
184
185
  });
185
186
  return {
186
187
  appSettings,
@@ -1,6 +1,9 @@
1
1
  import path from "node:path";
2
2
  import { homedir } from "node:os";
3
3
  import { createHash } from "node:crypto";
4
+ import { ROLE_NAMES } from "../../shared/constants.js";
5
+ import { createDefaultLaunchTemplate } from "../../shared/types/app-settings.js";
6
+ import { CLAUDE_MODEL_OPTIONS } from "../../shared/types/session.js";
4
7
  const MAX_RECENT_REPOSITORIES = 5;
5
8
  export function createAppSettingsService(deps) {
6
9
  const settingsPath = deps.settingsPath ?? path.join(homedir(), ".vcm", "settings.json");
@@ -184,7 +187,9 @@ function normalizePreferences(input) {
184
187
  : candidate.roundCompletionAlerts;
185
188
  return {
186
189
  themeMode: normalizeThemeMode(candidate.themeMode),
187
- flowPauseAlerts: rawFlowPauseAlerts !== false
190
+ flowPauseAlerts: rawFlowPauseAlerts !== false,
191
+ permissionRequestMode: normalizePermissionRequestMode(candidate.permissionRequestMode),
192
+ launchTemplate: normalizeLaunchTemplate(candidate.launchTemplate)
188
193
  };
189
194
  }
190
195
  function normalizeThemeMode(input) {
@@ -193,6 +198,49 @@ function normalizeThemeMode(input) {
193
198
  }
194
199
  return "system";
195
200
  }
201
+ function normalizePermissionRequestMode(input) {
202
+ if (input === "allowAll") {
203
+ return input;
204
+ }
205
+ return "off";
206
+ }
207
+ function normalizeLaunchTemplate(input) {
208
+ const defaults = createDefaultLaunchTemplate();
209
+ if (!isObject(input)) {
210
+ return defaults;
211
+ }
212
+ const rawRoles = isObject(input.roles) ? input.roles : {};
213
+ const roles = {};
214
+ for (const role of ROLE_NAMES) {
215
+ roles[role] = normalizeRoleLaunchTemplateEntry(rawRoles[role], defaults.roles[role]);
216
+ }
217
+ return {
218
+ version: 1,
219
+ roles,
220
+ autoOrchestration: input.autoOrchestration !== false,
221
+ translationEnabled: input.translationEnabled !== false
222
+ };
223
+ }
224
+ function normalizeRoleLaunchTemplateEntry(input, fallback) {
225
+ const candidate = isObject(input) ? input : {};
226
+ return {
227
+ permissionMode: normalizeClaudePermissionMode(candidate.permissionMode, fallback.permissionMode),
228
+ model: normalizeClaudeModel(candidate.model, fallback.model)
229
+ };
230
+ }
231
+ function normalizeClaudePermissionMode(input, fallback) {
232
+ if (input === "bypassPermissions" || input === "default") {
233
+ return input;
234
+ }
235
+ return fallback;
236
+ }
237
+ function normalizeClaudeModel(input, fallback) {
238
+ if (typeof input !== "string") {
239
+ return fallback;
240
+ }
241
+ const model = CLAUDE_MODEL_OPTIONS.find((option) => option.value === input);
242
+ return model?.value ?? fallback;
243
+ }
196
244
  function normalizeTranslationConfig(input) {
197
245
  if (!input || typeof input !== "object") {
198
246
  return undefined;
@@ -86,6 +86,23 @@ export function createClaudeHookService(deps) {
86
86
  throwUnsupportedEvent(eventName);
87
87
  }
88
88
  const context = await getHookContext(input);
89
+ const scopedRouteDispatchInput = {
90
+ repoRoot: context.project.repoRoot,
91
+ taskRepoRoot: context.taskRepoRoot,
92
+ stateRepoRoot: context.taskRepoRoot,
93
+ stateRoot: context.config.stateRoot,
94
+ handoffDir: context.task.handoffDir,
95
+ taskSlug: input.taskSlug,
96
+ stoppedRole: input.role
97
+ };
98
+ const settleRouteDispatchInput = {
99
+ repoRoot: context.project.repoRoot,
100
+ taskRepoRoot: context.taskRepoRoot,
101
+ stateRepoRoot: context.taskRepoRoot,
102
+ stateRoot: context.config.stateRoot,
103
+ handoffDir: context.task.handoffDir,
104
+ taskSlug: input.taskSlug
105
+ };
89
106
  const session = await deps.sessionService.recordClaudeHookEvent(context.project.repoRoot, {
90
107
  taskSlug: input.taskSlug,
91
108
  role: input.role,
@@ -99,7 +116,17 @@ export function createClaudeHookService(deps) {
99
116
  stateRoot: context.config.stateRoot,
100
117
  taskSlug: input.taskSlug,
101
118
  role: input.role,
102
- eventName
119
+ eventName,
120
+ settleGuard: async () => {
121
+ const pending = await deps.messageService.listPendingRouteFiles(settleRouteDispatchInput);
122
+ if (pending.length === 0) {
123
+ return { action: "pause" };
124
+ }
125
+ const retried = await deps.messageService.scanAndDispatchPendingRouteFiles(settleRouteDispatchInput);
126
+ return retried.some((result) => result.delivered)
127
+ ? { action: "continue", reason: "pending route message dispatched" }
128
+ : { action: "pause" };
129
+ }
103
130
  });
104
131
  if (session) {
105
132
  await deps.translationService.recordConversationBoundary({
@@ -112,15 +139,7 @@ export function createClaudeHookService(deps) {
112
139
  occurredAt: session.lastStopAt ?? session.updatedAt
113
140
  });
114
141
  }
115
- const dispatched = await deps.messageService.scanAndDispatchPendingRouteFiles({
116
- repoRoot: context.project.repoRoot,
117
- taskRepoRoot: context.taskRepoRoot,
118
- stateRepoRoot: context.taskRepoRoot,
119
- stateRoot: context.config.stateRoot,
120
- handoffDir: context.task.handoffDir,
121
- taskSlug: input.taskSlug,
122
- stoppedRole: input.role
123
- });
142
+ const dispatched = await deps.messageService.scanAndDispatchPendingRouteFiles(scopedRouteDispatchInput);
124
143
  return {
125
144
  ok: true,
126
145
  eventName,
@@ -130,6 +149,36 @@ export function createClaudeHookService(deps) {
130
149
  dispatchedCount: dispatched.filter((result) => result.delivered).length
131
150
  };
132
151
  }
152
+ async function handlePermissionRequestHook(input) {
153
+ if (!isRoleName(input.role)) {
154
+ throw new VcmError({
155
+ code: "HOOK_ROLE_INVALID",
156
+ message: `Unknown hook role: ${input.role}`,
157
+ statusCode: 400
158
+ });
159
+ }
160
+ const eventName = input.event.hook_event_name;
161
+ if (eventName !== "PermissionRequest") {
162
+ throw new VcmError({
163
+ code: "HOOK_EVENT_UNSUPPORTED",
164
+ message: `Unsupported Claude Code permission hook event: ${String(eventName)}`,
165
+ statusCode: 400,
166
+ hint: "Use this endpoint for Claude Code PermissionRequest hooks only."
167
+ });
168
+ }
169
+ const preferences = await deps.appSettings.getPreferences();
170
+ if (preferences.permissionRequestMode !== "allowAll") {
171
+ return undefined;
172
+ }
173
+ return {
174
+ hookSpecificOutput: {
175
+ hookEventName: "PermissionRequest",
176
+ decision: {
177
+ behavior: "allow"
178
+ }
179
+ }
180
+ };
181
+ }
133
182
  return {
134
183
  async handleHook(input) {
135
184
  const eventName = parseHookEvent(input.event.hook_event_name);
@@ -138,7 +187,8 @@ export function createClaudeHookService(deps) {
138
187
  }
139
188
  return handleStopHook(input);
140
189
  },
141
- handleStopHook
190
+ handleStopHook,
191
+ handlePermissionRequestHook
142
192
  };
143
193
  }
144
194
  function parseHookEvent(value) {
@@ -26,7 +26,8 @@ const MANAGED_BLOCK_PATTERN = /<!-- VCM:BEGIN(?:\s+version=(\d+))? -->[\s\S]*?<!
26
26
  const HASH_MANAGED_BLOCK_PATTERN = /# VCM:BEGIN(?:\s+version=(\d+))?\n[\s\S]*?# VCM:END/m;
27
27
  const CLAUDE_SETTINGS_PATH = ".claude/settings.json";
28
28
  const VCM_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,event}));});'"'"' | curl -fsS --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code" -H "content-type: application/json" --data-binary @- >/dev/null || true'`;
29
- const VCM_HOOK_EVENTS = ["UserPromptSubmit", "Stop"];
29
+ const VCM_PERMISSION_REQUEST_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,event}));});'"'"' | curl -fsS --max-time 5 -X POST "\${VCM_API_URL}/api/hooks/claude-code/permission-request" -H "content-type: application/json" --data-binary @- || true'`;
30
+ const VCM_HOOK_EVENTS = ["UserPromptSubmit", "Stop", "PermissionRequest"];
30
31
  const HARNESS_FILES = [
31
32
  {
32
33
  kind: "root-claude",
@@ -397,7 +398,7 @@ async function analyzeClaudeSettingsFile(fs, repoRoot) {
397
398
  path: CLAUDE_SETTINGS_PATH,
398
399
  action,
399
400
  reason: exists
400
- ? "Claude Code hook settings do not contain the VCM UserPromptSubmit/Stop hook bridge."
401
+ ? "Claude Code hook settings do not contain the VCM hook bridge."
401
402
  : "Claude Code hook settings are missing; VCM will create them."
402
403
  },
403
404
  nextContent: action === "ok" ? undefined : nextContent
@@ -432,7 +433,7 @@ function withVcmClaudeHooks(settings) {
432
433
  : [];
433
434
  hooks[eventName] = [
434
435
  ...existingMatchers.filter((entry) => !isVcmHookMatcher(entry)),
435
- createVcmHookMatcher()
436
+ createVcmHookMatcher(eventName)
436
437
  ];
437
438
  }
438
439
  return {
@@ -440,12 +441,12 @@ function withVcmClaudeHooks(settings) {
440
441
  hooks
441
442
  };
442
443
  }
443
- function createVcmHookMatcher() {
444
+ function createVcmHookMatcher(eventName) {
444
445
  return {
445
446
  hooks: [
446
447
  {
447
448
  type: "command",
448
- command: VCM_HOOK_COMMAND,
449
+ command: eventName === "PermissionRequest" ? VCM_PERMISSION_REQUEST_HOOK_COMMAND : VCM_HOOK_COMMAND,
449
450
  timeout: 5
450
451
  }
451
452
  ]
@@ -51,7 +51,7 @@ export function createRoundService(deps) {
51
51
  await save(input, next);
52
52
  return next;
53
53
  }
54
- async function settleIfStillCurrent(input, roundId, settleDeadlineAt) {
54
+ async function settleIfStillCurrent(input, roundId, settleDeadlineAt, settleGuard) {
55
55
  await withTaskLock(input, async () => {
56
56
  const state = await load(input);
57
57
  const current = state.currentRound;
@@ -60,7 +60,30 @@ export function createRoundService(deps) {
60
60
  current.settleDeadlineAt !== settleDeadlineAt) {
61
61
  return;
62
62
  }
63
- await settleIfNeeded(input, state, settleDeadlineAt);
63
+ const timestamp = maxIsoTimestamp(now(), settleDeadlineAt);
64
+ if (settleGuard) {
65
+ const decision = await runSettleGuard(settleGuard, {
66
+ ...input,
67
+ role: current.activeRole,
68
+ roundId,
69
+ settleDeadlineAt
70
+ });
71
+ if (decision.action === "continue") {
72
+ const nextRound = {
73
+ ...current,
74
+ settleDeadlineAt: addMilliseconds(timestamp, settleMs)
75
+ };
76
+ const next = {
77
+ ...state,
78
+ currentRound: nextRound,
79
+ updatedAt: timestamp
80
+ };
81
+ await save(input, next);
82
+ scheduleSettleTimer(input, nextRound, timestamp, settleGuard);
83
+ return;
84
+ }
85
+ }
86
+ await settleIfNeeded(input, state, timestamp);
64
87
  });
65
88
  }
66
89
  function clearSettleTimer(input) {
@@ -81,7 +104,7 @@ export function createRoundService(deps) {
81
104
  settleTimers.delete(key);
82
105
  }
83
106
  }
84
- function scheduleSettleTimer(input, round, timestamp) {
107
+ function scheduleSettleTimer(input, round, timestamp, settleGuard) {
85
108
  if (round.status !== "settling" || !round.settleDeadlineAt) {
86
109
  clearSettleTimer(input);
87
110
  return;
@@ -90,7 +113,7 @@ export function createRoundService(deps) {
90
113
  const delayMs = Math.max(0, new Date(round.settleDeadlineAt).getTime() - new Date(timestamp).getTime());
91
114
  const timer = setTimer(() => {
92
115
  settleTimers.delete(getRoundStatePath(input));
93
- void settleIfStillCurrent(input, round.id, round.settleDeadlineAt ?? "").catch(() => undefined);
116
+ void settleIfStillCurrent(input, round.id, round.settleDeadlineAt ?? "", settleGuard).catch(() => undefined);
94
117
  }, delayMs);
95
118
  settleTimers.set(getRoundStatePath(input), timer);
96
119
  }
@@ -112,8 +135,7 @@ export function createRoundService(deps) {
112
135
  async getTaskRoundState(input) {
113
136
  return withTaskLock(input, async () => {
114
137
  const timestamp = now();
115
- const state = await settleIfNeeded(input, await load(input), timestamp);
116
- return toTaskRoundState(state, timestamp);
138
+ return toTaskRoundState(await load(input), timestamp);
117
139
  });
118
140
  },
119
141
  async recordClaudeHookEvent(input) {
@@ -136,7 +158,7 @@ export function createRoundService(deps) {
136
158
  clearSettleTimer(input);
137
159
  }
138
160
  else if (next.currentRound) {
139
- scheduleSettleTimer(input, next.currentRound, timestamp);
161
+ scheduleSettleTimer(input, next.currentRound, timestamp, input.settleGuard);
140
162
  }
141
163
  return toTaskRoundState(next, timestamp);
142
164
  });
@@ -314,6 +336,17 @@ function appendUniqueRole(roles, role) {
314
336
  function addMilliseconds(value, milliseconds) {
315
337
  return new Date(new Date(value).getTime() + milliseconds).toISOString();
316
338
  }
339
+ function maxIsoTimestamp(left, right) {
340
+ return Date.parse(left) >= Date.parse(right) ? left : right;
341
+ }
342
+ async function runSettleGuard(settleGuard, input) {
343
+ try {
344
+ return await settleGuard(input);
345
+ }
346
+ catch {
347
+ return { action: "pause" };
348
+ }
349
+ }
317
350
  function getDurationMs(start, end) {
318
351
  const startMs = Date.parse(start);
319
352
  const endMs = Date.parse(end);
@@ -18,6 +18,7 @@ export function createSessionService(deps) {
18
18
  const paths = deps.artifactService.getHandoffPaths(taskRepoRoot, task.handoffDir);
19
19
  const persisted = await loadPersistedRoleRecord(deps.fs, taskRepoRoot, config.stateRoot, taskSlug, role);
20
20
  const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
21
+ const model = normalizeClaudeModel(input.model ?? persisted?.model);
21
22
  const claudeSessionId = launchMode === "resume"
22
23
  ? persisted?.claudeSessionId
23
24
  : randomUUID();
@@ -32,7 +33,7 @@ export function createSessionService(deps) {
32
33
  const transcriptPath = launchMode === "resume" && persisted?.transcriptPath
33
34
  ? persisted.transcriptPath
34
35
  : claudeTranscriptPath(taskRepoRoot, claudeSessionId);
35
- const startCommand = deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, claudeSessionId, launchMode === "resume");
36
+ const startCommand = deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, claudeSessionId, launchMode === "resume", model);
36
37
  const runtimeSession = await deps.runtime.createSession({
37
38
  taskSlug,
38
39
  role,
@@ -61,6 +62,7 @@ export function createSessionService(deps) {
61
62
  activityStatus: "idle",
62
63
  command: startCommand.display,
63
64
  permissionMode,
65
+ model,
64
66
  cwd: taskRepoRoot,
65
67
  terminalBackend: "node-pty",
66
68
  pid: runtimeSession.pid,
@@ -240,7 +242,8 @@ async function loadPersistedRoleRecord(fs, repoRoot, stateRoot, taskSlug, role)
240
242
  return record
241
243
  ? {
242
244
  ...record,
243
- permissionMode: normalizeClaudePermissionMode(record.permissionMode)
245
+ permissionMode: normalizeClaudePermissionMode(record.permissionMode),
246
+ model: normalizeClaudeModel(record.model)
244
247
  }
245
248
  : undefined;
246
249
  }
@@ -291,3 +294,14 @@ function normalizeClaudePermissionMode(value) {
291
294
  }
292
295
  return "default";
293
296
  }
297
+ function normalizeClaudeModel(value) {
298
+ if (value === "best"
299
+ || value === "fable"
300
+ || value === "opus"
301
+ || value === "opus[1m]"
302
+ || value === "claude-opus-4-8"
303
+ || value === "claude-opus-4-8[1m]") {
304
+ return value;
305
+ }
306
+ return "default";
307
+ }
@@ -1 +1,18 @@
1
+ import { ROLE_NAMES } from "../constants.js";
1
2
  export const THEME_MODES = ["system", "light", "dark"];
3
+ export const PERMISSION_REQUEST_MODES = ["off", "allowAll"];
4
+ export function createDefaultLaunchTemplate() {
5
+ const roles = {};
6
+ for (const role of ROLE_NAMES) {
7
+ roles[role] = {
8
+ permissionMode: "default",
9
+ model: "default"
10
+ };
11
+ }
12
+ return {
13
+ version: 1,
14
+ roles,
15
+ autoOrchestration: true,
16
+ translationEnabled: true
17
+ };
18
+ }
@@ -1 +1,37 @@
1
- export {};
1
+ export const CLAUDE_MODEL_OPTIONS = [
2
+ {
3
+ value: "default",
4
+ label: "Default",
5
+ description: "Account default"
6
+ },
7
+ {
8
+ value: "best",
9
+ label: "Best",
10
+ description: "Fable 5 or latest Opus"
11
+ },
12
+ {
13
+ value: "fable",
14
+ label: "Fable 5",
15
+ description: "1M context, v2.1.170+"
16
+ },
17
+ {
18
+ value: "opus",
19
+ label: "Opus",
20
+ description: "latest Opus"
21
+ },
22
+ {
23
+ value: "opus[1m]",
24
+ label: "Opus 1M",
25
+ description: "Force 1M context"
26
+ },
27
+ {
28
+ value: "claude-opus-4-8",
29
+ label: "Opus 4.8",
30
+ description: "Current Opus"
31
+ },
32
+ {
33
+ value: "claude-opus-4-8[1m]",
34
+ label: "Opus 4.8 1M",
35
+ description: "Opus 4.8 + 1M context"
36
+ }
37
+ ];