vibe-coding-master 0.2.7 → 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.
package/README.md CHANGED
@@ -405,15 +405,28 @@ CLAUDE.md
405
405
  .ai/tools/generate-public-surface
406
406
  .ai/tools/run-long-check
407
407
  .ai/tools/watch-job
408
+ .ai/tools/vcm-bash-guard
408
409
  .github/pull_request_template.md
409
410
  ```
410
411
 
411
412
  Repo-local skills are installed as `.claude/skills/<skill-name>/SKILL.md` so
412
413
  Claude Code can register them.
413
414
 
414
- VCM roles must not start background jobs. The only allowed background job is
415
- `.ai/tools/run-long-check` when used through `vcm-long-running-validation`;
416
- `.ai/tools/watch-job` enforces a 60 minute maximum timeout.
415
+ VCM roles must not run background Bash. A `PreToolUse` hook
416
+ (`.ai/tools/vcm-bash-guard`) denies `run_in_background`, `nohup`, `setsid`,
417
+ `disown`, and trailing `&` calls in VCM role sessions. The only sanctioned
418
+ long-running mechanism is `.ai/tools/run-long-check` plus
419
+ `.ai/tools/watch-job` through `vcm-long-running-validation`:
420
+
421
+ - The detached job worker itself enforces the job ceiling (`--timeout`, max 60
422
+ minutes) and a supervision lease: a job left without a live foreground
423
+ watcher for about 2 minutes is killed and recorded as `orphaned`.
424
+ - `watch-job` renews the lease and watches in windows of up to 8 minutes; it
425
+ exits `125` while the job is still running instead of killing it, and the
426
+ role re-runs it in the same turn until a terminal result.
427
+ - The VCM backend blocks a role from ending its turn while one of its
428
+ validation jobs is still running.
429
+ - Only one validation job may be active at a time.
417
430
 
418
431
  If a managed-block file already exists, VCM preserves user-authored content and only inserts or replaces the VCM block:
419
432
 
@@ -434,7 +447,7 @@ For `.gitignore`, VCM uses a gitignore-native managed block:
434
447
 
435
448
  `.ai/vcm/` is the active VCM local control area, and `.claude/worktrees/` is the Claude-compatible task worktree area. VCM keeps the task index in app-local project state under `~/.vcm/projects/`; each task runtime repo keeps its own session, message, orchestration, and translation state.
436
449
 
437
- VCM also JSON-merges `.claude/settings.json` to install Claude Code `UserPromptSubmit` and `Stop` hooks. The hooks post directly to the local VCM backend, so roles do not need a VCM CLI command to confirm delivery or report turn completion.
450
+ VCM also JSON-merges `.claude/settings.json` to install Claude Code `PreToolUse`, `UserPromptSubmit`, `Stop`, and `PermissionRequest` hooks plus a managed `env.BASH_DEFAULT_TIMEOUT_MS` so foreground watch windows fit inside the Bash tool timeout. The hooks post directly to the local VCM backend, so roles do not need a VCM CLI command to confirm delivery or report turn completion. The `Stop` hook forwards the backend response to Claude Code, which lets VCM block turn-end while a validation job is still running.
438
451
 
439
452
  Bootstrap is AI-assisted. VCM starts a visible temporary Claude Code session in the connected repository and asks it to use the `vcm-harness-bootstrap` skill. Bootstrap fills project-specific content and generated context:
440
453
 
@@ -2,8 +2,14 @@ export function registerClaudeHookRoutes(app, deps) {
2
2
  app.post("/api/hooks/claude-code", async (request) => {
3
3
  return deps.claudeHookService.handleHook(request.body);
4
4
  });
5
+ // The installed Stop hook pipes this response straight to Claude Code, so
6
+ // the body must be exactly the Stop-hook stdout contract.
5
7
  app.post("/api/hooks/claude-code/stop", async (request) => {
6
- return deps.claudeHookService.handleStopHook(request.body);
8
+ const result = await deps.claudeHookService.handleStopHook(request.body);
9
+ if (result.stopDecision) {
10
+ return { decision: "block", reason: result.stopDecision.reason };
11
+ }
12
+ return {};
7
13
  });
8
14
  app.post("/api/hooks/claude-code/permission-request", async (request, reply) => {
9
15
  const result = await deps.claudeHookService.handlePermissionRequestHook(request.body);
@@ -19,6 +19,7 @@ import { createWeixinIlinkChannel } from "./gateway/channels/weixin-ilink-channe
19
19
  import { createGatewayAuditLog } from "./gateway/gateway-audit-log.js";
20
20
  import { createGatewayService } from "./gateway/gateway-service.js";
21
21
  import { createGatewaySettingsService } from "./gateway/gateway-settings-service.js";
22
+ import { createJobGuardService } from "./services/job-guard-service.js";
22
23
  import { createProjectService } from "./services/project-service.js";
23
24
  import { createSessionRegistry } from "./runtime/session-registry.js";
24
25
  import { createSessionService } from "./services/session-service.js";
@@ -218,7 +219,8 @@ export function createDefaultServerDeps(options = {}) {
218
219
  roundService,
219
220
  translationService,
220
221
  appSettings,
221
- gatewayService
222
+ gatewayService,
223
+ jobGuard: createJobGuardService()
222
224
  });
223
225
  return {
224
226
  appSettings,
@@ -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,
@@ -81,12 +86,33 @@ export function createClaudeHookService(deps) {
81
86
  acceptedMessageId: submitted?.id
82
87
  };
83
88
  }
84
- async function handleStopHook(input) {
89
+ async function processStopHook(input, options) {
85
90
  const eventName = parseHookEvent(input.event.hook_event_name ?? "Stop");
86
91
  if (eventName !== "Stop") {
87
92
  throwUnsupportedEvent(eventName);
88
93
  }
89
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
+ }
90
116
  const scopedRouteDispatchInput = {
91
117
  repoRoot: context.project.repoRoot,
92
118
  taskRepoRoot: context.taskRepoRoot,
@@ -194,9 +220,13 @@ export function createClaudeHookService(deps) {
194
220
  if (eventName === "UserPromptSubmit") {
195
221
  return handleUserPromptSubmitHook(input);
196
222
  }
197
- 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 });
198
229
  },
199
- handleStopHook,
200
230
  handlePermissionRequestHook
201
231
  };
202
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) {
@@ -0,0 +1,126 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ const ACTIVE_JOB_STATUSES = new Set(["queued", "starting", "running"]);
4
+ const QUEUED_JOB_FRESH_MS = 120_000;
5
+ export const MAX_CONSECUTIVE_STOP_BLOCKS = 3;
6
+ export function createJobGuardService(deps = {}) {
7
+ const isProcessAlive = deps.isProcessAlive ?? defaultIsProcessAlive;
8
+ const now = deps.now ?? Date.now;
9
+ const blockStates = new Map();
10
+ async function findActiveJobs(taskRepoRoot) {
11
+ const jobsRoot = path.join(taskRepoRoot, ".ai/vcm/jobs");
12
+ let entries;
13
+ try {
14
+ entries = await fs.readdir(jobsRoot);
15
+ }
16
+ catch {
17
+ return [];
18
+ }
19
+ const jobs = [];
20
+ for (const entry of entries.sort()) {
21
+ const statusPath = path.join(jobsRoot, entry, "status.json");
22
+ let status;
23
+ try {
24
+ status = JSON.parse(await fs.readFile(statusPath, "utf8"));
25
+ }
26
+ catch {
27
+ continue;
28
+ }
29
+ if (typeof status.status !== "string" || !ACTIVE_JOB_STATUSES.has(status.status)) {
30
+ continue;
31
+ }
32
+ const processId = numberOrUndefined(status.processId);
33
+ const workerPid = numberOrUndefined(status.workerPid);
34
+ const pid = processId ?? workerPid;
35
+ if (pid !== undefined) {
36
+ if (!isProcessAlive(pid)) {
37
+ continue;
38
+ }
39
+ }
40
+ else {
41
+ // queued entry whose worker has not reported a pid yet: only trust it briefly
42
+ try {
43
+ const stat = await fs.stat(statusPath);
44
+ if (now() - stat.mtimeMs > QUEUED_JOB_FRESH_MS) {
45
+ continue;
46
+ }
47
+ }
48
+ catch {
49
+ continue;
50
+ }
51
+ }
52
+ let leaseMtimeMs;
53
+ try {
54
+ leaseMtimeMs = (await fs.stat(path.join(jobsRoot, entry, "lease"))).mtimeMs;
55
+ }
56
+ catch {
57
+ leaseMtimeMs = undefined;
58
+ }
59
+ jobs.push({
60
+ jobId: typeof status.jobId === "string" ? status.jobId : entry,
61
+ status: status.status,
62
+ startedAt: typeof status.startedAt === "string" ? status.startedAt : undefined,
63
+ timeoutSeconds: numberOrUndefined(status.timeoutSeconds),
64
+ processId,
65
+ workerPid,
66
+ leaseMtimeMs
67
+ });
68
+ }
69
+ return jobs;
70
+ }
71
+ return {
72
+ findActiveJobs,
73
+ async evaluateStop(input) {
74
+ const key = stateKey(input);
75
+ const jobs = await findActiveJobs(input.taskRepoRoot);
76
+ if (jobs.length === 0) {
77
+ blockStates.delete(key);
78
+ return { behavior: "allow" };
79
+ }
80
+ const leaseMtimeMs = jobs.reduce((latest, job) => job.leaseMtimeMs !== undefined && (latest === undefined || job.leaseMtimeMs > latest)
81
+ ? job.leaseMtimeMs
82
+ : latest, undefined);
83
+ let state = blockStates.get(key) ?? { count: 0 };
84
+ const watcherProgressed = state.count > 0
85
+ && leaseMtimeMs !== undefined
86
+ && state.lastLeaseMtimeMs !== undefined
87
+ && leaseMtimeMs > state.lastLeaseMtimeMs;
88
+ if (watcherProgressed) {
89
+ state = { count: 0 };
90
+ }
91
+ if (state.count >= MAX_CONSECUTIVE_STOP_BLOCKS) {
92
+ // The role keeps trying to stop without watching; let it stop so the
93
+ // round can settle. The job worker lease will reap the job itself.
94
+ blockStates.delete(key);
95
+ return { behavior: "allow" };
96
+ }
97
+ blockStates.set(key, { count: state.count + 1, lastLeaseMtimeMs: leaseMtimeMs });
98
+ return { behavior: "block", reason: buildBlockReason(jobs) };
99
+ },
100
+ notePromptSubmitted(input) {
101
+ blockStates.delete(stateKey(input));
102
+ }
103
+ };
104
+ }
105
+ function stateKey(input) {
106
+ return `${input.repoRoot}::${input.taskSlug}::${input.role}`;
107
+ }
108
+ function buildBlockReason(jobs) {
109
+ const first = jobs[0];
110
+ const listing = jobs.map((job) => `${job.jobId} (${job.status})`).join(", ");
111
+ return `VCM: validation job ${listing} is still running. Do not end the turn while a validation job is running. `
112
+ + `Run \`.ai/tools/watch-job ${first.jobId}\` again now and keep watching until it reports a terminal result `
113
+ + `(success, failed, timeout, or orphaned), then record the result.`;
114
+ }
115
+ function numberOrUndefined(value) {
116
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
117
+ }
118
+ function defaultIsProcessAlive(pid) {
119
+ try {
120
+ process.kill(pid, 0);
121
+ return true;
122
+ }
123
+ catch (error) {
124
+ return error.code === "EPERM";
125
+ }
126
+ }
@@ -1,24 +1,28 @@
1
1
  export function renderArchitectHarnessRules() {
2
- return `## VCM Architect Rules
2
+ return `
3
+ ## VCM Architect Rules
3
4
 
4
5
  ### Role Scope
5
6
 
6
7
  - Own technical analysis, architecture planning, module boundaries, file responsibilities, public contracts, verifiable behavior, phase boundaries, behavior/contract proof points, risks, and Replan triggers.
8
+ - Own \`docs/known-issues.md\` promotion and durable issue updates.
9
+ - Own architecture docs sync across \`docs/ARCHITECTURE.md\` and affected \`<module>/ARCHITECTURE.md\` files.
7
10
  - Do not implement production code.
8
11
  - Do not design complete test cases, coverage matrices, or final validation strategy; reviewer owns independent test design, test adequacy, and validation confidence.
9
12
  - Do not make product priority or approval decisions; route those questions back to project-manager.
10
- - Reply to project-manager with the \`vcm-route-message\` skill.
11
13
 
12
14
  ### Planning Inputs
13
15
 
14
- - Read \`CLAUDE.md\`, the role command, durable plans when present, relevant handoff artifacts, and affected project docs before planning.
15
- - Use \`docs/ARCHITECTURE.md\`, \`docs/MODULE_MAP.md\`, \`docs/SECURITY.md\`, and \`docs/DEPENDENCY_RULES.md\` as durable project truth when relevant.
16
+ - Read the role message, durable plans when present, relevant handoff artifacts, \`docs/ARCHITECTURE.md\`, affected \`<module>/ARCHITECTURE.md\` files when present, and affected project docs before planning.
17
+ - Read \`.ai/generated/module-index.json\` when planning module scope, file scope, dependency direction, or phased work.
18
+ - Read \`.ai/generated/public-surface.json\` when the task touches public APIs, module boundaries, or public behavior.
16
19
  - If durable docs conflict with the requested plan or code reality, report the conflict to project-manager and identify whether user approval is required.
17
20
 
18
21
  ### Architecture Plan
19
22
 
20
23
  - Write \`.ai/vcm/handoffs/architecture-plan.md\` before coder work starts.
21
- - The plan must cover changed files, file responsibilities, public interfaces or user-visible behavior, required behavior or contract proof points, docs impact, risks, and Replan triggers.
24
+ - The plan must cover changed files, task-local file responsibilities, affected modules, public interfaces or user-visible behavior, required behavior or contract proof points, docs impact, risks, and Replan triggers.
25
+ - For docs impact, state whether changes belong in \`docs/ARCHITECTURE.md\`, affected \`<module>/ARCHITECTURE.md\`, \`.ai/generated/public-surface.json\`, or no durable architecture doc.
22
26
  - Keep implementation work scoped to what can be safely described, validated, reviewed, and handed off.
23
27
 
24
28
  ### Phase Planning
@@ -39,10 +43,17 @@ export function renderArchitectHarnessRules() {
39
43
  ### Docs Sync
40
44
 
41
45
  - Perform docs sync only when project-manager requests it after reviewer completes.
42
- - Check whether final code changed durable project truth: architecture, module map, public contracts, security constraints, dependency rules, or durable plans.
43
- - Update affected durable docs when project truth changed; otherwise state which docs were checked and why they remain current.
44
- - Treat reviewer-reported docs gaps as inputs; resolve technical docs drift or report conflicts back to project-manager.
46
+ - Update \`docs/ARCHITECTURE.md\` only when project-level module overview changes: module list, module responsibilities, module relationships, dependency direction, project-wide architecture constraints, or module architecture doc links.
47
+ - Update affected \`<module>/ARCHITECTURE.md\` when module-level detailed design changes: boundaries, behavior, important public surface explanations, internal risks, or module-specific architecture notes.
48
+ - Treat \`.ai/generated/public-surface.json\` as the full machine index for public surface. Verify or report its freshness when public APIs changed; do not replace it with prose in architecture docs.
49
+ - When module structure changes, require \`.ai/tools/generate-module-index --check\` or regeneration.
50
+ - When crate-external public APIs change, require \`.ai/tools/generate-public-surface --check\` or regeneration.
45
51
  - Read \`.ai/vcm/handoffs/known-issues.md\` and promote confirmed unresolved issues to \`docs/known-issues.md\`.
46
52
  - Write \`.ai/vcm/handoffs/docs-sync-report.md\` with decision, evidence reviewed, architecture drift check, docs updated, docs left unchanged, remaining documentation risks, and handoff notes.
53
+
54
+ ### Background Jobs
55
+
56
+ - Never background a Bash command: no \`run_in_background\`, \`nohup\`, \`setsid\`, \`disown\`, or trailing \`&\`.
57
+ - For any command that may exceed 2 minutes, use the \`vcm-long-running-validation\` skill and stay in the turn, re-running \`.ai/tools/watch-job\` until it reports a terminal result.
47
58
  `;
48
59
  }
@@ -3,16 +3,16 @@ export function renderRootClaudeHarnessRules() {
3
3
 
4
4
  - Use the durable project docs below as role-relevant project truth.
5
5
  - Read module-local \`CLAUDE.md\` before editing a subdirectory if one exists.
6
+ - Use \`vcm-route-message\` whenever a VCM role hands off work, asks another role a question, reports a result, reports a blocker, or raises a finding. Follow its write-then-stop rule.
6
7
  - Use \`vcm-long-running-validation\` for long-running validation. Follow the background job limits below.
7
8
 
8
9
  ## VCM Background Jobs
9
10
 
10
- - Do not start background jobs.
11
- - The only allowed background job is \`.ai/tools/run-long-check\` when used through the \`vcm-long-running-validation\` skill.
12
- - \`vcm-long-running-validation\` has a hard maximum timeout of 60 minutes.
13
- - Do not run or suggest operations expected to exceed 60 minutes without user approval.
14
- - Design every single validation/build operation to complete within 60 minutes; split anything larger before running it.
15
- - Do not end the current turn only to wait for a long-running shell callback.
11
+ - Never run the Bash tool with \`run_in_background: true\`. Never detach a process with \`nohup\`, \`setsid\`, \`disown\`, or a trailing \`&\`. VCM denies these calls.
12
+ - The only sanctioned long-running mechanism is the \`vcm-long-running-validation\` skill: \`.ai/tools/run-long-check\` plus \`.ai/tools/watch-job\`.
13
+ - The moment a command might run longer than 2 minutes, switch to that skill instead of running the command directly.
14
+ - While a job is running, stay in the current turn and keep calling \`.ai/tools/watch-job\` until it reports a terminal result; VCM blocks turn-end while a job is running, and a job without a live watcher is killed automatically.
15
+ - Hard ceiling: 60 minutes per job, enforced by the job worker. Do not run or suggest operations expected to exceed 60 minutes without user approval; split larger work first.
16
16
 
17
17
  ## VCM Durable Project Docs
18
18
 
@@ -33,23 +33,19 @@ export function renderRootClaudeHarnessRules() {
33
33
  - Keep role outputs under \`.ai/vcm/handoffs/\`.
34
34
  - Runtime task records and handoffs under \`.ai/vcm/\` are temporary. Durable facts must move into code, tests, PR text, commit history, or long-term docs.
35
35
  - Record current-task unresolved findings in \`.ai/vcm/handoffs/known-issues.md\`.
36
- - Use the \`vcm-route-message\` skill when writing or updating VCM role messages.
37
- - Use the \`vcm-final-acceptance\` skill before declaring a VCM-managed task complete.
38
36
 
39
37
  ## VCM Validation Levels
40
38
 
41
39
  - L0 fast checks: format, lint, typecheck, boundary, dependency, or other cheap project checks.
42
- - L1 focused unit / contract checks: changed behavior, public function contracts, and direct regressions.
40
+ - L1 coder unit checks: changed behavior and direct regressions through project-defined unit tests.
43
41
  - L2 module / integration checks: module-level behavior, API contracts, service integration, persistence, or cross-file wiring.
44
42
  - L3 smoke E2E checks: core user journeys or critical browser/API flows.
45
43
  - L4 full regression / release checks are release-only unless explicitly requested.
46
- - Coder normally runs baseline unit-level checks plus \`check-fast\`; reviewer decides final validation sufficiency and whether L2/L3 is required.
47
44
 
48
45
  ## VCM Worktree Policy
49
46
 
50
47
  - Use one branch, one worktree, one handoff directory, and one PR or final patch per VCM-managed task.
51
48
  - Roles work sequentially in the same task worktree.
52
49
  - If \`git status\` shows uncommitted changes, commit them before handing off to another role.
53
-
54
50
  `;
55
51
  }
@@ -1,21 +1,49 @@
1
1
  export function renderCoderHarnessRules() {
2
- return `## VCM Coder Rules
3
-
4
- - Implement only the approved task scope and architecture plan.
5
- - Read \`CLAUDE.md\`, the route message or durable plan, architecture plan, relevant project docs, and cited handoff artifacts before editing.
6
- - Add or update direct validation for changed behavior when practical.
7
- - Record confirmed out-of-scope implementation issues in \`.ai/vcm/handoffs/known-issues.md\`; do not update \`docs/known-issues.md\` directly.
8
- - Do not update durable docs unless architect explicitly assigns a mechanical edit.
9
- - Do not change module boundaries, public contracts, dependency direction, durable docs, or test strategy without project-manager/architect Replan.
2
+ return `
3
+ ## VCM Coder Rules
4
+
5
+ ### Role Scope
6
+
7
+ - Own implementation and baseline implementation tests inside the approved task scope, current phase, role message, and architecture plan.
8
+ - Do not decide architecture, module boundaries, public contracts, dependency direction, durable docs updates, or final test adequacy.
9
+
10
+ ### Inputs
11
+
12
+ - Before editing, read the role message, the architecture plan, current phase when present, affected code/tests, and validation instructions from the role message or project docs.
13
+ - Read durable architecture/module/security/dependency docs only when the architecture plan or role message references them.
14
+ - Stop before editing when the architecture plan, role message, allowed write scope, public contract, or validation expectation is missing or unclear; reply to project-manager instead of inferring it.
15
+ - Use \`.ai/generated/module-index.json\` to locate approved module source and test files.
16
+ - Use \`.ai/generated/public-surface.json\` to avoid accidental public API drift.
17
+
18
+ ### Implementation
19
+
20
+ - Make only the implementation changes needed for the approved scope.
10
21
  - Do not weaken, delete, or skip tests to make validation pass.
11
- - Run \`.ai/tools/check-fast\` before handoff.
12
- - Run focused validation from \`CLAUDE.md\`, the role command, and project docs when changed behavior requires it; record skipped checks with reasons.
13
- - Stop and reply to project-manager when the approved plan conflicts with code reality.
14
- - Stop before editing when the required architecture plan, route message, approved scope, public contract, or validation expectation is missing or unclear.
15
- - If implementation requires architecture, public contract, dependency, durable docs, or test strategy changes, stop and request Replan instead of continuing.
16
- - Reply through \`.ai/vcm/handoffs/messages/coder-project-manager.md\`.
17
- - Use the \`vcm-route-message\` skill when writing or updating a role message.
18
- - After writing a route file, end the current turn immediately; do not poll, loop, or wait for another role's answer.
19
- - For slow validation, use the \`vcm-long-running-validation\` skill instead of ending the turn to wait for a shell callback.
22
+ - Record confirmed out-of-scope issues found during implementation in \`.ai/vcm/handoffs/known-issues.md\`.
23
+
24
+ ### Generated Context
25
+
26
+ - Regenerate \`.ai/generated/module-index.json\` with \`.ai/tools/generate-module-index\` after module, manifest, source-file, or test-file changes.
27
+ - Regenerate \`.ai/generated/public-surface.json\` with \`.ai/tools/generate-public-surface\` after crate-external public API or public visibility changes.
28
+ - Do not hand-edit generated context files.
29
+
30
+ ### Baseline Tests
31
+
32
+ - Add or update baseline unit tests for changed behavior: direct unit coverage, key happy path, key boundary or failure path when applicable.
33
+ - Coder validation is limited to baseline unit-level or fast L1/L2 checks; do not do smoke, integration, or E2E testing.
34
+ - If baseline unit tests cannot be run, explain the reason in the route message to project-manager.
35
+
36
+ ### Replan And Continuation
37
+
38
+ - Stop and request Replan through project-manager when the approved plan conflicts with code reality.
39
+ - Request Replan only for architecture, public contract, dependency, phase-boundary, validation-boundary, or durable-doc changes that must be decided before implementation can continue.
40
+ - Do not request Replan because of workload, session length, or context size.
41
+ - If the plan remains valid but the assigned work cannot be finished in this turn, include completed work, remaining work, validation state, and next continuation step in the route message, then ask project-manager for continuation.
42
+ - If implementation exposes a broad testing gap beyond baseline unit tests, report it to project-manager for reviewer follow-up.
43
+
44
+ ### Background Jobs
45
+
46
+ - Never background a Bash command: no \`run_in_background\`, \`nohup\`, \`setsid\`, \`disown\`, or trailing \`&\`.
47
+ - For any command that may exceed 2 minutes, use the \`vcm-long-running-validation\` skill and stay in the turn, re-running \`.ai/tools/watch-job\` until it reports a terminal result.
20
48
  `;
21
49
  }
@@ -1,7 +1,8 @@
1
1
  export function renderGitignoreHarnessRules() {
2
2
  return [
3
- "# VCM local app state, task metadata, session records, and task worktrees.",
3
+ "# VCM runtime task metadata, handoffs, session records, logs, and task worktrees.",
4
4
  ".ai/vcm/",
5
- ".claude/worktrees/"
5
+ ".claude/worktrees/",
6
+ ".ai/tools/__pycache__/"
6
7
  ].join("\n");
7
8
  }