vibe-coding-master 0.2.6 → 0.2.8

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.
Files changed (43) hide show
  1. package/README.md +164 -9
  2. package/dist/backend/api/claude-hook-routes.js +7 -1
  3. package/dist/backend/api/gateway-routes.js +17 -0
  4. package/dist/backend/api/round-routes.js +1 -1
  5. package/dist/backend/gateway/channels/weixin-ilink-channel.js +304 -0
  6. package/dist/backend/gateway/gateway-audit-log.js +39 -0
  7. package/dist/backend/gateway/gateway-command-parser.js +77 -0
  8. package/dist/backend/gateway/gateway-service.js +848 -0
  9. package/dist/backend/gateway/gateway-settings-service.js +214 -0
  10. package/dist/backend/server.js +42 -2
  11. package/dist/backend/services/claude-hook-service.js +46 -7
  12. package/dist/backend/services/harness-service.js +37 -32
  13. package/dist/backend/services/job-guard-service.js +126 -0
  14. package/dist/backend/services/round-service.js +110 -64
  15. package/dist/backend/services/session-service.js +10 -4
  16. package/dist/backend/services/task-service.js +32 -3
  17. package/dist/backend/services/translation-service.js +15 -0
  18. package/dist/backend/templates/harness/architect-agent.js +19 -8
  19. package/dist/backend/templates/harness/claude-root.js +7 -11
  20. package/dist/backend/templates/harness/coder-agent.js +45 -17
  21. package/dist/backend/templates/harness/gitignore.js +3 -2
  22. package/dist/backend/templates/harness/project-manager-agent.js +16 -11
  23. package/dist/backend/templates/harness/reviewer-agent.js +25 -28
  24. package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +42 -31
  25. package/dist/backend/templates/harness/vcm-long-running-validation-skill.js +37 -18
  26. package/dist/shared/types/gateway.js +1 -0
  27. package/dist-frontend/assets/{index-CPXFnxAY.css → index-7lq6YPCq.css} +1 -1
  28. package/dist-frontend/assets/index-DHuS-DYr.js +92 -0
  29. package/dist-frontend/index.html +2 -2
  30. package/docs/full-harness-baseline.md +7 -5
  31. package/docs/gateway-design.md +200 -27
  32. package/docs/product-design.md +35 -14
  33. package/docs/v0.2-implementation-plan.md +22 -7
  34. package/docs/vcm-cc-best-practices.md +11 -4
  35. package/package.json +2 -1
  36. package/scripts/harness-tools/run-long-check +401 -0
  37. package/scripts/harness-tools/vcm-bash-guard +107 -0
  38. package/scripts/harness-tools/watch-job +204 -0
  39. package/scripts/install-vcm-harness.mjs +93 -387
  40. package/scripts/uninstall-vcm-harness.mjs +18 -1
  41. package/scripts/verify-package.mjs +3 -0
  42. package/dist/backend/templates/harness/known-issues-doc.js +0 -22
  43. package/dist-frontend/assets/index-D0_02lmQ.js +0 -90
@@ -0,0 +1,214 @@
1
+ import path from "node:path";
2
+ import { homedir } from "node:os";
3
+ const DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
4
+ const MAX_DEDUPE_IDS = 1000;
5
+ export function createGatewaySettingsService(deps) {
6
+ const settingsPath = deps.settingsPath ?? path.join(homedir(), ".vcm", "gateway", "settings.json");
7
+ const auditPath = deps.auditPath ?? path.join(homedir(), ".vcm", "gateway", "audit.jsonl");
8
+ const now = deps.now ?? (() => new Date().toISOString());
9
+ let cachedSettings = null;
10
+ async function loadSettings() {
11
+ if (cachedSettings) {
12
+ return cachedSettings;
13
+ }
14
+ if (!(await deps.fs.pathExists(settingsPath))) {
15
+ cachedSettings = normalizeSettings({}, now());
16
+ await saveSettings(cachedSettings);
17
+ return cachedSettings;
18
+ }
19
+ cachedSettings = normalizeSettings(await deps.fs.readJson(settingsPath), now());
20
+ return cachedSettings;
21
+ }
22
+ async function saveSettings(settings) {
23
+ cachedSettings = normalizeSettings(settings, now());
24
+ await deps.fs.writeJsonAtomic(settingsPath, cachedSettings);
25
+ return cachedSettings;
26
+ }
27
+ return {
28
+ loadSettings,
29
+ async updateSettings(input) {
30
+ const current = await loadSettings();
31
+ return saveSettings({
32
+ ...current,
33
+ enabled: input.enabled ?? current.enabled,
34
+ translationEnabled: input.translationEnabled ?? current.translationEnabled,
35
+ currentProjectId: input.currentProjectId !== undefined ? normalizeNullableString(input.currentProjectId) : current.currentProjectId,
36
+ currentTaskSlug: input.currentTaskSlug !== undefined ? normalizeNullableString(input.currentTaskSlug) : current.currentTaskSlug,
37
+ updatedAt: now()
38
+ });
39
+ },
40
+ saveSettings,
41
+ async resetBinding() {
42
+ const current = await loadSettings();
43
+ return saveSettings({
44
+ ...current,
45
+ enabled: false,
46
+ binding: createDefaultBinding(),
47
+ dedupe: {
48
+ recentInboundMessageIds: []
49
+ },
50
+ pushCursors: {},
51
+ lastPollStatus: {
52
+ state: "idle"
53
+ },
54
+ lastMessageStatus: null,
55
+ updatedAt: now()
56
+ });
57
+ },
58
+ expose(settings, running = false) {
59
+ return {
60
+ version: 1,
61
+ enabled: settings.enabled,
62
+ running,
63
+ channel: settings.channel,
64
+ translationEnabled: settings.translationEnabled,
65
+ currentProjectId: settings.currentProjectId,
66
+ currentTaskSlug: settings.currentTaskSlug,
67
+ binding: {
68
+ accountId: settings.binding.accountId,
69
+ baseUrl: settings.binding.baseUrl,
70
+ boundUserId: settings.binding.boundUserId,
71
+ loginUserId: settings.binding.loginUserId,
72
+ tokenConfigured: Boolean(settings.binding.token)
73
+ },
74
+ pendingConfirmations: settings.pendingConfirmations,
75
+ lastPollStatus: settings.lastPollStatus,
76
+ lastMessageStatus: settings.lastMessageStatus,
77
+ updatedAt: settings.updatedAt
78
+ };
79
+ },
80
+ getSettingsPath() {
81
+ return settingsPath;
82
+ },
83
+ getAuditPath() {
84
+ return auditPath;
85
+ }
86
+ };
87
+ }
88
+ export function normalizeSettings(input, timestamp) {
89
+ const bindingInput = isObject(input.binding) ? input.binding : {};
90
+ const dedupeInput = isObject(input.dedupe) ? input.dedupe : {};
91
+ const pendingInput = isObject(input.pendingConfirmations)
92
+ ? input.pendingConfirmations
93
+ : {};
94
+ const pushCursors = isObject(input.pushCursors) ? input.pushCursors : {};
95
+ return {
96
+ version: 1,
97
+ enabled: input.enabled === true,
98
+ channel: "weixin-ilink",
99
+ translationEnabled: input.translationEnabled !== false,
100
+ currentProjectId: normalizeNullableString(input.currentProjectId),
101
+ currentTaskSlug: normalizeNullableString(input.currentTaskSlug),
102
+ binding: {
103
+ ...createDefaultBinding(),
104
+ accountId: normalizeNullableString(bindingInput.accountId),
105
+ baseUrl: normalizeBaseUrl(bindingInput.baseUrl),
106
+ boundUserId: normalizeNullableString(bindingInput.boundUserId),
107
+ loginUserId: normalizeNullableString(bindingInput.loginUserId),
108
+ token: normalizeNullableString(bindingInput.token),
109
+ getUpdatesBuf: typeof bindingInput.getUpdatesBuf === "string" ? bindingInput.getUpdatesBuf : "",
110
+ contextTokens: isObject(bindingInput.contextTokens)
111
+ ? normalizeStringRecord(bindingInput.contextTokens)
112
+ : {}
113
+ },
114
+ dedupe: {
115
+ recentInboundMessageIds: Array.isArray(dedupeInput.recentInboundMessageIds)
116
+ ? dedupeInput.recentInboundMessageIds.filter((value) => typeof value === "string").slice(-MAX_DEDUPE_IDS)
117
+ : []
118
+ },
119
+ pendingConfirmations: {
120
+ closeTask: normalizeCloseTaskConfirmation(pendingInput.closeTask)
121
+ },
122
+ pushCursors: normalizePushCursors(pushCursors),
123
+ lastPollStatus: normalizePollStatus(input.lastPollStatus),
124
+ lastMessageStatus: normalizeMessageStatus(input.lastMessageStatus),
125
+ updatedAt: typeof input.updatedAt === "string" ? input.updatedAt : timestamp
126
+ };
127
+ }
128
+ function createDefaultBinding() {
129
+ return {
130
+ accountId: null,
131
+ baseUrl: DEFAULT_BASE_URL,
132
+ boundUserId: null,
133
+ loginUserId: null,
134
+ token: null,
135
+ getUpdatesBuf: "",
136
+ contextTokens: {}
137
+ };
138
+ }
139
+ function normalizeCloseTaskConfirmation(input) {
140
+ if (!isObject(input)) {
141
+ return null;
142
+ }
143
+ const taskSlug = normalizeNullableString(input.taskSlug);
144
+ const createdAt = normalizeNullableString(input.createdAt);
145
+ const expiresAt = normalizeNullableString(input.expiresAt);
146
+ if (!taskSlug || !createdAt || !expiresAt) {
147
+ return null;
148
+ }
149
+ return { taskSlug, createdAt, expiresAt };
150
+ }
151
+ function normalizePushCursors(input) {
152
+ const out = {};
153
+ for (const [key, value] of Object.entries(input)) {
154
+ if (!isObject(value)) {
155
+ continue;
156
+ }
157
+ out[key] = {
158
+ lastTranscriptEventId: normalizeNullableString(value.lastTranscriptEventId),
159
+ lastTranscriptTimestamp: normalizeNullableString(value.lastTranscriptTimestamp)
160
+ };
161
+ }
162
+ return out;
163
+ }
164
+ function normalizePollStatus(input) {
165
+ if (!isObject(input)) {
166
+ return { state: "idle" };
167
+ }
168
+ const state = input.state === "running" || input.state === "error" || input.state === "expired"
169
+ ? input.state
170
+ : "idle";
171
+ return {
172
+ state,
173
+ checkedAt: normalizeNullableString(input.checkedAt) ?? undefined,
174
+ error: normalizeNullableString(input.error) ?? undefined
175
+ };
176
+ }
177
+ function normalizeMessageStatus(input) {
178
+ if (!isObject(input)) {
179
+ return null;
180
+ }
181
+ return {
182
+ checkedAt: normalizeNullableString(input.checkedAt) ?? undefined,
183
+ direction: input.direction === "inbound" || input.direction === "outbound" ? input.direction : undefined,
184
+ command: normalizeNullableString(input.command) ?? undefined,
185
+ result: input.result === "ok" || input.result === "ignored" || input.result === "error" ? input.result : undefined,
186
+ preview: normalizeNullableString(input.preview) ?? undefined,
187
+ error: normalizeNullableString(input.error) ?? undefined
188
+ };
189
+ }
190
+ function normalizeBaseUrl(input) {
191
+ const raw = typeof input === "string" ? input.trim() : "";
192
+ if (!raw) {
193
+ return DEFAULT_BASE_URL;
194
+ }
195
+ if (raw.startsWith("http://") || raw.startsWith("https://")) {
196
+ return raw.replace(/\/+$/, "");
197
+ }
198
+ return `https://${raw.replace(/\/+$/, "")}`;
199
+ }
200
+ function normalizeNullableString(input) {
201
+ return typeof input === "string" && input.trim() ? input.trim() : null;
202
+ }
203
+ function normalizeStringRecord(input) {
204
+ const out = {};
205
+ for (const [key, value] of Object.entries(input)) {
206
+ if (typeof value === "string") {
207
+ out[key] = value;
208
+ }
209
+ }
210
+ return out;
211
+ }
212
+ function isObject(value) {
213
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
214
+ }
@@ -14,6 +14,12 @@ import { createHarnessService, createScriptFixedHarnessInstaller } from "./servi
14
14
  import { createNodeFileSystemAdapter } from "./adapters/filesystem.js";
15
15
  import { createNodePtyTerminalRuntime } from "./runtime/node-pty-runtime.js";
16
16
  import { createOpenAiCompatibleTranslationProvider } from "./adapters/translation-provider.js";
17
+ import { registerGatewayRoutes } from "./api/gateway-routes.js";
18
+ import { createWeixinIlinkChannel } from "./gateway/channels/weixin-ilink-channel.js";
19
+ import { createGatewayAuditLog } from "./gateway/gateway-audit-log.js";
20
+ import { createGatewayService } from "./gateway/gateway-service.js";
21
+ import { createGatewaySettingsService } from "./gateway/gateway-settings-service.js";
22
+ import { createJobGuardService } from "./services/job-guard-service.js";
17
23
  import { createProjectService } from "./services/project-service.js";
18
24
  import { createSessionRegistry } from "./runtime/session-registry.js";
19
25
  import { createSessionService } from "./services/session-service.js";
@@ -90,7 +96,14 @@ export async function createServer(deps, options = {}) {
90
96
  taskService: deps.taskService,
91
97
  translationService: deps.translationService
92
98
  });
99
+ registerGatewayRoutes(app, { gatewayService: deps.gatewayService });
93
100
  registerTerminalWs(app, { runtime: deps.runtime });
101
+ app.addHook("onReady", async () => {
102
+ await deps.gatewayService.start();
103
+ });
104
+ app.addHook("onClose", async () => {
105
+ deps.gatewayService.stop();
106
+ });
94
107
  if (options.staticDir) {
95
108
  await app.register(fastifyStatic, {
96
109
  root: options.staticDir,
@@ -162,7 +175,12 @@ export function createDefaultServerDeps(options = {}) {
162
175
  sessionService,
163
176
  taskService
164
177
  });
165
- const roundService = createRoundService({ fs });
178
+ const roundService = createRoundService({
179
+ fs,
180
+ onSessionStatusChange: async ({ repoRoot, taskSlug, status }) => {
181
+ await taskService.updateTaskStatus(repoRoot, taskSlug, status);
182
+ }
183
+ });
166
184
  const transcripts = createClaudeTranscriptService();
167
185
  const translationService = createTranslationService({
168
186
  runtime,
@@ -174,6 +192,25 @@ export function createDefaultServerDeps(options = {}) {
174
192
  appSettings,
175
193
  provider: createOpenAiCompatibleTranslationProvider()
176
194
  });
195
+ const gatewaySettings = createGatewaySettingsService({ fs });
196
+ const gatewayAudit = createGatewayAuditLog({
197
+ fs,
198
+ auditPath: gatewaySettings.getAuditPath()
199
+ });
200
+ const gatewayService = createGatewayService({
201
+ fs,
202
+ settings: gatewaySettings,
203
+ audit: gatewayAudit,
204
+ channel: createWeixinIlinkChannel(),
205
+ projectService,
206
+ taskService,
207
+ sessionService,
208
+ messageService,
209
+ translationService,
210
+ roundService,
211
+ runtime,
212
+ appSettings
213
+ });
177
214
  const claudeHookService = createClaudeHookService({
178
215
  projectService,
179
216
  taskService,
@@ -181,7 +218,9 @@ export function createDefaultServerDeps(options = {}) {
181
218
  messageService,
182
219
  roundService,
183
220
  translationService,
184
- appSettings
221
+ appSettings,
222
+ gatewayService,
223
+ jobGuard: createJobGuardService()
185
224
  });
186
225
  return {
187
226
  appSettings,
@@ -196,6 +235,7 @@ export function createDefaultServerDeps(options = {}) {
196
235
  roundService,
197
236
  statusService,
198
237
  translationService,
238
+ gatewayService,
199
239
  runtime
200
240
  };
201
241
  }
@@ -34,6 +34,11 @@ export function createClaudeHookService(deps) {
34
34
  throwUnsupportedEvent(eventName);
35
35
  }
36
36
  const context = await getHookContext(input);
37
+ deps.jobGuard?.notePromptSubmitted({
38
+ repoRoot: context.project.repoRoot,
39
+ taskSlug: input.taskSlug,
40
+ role: input.role
41
+ });
37
42
  const session = await deps.sessionService.recordClaudeHookEvent(context.project.repoRoot, {
38
43
  taskSlug: input.taskSlug,
39
44
  role: input.role,
@@ -43,6 +48,7 @@ export function createClaudeHookService(deps) {
43
48
  cwd: stringOrUndefined(input.event.cwd)
44
49
  });
45
50
  await deps.roundService.recordClaudeHookEvent({
51
+ repoRoot: context.project.repoRoot,
46
52
  stateRepoRoot: context.taskRepoRoot,
47
53
  stateRoot: context.config.stateRoot,
48
54
  taskSlug: input.taskSlug,
@@ -57,7 +63,7 @@ export function createClaudeHookService(deps) {
57
63
  role: input.role,
58
64
  sessionId: session.id,
59
65
  boundaryKind: "start",
60
- occurredAt: session.lastPromptSubmittedAt ?? session.updatedAt
66
+ occurredAt: session.lastTurnStartedAt ?? session.updatedAt
61
67
  });
62
68
  }
63
69
  const submitted = await deps.messageService.confirmPromptSubmitted({
@@ -80,12 +86,33 @@ export function createClaudeHookService(deps) {
80
86
  acceptedMessageId: submitted?.id
81
87
  };
82
88
  }
83
- async function handleStopHook(input) {
89
+ async function processStopHook(input, options) {
84
90
  const eventName = parseHookEvent(input.event.hook_event_name ?? "Stop");
85
91
  if (eventName !== "Stop") {
86
92
  throwUnsupportedEvent(eventName);
87
93
  }
88
94
  const context = await getHookContext(input);
95
+ if (options.allowBlock && deps.jobGuard) {
96
+ const verdict = await deps.jobGuard.evaluateStop({
97
+ repoRoot: context.project.repoRoot,
98
+ taskSlug: input.taskSlug,
99
+ role: input.role,
100
+ taskRepoRoot: context.taskRepoRoot
101
+ });
102
+ if (verdict.behavior === "block") {
103
+ // The role turn stays alive: skip all turn-end bookkeeping so the
104
+ // round keeps running and no route dispatch happens yet.
105
+ return {
106
+ ok: true,
107
+ eventName,
108
+ taskSlug: input.taskSlug,
109
+ role: input.role,
110
+ sessionUpdated: false,
111
+ dispatchedCount: 0,
112
+ stopDecision: { behavior: "block", reason: verdict.reason }
113
+ };
114
+ }
115
+ }
89
116
  const scopedRouteDispatchInput = {
90
117
  repoRoot: context.project.repoRoot,
91
118
  taskRepoRoot: context.taskRepoRoot,
@@ -112,6 +139,7 @@ export function createClaudeHookService(deps) {
112
139
  cwd: stringOrUndefined(input.event.cwd)
113
140
  });
114
141
  await deps.roundService.recordClaudeHookEvent({
142
+ repoRoot: context.project.repoRoot,
115
143
  stateRepoRoot: context.taskRepoRoot,
116
144
  stateRoot: context.config.stateRoot,
117
145
  taskSlug: input.taskSlug,
@@ -120,12 +148,12 @@ export function createClaudeHookService(deps) {
120
148
  settleGuard: async () => {
121
149
  const pending = await deps.messageService.listPendingRouteFiles(settleRouteDispatchInput);
122
150
  if (pending.length === 0) {
123
- return { action: "pause" };
151
+ return { action: "stop" };
124
152
  }
125
153
  const retried = await deps.messageService.scanAndDispatchPendingRouteFiles(settleRouteDispatchInput);
126
154
  return retried.some((result) => result.delivered)
127
155
  ? { action: "continue", reason: "pending route message dispatched" }
128
- : { action: "pause" };
156
+ : { action: "stop" };
129
157
  }
130
158
  });
131
159
  if (session) {
@@ -136,9 +164,16 @@ export function createClaudeHookService(deps) {
136
164
  role: input.role,
137
165
  sessionId: session.id,
138
166
  boundaryKind: "end",
139
- occurredAt: session.lastStopAt ?? session.updatedAt
167
+ occurredAt: session.lastTurnEndedAt ?? session.updatedAt
140
168
  });
141
169
  }
170
+ if (session && input.role === "project-manager") {
171
+ void deps.gatewayService?.handlePmStop({
172
+ repoRoot: context.project.repoRoot,
173
+ taskSlug: input.taskSlug,
174
+ session
175
+ }).catch(() => undefined);
176
+ }
142
177
  const dispatched = await deps.messageService.scanAndDispatchPendingRouteFiles(scopedRouteDispatchInput);
143
178
  return {
144
179
  ok: true,
@@ -185,9 +220,13 @@ export function createClaudeHookService(deps) {
185
220
  if (eventName === "UserPromptSubmit") {
186
221
  return handleUserPromptSubmitHook(input);
187
222
  }
188
- return handleStopHook(input);
223
+ // Legacy combined endpoint: the installed hook discards the response,
224
+ // so a block decision could not be enforced. Never block here.
225
+ return processStopHook(input, { allowBlock: false });
226
+ },
227
+ handleStopHook(input) {
228
+ return processStopHook(input, { allowBlock: true });
189
229
  },
190
- handleStopHook,
191
230
  handlePermissionRequestHook
192
231
  };
193
232
  }
@@ -6,7 +6,6 @@ import { renderArchitectHarnessRules } from "../templates/harness/architect-agen
6
6
  import { renderCoderHarnessRules } from "../templates/harness/coder-agent.js";
7
7
  import { renderRootClaudeHarnessRules } from "../templates/harness/claude-root.js";
8
8
  import { renderGitignoreHarnessRules } from "../templates/harness/gitignore.js";
9
- import { renderKnownIssuesDocHarnessRules } from "../templates/harness/known-issues-doc.js";
10
9
  import { renderProjectManagerHarnessRules } from "../templates/harness/project-manager-agent.js";
11
10
  import { renderPullRequestTemplateHarnessRules } from "../templates/harness/pull-request-template.js";
12
11
  import { renderReviewerHarnessRules } from "../templates/harness/reviewer-agent.js";
@@ -26,13 +25,22 @@ const MANAGED_BLOCK_PATTERN = /<!-- VCM:BEGIN(?:\s+version=(\d+))? -->[\s\S]*?<!
26
25
  const HASH_MANAGED_BLOCK_PATTERN = /# VCM:BEGIN(?:\s+version=(\d+))?\n[\s\S]*?# VCM:END/m;
27
26
  const CLAUDE_SETTINGS_PATH = ".claude/settings.json";
28
27
  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'`;
28
+ const VCM_STOP_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/stop" -H "content-type: application/json" --data-binary @- || true'`;
29
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
+ const VCM_BASH_GUARD_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ]; then exit 0; fi; exec python3 "\${CLAUDE_PROJECT_DIR:-.}/.ai/tools/vcm-bash-guard"'`;
31
+ const VCM_BASH_DEFAULT_TIMEOUT_MS = "600000";
32
+ const VCM_HOOK_DEFINITIONS = [
33
+ { eventName: "PreToolUse", matcher: "Bash", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
34
+ { eventName: "UserPromptSubmit", command: VCM_HOOK_COMMAND, timeout: 5 },
35
+ { eventName: "Stop", command: VCM_STOP_HOOK_COMMAND, timeout: 10 },
36
+ { eventName: "PermissionRequest", command: VCM_PERMISSION_REQUEST_HOOK_COMMAND, timeout: 5 }
37
+ ];
31
38
  const HARNESS_FILES = [
32
39
  {
33
40
  kind: "root-claude",
34
41
  path: "CLAUDE.md",
35
42
  title: "CLAUDE.md",
43
+ blankLineBeforeEnd: true,
36
44
  renderRules: renderRootClaudeHarnessRules
37
45
  },
38
46
  {
@@ -42,12 +50,6 @@ const HARNESS_FILES = [
42
50
  commentStyle: "hash",
43
51
  renderRules: renderGitignoreHarnessRules
44
52
  },
45
- {
46
- kind: "docs-known-issues",
47
- path: "docs/known-issues.md",
48
- title: "Known Issues",
49
- renderRules: renderKnownIssuesDocHarnessRules
50
- },
51
53
  {
52
54
  kind: "pull-request-template",
53
55
  path: ".github/pull_request_template.md",
@@ -97,6 +99,7 @@ const HARNESS_FILES = [
97
99
  kind: "agent-architect",
98
100
  path: ".claude/agents/architect.md",
99
101
  title: "Architect Agent",
102
+ blankLineBeforeEnd: true,
100
103
  frontmatter: renderAgentFrontmatter("architect", "VCM architecture role for plans, module boundaries, public contracts, verifiable behavior, and docs sync."),
101
104
  renderRules: renderArchitectHarnessRules
102
105
  },
@@ -111,7 +114,7 @@ const HARNESS_FILES = [
111
114
  kind: "agent-reviewer",
112
115
  path: ".claude/agents/reviewer.md",
113
116
  title: "Reviewer Agent",
114
- frontmatter: renderAgentFrontmatter("reviewer", "VCM independent review role for acceptance, test adequacy, scope checks, docs gaps, and risk findings."),
117
+ frontmatter: renderAgentFrontmatter("reviewer", "VCM independent review role for acceptance, test adequacy, scope checks, and risk findings."),
115
118
  renderRules: renderReviewerHarnessRules
116
119
  }
117
120
  ];
@@ -344,10 +347,11 @@ function renderHarnessStatus(analyses) {
344
347
  };
345
348
  }
346
349
  function renderManagedBlock(definition, rules) {
350
+ const endSpacing = definition.blankLineBeforeEnd ? "\n\n" : "\n";
347
351
  if (definition.commentStyle === "hash") {
348
- return `# VCM:BEGIN version=${VCM_HARNESS_VERSION}\n${rules.trimEnd()}\n# VCM:END`;
352
+ return `# VCM:BEGIN version=${VCM_HARNESS_VERSION}\n${rules.trimEnd()}${endSpacing}# VCM:END`;
349
353
  }
350
- return `<!-- VCM:BEGIN version=${VCM_HARNESS_VERSION} -->\n${rules.trimEnd()}\n<!-- VCM:END -->`;
354
+ return `<!-- VCM:BEGIN version=${VCM_HARNESS_VERSION} -->\n${rules.trimEnd()}${endSpacing}<!-- VCM:END -->`;
351
355
  }
352
356
  function getManagedBlockPattern(definition) {
353
357
  return definition.commentStyle === "hash"
@@ -427,29 +431,30 @@ function withVcmClaudeHooks(settings) {
427
431
  delete hooks[eventName];
428
432
  }
429
433
  }
430
- for (const eventName of VCM_HOOK_EVENTS) {
431
- const existingMatchers = Array.isArray(hooks[eventName])
432
- ? hooks[eventName]
434
+ for (const definition of VCM_HOOK_DEFINITIONS) {
435
+ const existingMatchers = Array.isArray(hooks[definition.eventName])
436
+ ? hooks[definition.eventName]
433
437
  : [];
434
- hooks[eventName] = [
438
+ hooks[definition.eventName] = [
435
439
  ...existingMatchers.filter((entry) => !isVcmHookMatcher(entry)),
436
- createVcmHookMatcher(eventName)
440
+ {
441
+ ...(definition.matcher ? { matcher: definition.matcher } : {}),
442
+ hooks: [
443
+ {
444
+ type: "command",
445
+ command: definition.command,
446
+ timeout: definition.timeout
447
+ }
448
+ ]
449
+ }
437
450
  ];
438
451
  }
452
+ const env = isPlainObject(settings.env) ? { ...settings.env } : {};
453
+ env.BASH_DEFAULT_TIMEOUT_MS = VCM_BASH_DEFAULT_TIMEOUT_MS;
439
454
  return {
440
455
  ...settings,
441
- hooks
442
- };
443
- }
444
- function createVcmHookMatcher(eventName) {
445
- return {
446
- hooks: [
447
- {
448
- type: "command",
449
- command: eventName === "PermissionRequest" ? VCM_PERMISSION_REQUEST_HOOK_COMMAND : VCM_HOOK_COMMAND,
450
- timeout: 5
451
- }
452
- ]
456
+ hooks,
457
+ env
453
458
  };
454
459
  }
455
460
  function isVcmHookMatcher(value) {
@@ -460,12 +465,12 @@ function isVcmHookMatcher(value) {
460
465
  if (!isPlainObject(hook)) {
461
466
  return false;
462
467
  }
463
- return typeof hook.command === "string" && hook.command.includes("hook-event");
464
- }) || value.hooks.some((hook) => {
465
- if (!isPlainObject(hook)) {
468
+ if (typeof hook.command !== "string") {
466
469
  return false;
467
470
  }
468
- return typeof hook.command === "string" && hook.command.includes("/api/hooks/claude-code");
471
+ return hook.command.includes("VCM") ||
472
+ hook.command.includes("/api/hooks/claude-code") ||
473
+ hook.command.includes("hook-event");
469
474
  });
470
475
  }
471
476
  function isPlainObject(value) {