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 +17 -4
- package/dist/backend/api/claude-hook-routes.js +7 -1
- package/dist/backend/server.js +3 -1
- package/dist/backend/services/claude-hook-service.js +33 -3
- package/dist/backend/services/harness-service.js +37 -32
- package/dist/backend/services/job-guard-service.js +126 -0
- package/dist/backend/templates/harness/architect-agent.js +19 -8
- package/dist/backend/templates/harness/claude-root.js +7 -11
- package/dist/backend/templates/harness/coder-agent.js +45 -17
- package/dist/backend/templates/harness/gitignore.js +3 -2
- package/dist/backend/templates/harness/project-manager-agent.js +16 -11
- package/dist/backend/templates/harness/reviewer-agent.js +25 -28
- package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +42 -31
- package/dist/backend/templates/harness/vcm-long-running-validation-skill.js +37 -18
- package/docs/full-harness-baseline.md +7 -5
- package/docs/product-design.md +1 -1
- package/docs/vcm-cc-best-practices.md +11 -4
- package/package.json +1 -1
- package/scripts/harness-tools/run-long-check +401 -0
- package/scripts/harness-tools/vcm-bash-guard +107 -0
- package/scripts/harness-tools/watch-job +204 -0
- package/scripts/install-vcm-harness.mjs +93 -387
- package/scripts/uninstall-vcm-harness.mjs +18 -1
- package/scripts/verify-package.mjs +3 -0
- package/dist/backend/templates/harness/known-issues-doc.js +0 -22
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
|
|
415
|
-
`.ai/tools/
|
|
416
|
-
|
|
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 `
|
|
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
|
-
|
|
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);
|
package/dist/backend/server.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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()}
|
|
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()}
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
464
|
-
}) || value.hooks.some((hook) => {
|
|
465
|
-
if (!isPlainObject(hook)) {
|
|
468
|
+
if (typeof hook.command !== "string") {
|
|
466
469
|
return false;
|
|
467
470
|
}
|
|
468
|
-
return
|
|
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
|
|
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
|
|
15
|
-
-
|
|
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
|
-
-
|
|
43
|
-
- Update affected
|
|
44
|
-
- Treat
|
|
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
|
-
-
|
|
11
|
-
- The only
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
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
|
|
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
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
- Do not
|
|
9
|
-
|
|
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
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
}
|