vibe-coding-master 0.0.17 → 0.2.1
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 +60 -28
- package/dist/backend/api/artifact-routes.js +5 -5
- package/dist/backend/api/harness-routes.js +8 -0
- package/dist/backend/server.js +8 -2
- package/dist/backend/services/artifact-service.js +12 -12
- package/dist/backend/services/harness-service.js +586 -5
- package/dist/backend/services/project-service.js +4 -1
- package/dist/backend/services/session-service.js +1 -3
- package/dist/backend/services/task-service.js +16 -17
- package/dist/backend/templates/handoff.js +64 -26
- package/dist/backend/templates/harness/architect-agent.js +42 -12
- package/dist/backend/templates/harness/claude-root.js +42 -18
- package/dist/backend/templates/harness/coder-agent.js +15 -11
- package/dist/backend/templates/harness/known-issues-doc.js +22 -0
- package/dist/backend/templates/harness/project-manager-agent.js +66 -16
- package/dist/backend/templates/harness/pull-request-template.js +29 -0
- package/dist/backend/templates/harness/reviewer-agent.js +40 -12
- package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +105 -0
- package/dist/backend/templates/harness/vcm-harness-bootstrap-skill.js +78 -0
- package/dist/backend/templates/harness/vcm-long-running-validation-skill.js +50 -0
- package/dist/backend/templates/harness/vcm-route-message-skill.js +86 -0
- package/dist/backend/templates/message-envelope.js +1 -0
- package/dist/backend/templates/role-command.js +7 -1
- package/dist/shared/validation/artifact-check.js +14 -9
- package/dist-frontend/assets/index-BmpJxnNx.css +32 -0
- package/dist-frontend/assets/index-CGUkhVAP.js +90 -0
- package/dist-frontend/index.html +2 -2
- package/docs/cc-best-practices.md +433 -192
- package/docs/full-harness-baseline.md +258 -0
- package/docs/product-design.md +9 -9
- package/docs/v0.2-implementation-plan.md +379 -0
- package/docs/vcm-cc-best-practices.md +449 -0
- package/package.json +3 -1
- package/scripts/harness-tools/generate-module-index +298 -0
- package/scripts/harness-tools/generate-public-surface +692 -0
- package/scripts/install-vcm-harness.mjs +1690 -0
- package/scripts/uninstall-vcm-harness.mjs +490 -0
- package/scripts/verify-package.mjs +4 -0
- package/dist-frontend/assets/index-D40qaonx.css +0 -32
- package/dist-frontend/assets/index-DK2F4LFT.js +0 -90
- package/docs/v1-architecture-design.md +0 -1014
- package/docs/v1-implementation-plan.md +0 -1379
|
@@ -1,10 +1,26 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
1
3
|
import path from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
2
5
|
import { renderArchitectHarnessRules } from "../templates/harness/architect-agent.js";
|
|
3
6
|
import { renderCoderHarnessRules } from "../templates/harness/coder-agent.js";
|
|
4
7
|
import { renderRootClaudeHarnessRules } from "../templates/harness/claude-root.js";
|
|
5
8
|
import { renderGitignoreHarnessRules } from "../templates/harness/gitignore.js";
|
|
9
|
+
import { renderKnownIssuesDocHarnessRules } from "../templates/harness/known-issues-doc.js";
|
|
6
10
|
import { renderProjectManagerHarnessRules } from "../templates/harness/project-manager-agent.js";
|
|
11
|
+
import { renderPullRequestTemplateHarnessRules } from "../templates/harness/pull-request-template.js";
|
|
7
12
|
import { renderReviewerHarnessRules } from "../templates/harness/reviewer-agent.js";
|
|
13
|
+
import { renderVcmFinalAcceptanceSkillRules } from "../templates/harness/vcm-final-acceptance-skill.js";
|
|
14
|
+
import { renderVcmHarnessBootstrapSkillRules } from "../templates/harness/vcm-harness-bootstrap-skill.js";
|
|
15
|
+
import { renderVcmLongRunningValidationSkillRules } from "../templates/harness/vcm-long-running-validation-skill.js";
|
|
16
|
+
import { renderVcmRouteMessageSkillRules } from "../templates/harness/vcm-route-message-skill.js";
|
|
17
|
+
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
18
|
+
import { VcmError } from "../errors.js";
|
|
19
|
+
const execFileAsync = promisify(execFile);
|
|
20
|
+
const BOOTSTRAP_TASK_SLUG = "__vcm-harness-bootstrap__";
|
|
21
|
+
const BOOTSTRAP_RUNTIME_ROLE = "project-manager";
|
|
22
|
+
const BOOTSTRAP_LOG_PATH = ".ai/vcm/bootstrap/bootstrap.log";
|
|
23
|
+
const BOOTSTRAP_SESSION_PATH = ".ai/vcm/bootstrap/session.json";
|
|
8
24
|
export const VCM_HARNESS_VERSION = 1;
|
|
9
25
|
const MANAGED_BLOCK_PATTERN = /<!-- VCM:BEGIN(?:\s+version=(\d+))? -->[\s\S]*?<!-- VCM:END -->/m;
|
|
10
26
|
const HASH_MANAGED_BLOCK_PATTERN = /# VCM:BEGIN(?:\s+version=(\d+))?\n[\s\S]*?# VCM:END/m;
|
|
@@ -25,6 +41,50 @@ const HARNESS_FILES = [
|
|
|
25
41
|
commentStyle: "hash",
|
|
26
42
|
renderRules: renderGitignoreHarnessRules
|
|
27
43
|
},
|
|
44
|
+
{
|
|
45
|
+
kind: "docs-known-issues",
|
|
46
|
+
path: "docs/known-issues.md",
|
|
47
|
+
title: "Known Issues",
|
|
48
|
+
renderRules: renderKnownIssuesDocHarnessRules
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
kind: "pull-request-template",
|
|
52
|
+
path: ".github/pull_request_template.md",
|
|
53
|
+
title: "Pull Request Template",
|
|
54
|
+
renderRules: renderPullRequestTemplateHarnessRules
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
kind: "skill-vcm-route-message",
|
|
58
|
+
path: ".claude/skills/vcm-route-message/SKILL.md",
|
|
59
|
+
title: "VCM Route Message Skill",
|
|
60
|
+
frontmatter: renderSkillFrontmatter("vcm-route-message", "Use when a VCM role needs to hand off work, ask a question, report a result, report a blocker, or raise a finding to another VCM role."),
|
|
61
|
+
ownership: "whole-file",
|
|
62
|
+
renderRules: renderVcmRouteMessageSkillRules
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
kind: "skill-vcm-final-acceptance",
|
|
66
|
+
path: ".claude/skills/vcm-final-acceptance/SKILL.md",
|
|
67
|
+
title: "VCM Final Acceptance Skill",
|
|
68
|
+
frontmatter: renderSkillFrontmatter("vcm-final-acceptance", "Use when project-manager is ready to decide whether a VCM-managed task can be accepted, returned for follow-up, or blocked for a decision."),
|
|
69
|
+
ownership: "whole-file",
|
|
70
|
+
renderRules: renderVcmFinalAcceptanceSkillRules
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
kind: "skill-vcm-harness-bootstrap",
|
|
74
|
+
path: ".claude/skills/vcm-harness-bootstrap/SKILL.md",
|
|
75
|
+
title: "VCM Harness Bootstrap Skill",
|
|
76
|
+
frontmatter: renderSkillFrontmatter("vcm-harness-bootstrap", "Use when VCM needs AI-assisted project understanding to finish or refresh project-specific harness content."),
|
|
77
|
+
ownership: "whole-file",
|
|
78
|
+
renderRules: renderVcmHarnessBootstrapSkillRules
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
kind: "skill-vcm-long-running-validation",
|
|
82
|
+
path: ".claude/skills/vcm-long-running-validation/SKILL.md",
|
|
83
|
+
title: "VCM Long-Running Validation Skill",
|
|
84
|
+
frontmatter: renderSkillFrontmatter("vcm-long-running-validation", "Use for builds, browser checks, E2E tests, release suites, or any validation command that may take long enough for shell-completion callbacks to become unreliable."),
|
|
85
|
+
ownership: "whole-file",
|
|
86
|
+
renderRules: renderVcmLongRunningValidationSkillRules
|
|
87
|
+
},
|
|
28
88
|
{
|
|
29
89
|
kind: "agent-project-manager",
|
|
30
90
|
path: ".claude/agents/project-manager.md",
|
|
@@ -36,14 +96,14 @@ const HARNESS_FILES = [
|
|
|
36
96
|
kind: "agent-architect",
|
|
37
97
|
path: ".claude/agents/architect.md",
|
|
38
98
|
title: "Architect Agent",
|
|
39
|
-
frontmatter: renderAgentFrontmatter("architect", "VCM architecture role for plans, module boundaries, public contracts,
|
|
99
|
+
frontmatter: renderAgentFrontmatter("architect", "VCM architecture role for plans, module boundaries, public contracts, verifiable behavior, and docs sync."),
|
|
40
100
|
renderRules: renderArchitectHarnessRules
|
|
41
101
|
},
|
|
42
102
|
{
|
|
43
103
|
kind: "agent-coder",
|
|
44
104
|
path: ".claude/agents/coder.md",
|
|
45
105
|
title: "Coder Agent",
|
|
46
|
-
frontmatter: renderAgentFrontmatter("coder", "VCM implementation role for scoped code changes
|
|
106
|
+
frontmatter: renderAgentFrontmatter("coder", "VCM implementation role for scoped code changes and focused tests."),
|
|
47
107
|
renderRules: renderCoderHarnessRules
|
|
48
108
|
},
|
|
49
109
|
{
|
|
@@ -55,12 +115,16 @@ const HARNESS_FILES = [
|
|
|
55
115
|
}
|
|
56
116
|
];
|
|
57
117
|
export function createHarnessService(deps) {
|
|
118
|
+
const now = deps.now ?? (() => new Date().toISOString());
|
|
58
119
|
return {
|
|
59
120
|
async getHarnessStatus(repoRoot) {
|
|
60
121
|
const analyses = await analyzeHarnessFiles(deps.fs, repoRoot);
|
|
61
122
|
return renderHarnessStatus(analyses);
|
|
62
123
|
},
|
|
63
124
|
async applyHarness(repoRoot) {
|
|
125
|
+
if (deps.runFixedInstaller) {
|
|
126
|
+
return deps.runFixedInstaller(repoRoot);
|
|
127
|
+
}
|
|
64
128
|
const analyses = await analyzeHarnessFiles(deps.fs, repoRoot);
|
|
65
129
|
const changedFiles = [];
|
|
66
130
|
for (const analysis of analyses) {
|
|
@@ -79,6 +143,78 @@ export function createHarnessService(deps) {
|
|
|
79
143
|
? "VCM Harness is already up to date."
|
|
80
144
|
: "VCM Harness updated. Review these files and commit the harness changes before starting long-running work."
|
|
81
145
|
};
|
|
146
|
+
},
|
|
147
|
+
async getBootstrapStatus(repoRoot) {
|
|
148
|
+
return getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
149
|
+
},
|
|
150
|
+
async startHarnessBootstrap(repoRoot, input = {}) {
|
|
151
|
+
if (!deps.runtime || !deps.projectService) {
|
|
152
|
+
throw new VcmError({
|
|
153
|
+
code: "HARNESS_BOOTSTRAP_UNAVAILABLE",
|
|
154
|
+
message: "Harness bootstrap sessions are not available in this VCM runtime.",
|
|
155
|
+
statusCode: 501
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
const currentStatus = await getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
159
|
+
if (currentStatus.session?.status === "running") {
|
|
160
|
+
return {
|
|
161
|
+
status: currentStatus,
|
|
162
|
+
session: currentStatus.session,
|
|
163
|
+
prompt: buildHarnessBootstrapPrompt(repoRoot)
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (!currentStatus.canStart) {
|
|
167
|
+
throw new VcmError({
|
|
168
|
+
code: "HARNESS_BOOTSTRAP_NOT_READY",
|
|
169
|
+
message: "Install the fixed VCM harness before running harness bootstrap.",
|
|
170
|
+
statusCode: 409
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
const config = await deps.projectService.loadConfig(repoRoot);
|
|
174
|
+
const claudeSessionId = randomUUID();
|
|
175
|
+
const command = buildClaudeStartCommand(config.claudeCommand, "default", claudeSessionId);
|
|
176
|
+
const logPath = path.join(repoRoot, BOOTSTRAP_LOG_PATH);
|
|
177
|
+
const runtimeSession = await deps.runtime.createSession({
|
|
178
|
+
taskSlug: BOOTSTRAP_TASK_SLUG,
|
|
179
|
+
role: BOOTSTRAP_RUNTIME_ROLE,
|
|
180
|
+
command: command.command,
|
|
181
|
+
args: command.args,
|
|
182
|
+
cwd: repoRoot,
|
|
183
|
+
env: {
|
|
184
|
+
VCM_API_URL: deps.apiUrl,
|
|
185
|
+
VCM_TASK_REPO_ROOT: repoRoot,
|
|
186
|
+
VCM_HARNESS_BOOTSTRAP: "1",
|
|
187
|
+
VCM_SESSION_ID: claudeSessionId
|
|
188
|
+
},
|
|
189
|
+
cols: input.cols,
|
|
190
|
+
rows: input.rows,
|
|
191
|
+
logPath
|
|
192
|
+
});
|
|
193
|
+
const timestamp = now();
|
|
194
|
+
const session = {
|
|
195
|
+
id: runtimeSession.id,
|
|
196
|
+
claudeSessionId,
|
|
197
|
+
status: runtimeSession.status === "crashed" ? "crashed" : runtimeSession.status === "exited" ? "exited" : "running",
|
|
198
|
+
command: command.display,
|
|
199
|
+
cwd: repoRoot,
|
|
200
|
+
logPath: BOOTSTRAP_LOG_PATH,
|
|
201
|
+
startedAt: runtimeSession.startedAt,
|
|
202
|
+
updatedAt: timestamp,
|
|
203
|
+
lastOutputAt: runtimeSession.lastOutputAt,
|
|
204
|
+
exitCode: runtimeSession.exitCode
|
|
205
|
+
};
|
|
206
|
+
await persistHarnessBootstrapSession(deps.fs, repoRoot, session);
|
|
207
|
+
const prompt = buildHarnessBootstrapPrompt(repoRoot);
|
|
208
|
+
await submitTerminalInput(deps.runtime, runtimeSession.id, prompt);
|
|
209
|
+
const nextStatus = await getHarnessBootstrapStatus(deps, repoRoot, now);
|
|
210
|
+
return {
|
|
211
|
+
status: {
|
|
212
|
+
...nextStatus,
|
|
213
|
+
session
|
|
214
|
+
},
|
|
215
|
+
session,
|
|
216
|
+
prompt
|
|
217
|
+
};
|
|
82
218
|
}
|
|
83
219
|
};
|
|
84
220
|
}
|
|
@@ -92,8 +228,15 @@ async function analyzeHarnessFiles(fs, repoRoot) {
|
|
|
92
228
|
}
|
|
93
229
|
async function analyzeHarnessFile(fs, repoRoot, definition) {
|
|
94
230
|
const absolutePath = resolveHarnessPath(repoRoot, definition.path);
|
|
95
|
-
const
|
|
96
|
-
|
|
231
|
+
const expectedContent = definition.ownership === "whole-file"
|
|
232
|
+
? renderWholeHarnessFile(definition)
|
|
233
|
+
: undefined;
|
|
234
|
+
const expectedBlock = definition.ownership === "whole-file"
|
|
235
|
+
? undefined
|
|
236
|
+
: renderManagedBlock(definition, definition.renderRules());
|
|
237
|
+
const managedBlockPattern = definition.ownership === "whole-file"
|
|
238
|
+
? undefined
|
|
239
|
+
: getManagedBlockPattern(definition);
|
|
97
240
|
const exists = await fs.pathExists(absolutePath);
|
|
98
241
|
if (!exists) {
|
|
99
242
|
return {
|
|
@@ -110,10 +253,34 @@ async function analyzeHarnessFile(fs, repoRoot, definition) {
|
|
|
110
253
|
action: "create",
|
|
111
254
|
reason: "File is missing; VCM will create a recommended default."
|
|
112
255
|
},
|
|
113
|
-
nextContent: renderNewHarnessFile(definition, expectedBlock)
|
|
256
|
+
nextContent: expectedContent ?? renderNewHarnessFile(definition, expectedBlock ?? "")
|
|
114
257
|
};
|
|
115
258
|
}
|
|
116
259
|
const currentContent = await fs.readText(absolutePath);
|
|
260
|
+
if (expectedContent !== undefined) {
|
|
261
|
+
const action = currentContent === expectedContent ? "ok" : "update";
|
|
262
|
+
return {
|
|
263
|
+
definition,
|
|
264
|
+
status: {
|
|
265
|
+
kind: definition.kind,
|
|
266
|
+
path: definition.path,
|
|
267
|
+
exists: true,
|
|
268
|
+
hasManagedBlock: false,
|
|
269
|
+
action
|
|
270
|
+
},
|
|
271
|
+
plannedChange: action === "ok"
|
|
272
|
+
? undefined
|
|
273
|
+
: {
|
|
274
|
+
path: definition.path,
|
|
275
|
+
action,
|
|
276
|
+
reason: "VCM whole-file content differs from the recommended baseline."
|
|
277
|
+
},
|
|
278
|
+
nextContent: action === "ok" ? undefined : expectedContent
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if (!expectedBlock || !managedBlockPattern) {
|
|
282
|
+
throw new Error(`Invalid harness definition: ${definition.path}`);
|
|
283
|
+
}
|
|
117
284
|
const match = currentContent.match(managedBlockPattern);
|
|
118
285
|
if (!match) {
|
|
119
286
|
return {
|
|
@@ -192,6 +359,9 @@ function renderNewHarnessFile(definition, block) {
|
|
|
192
359
|
: "";
|
|
193
360
|
return `${frontmatter}# ${definition.title}\n\n${block}\n`;
|
|
194
361
|
}
|
|
362
|
+
function renderWholeHarnessFile(definition) {
|
|
363
|
+
return ensureTrailingNewline(renderNewHarnessFile(definition, definition.renderRules().trimEnd()));
|
|
364
|
+
}
|
|
195
365
|
async function analyzeClaudeSettingsFile(fs, repoRoot) {
|
|
196
366
|
const definition = {
|
|
197
367
|
kind: "claude-settings",
|
|
@@ -303,6 +473,417 @@ function isPlainObject(value) {
|
|
|
303
473
|
function renderAgentFrontmatter(name, description) {
|
|
304
474
|
return `---\nname: ${name}\ndescription: ${description}\ntools: Read, Grep, Glob, Bash, Edit, Write\n---`;
|
|
305
475
|
}
|
|
476
|
+
function renderSkillFrontmatter(name, description) {
|
|
477
|
+
return `---\nname: ${name}\ndescription: ${description}\n---`;
|
|
478
|
+
}
|
|
306
479
|
function resolveHarnessPath(repoRoot, relativePath) {
|
|
307
480
|
return path.join(repoRoot, relativePath);
|
|
308
481
|
}
|
|
482
|
+
function ensureTrailingNewline(content) {
|
|
483
|
+
return content.endsWith("\n") ? content : `${content}\n`;
|
|
484
|
+
}
|
|
485
|
+
async function getHarnessBootstrapStatus(deps, repoRoot, now) {
|
|
486
|
+
const moduleIndex = await readOptionalJsonObject(deps.fs, repoRoot, ".ai/generated/module-index.json");
|
|
487
|
+
const checks = [
|
|
488
|
+
await checkFixedHarness(deps.fs, repoRoot),
|
|
489
|
+
await checkProjectContext(deps.fs, repoRoot),
|
|
490
|
+
checkGeneratedJson(moduleIndex, ".ai/generated/module-index.json", "module-index", "Module index"),
|
|
491
|
+
checkGeneratedJson(await readOptionalJsonObject(deps.fs, repoRoot, ".ai/generated/public-surface.json"), ".ai/generated/public-surface.json", "public-surface", "Public surface"),
|
|
492
|
+
await checkFilledMarkdown(deps.fs, repoRoot, "docs/ARCHITECTURE.md", "Project architecture", "project-architecture"),
|
|
493
|
+
await checkModuleArchitectureDocs(deps.fs, repoRoot, moduleIndex),
|
|
494
|
+
await checkFilledMarkdown(deps.fs, repoRoot, "docs/TESTING.md", "Testing doc", "testing-doc")
|
|
495
|
+
];
|
|
496
|
+
const session = await getCurrentHarnessBootstrapSession(deps, repoRoot, now);
|
|
497
|
+
const fixedHarnessReady = checks[0]?.status === "ok";
|
|
498
|
+
const projectChecks = checks.slice(1);
|
|
499
|
+
const projectComplete = projectChecks.every((check) => check.status === "ok");
|
|
500
|
+
const projectStarted = projectChecks.some((check) => check.status === "ok" || check.status === "incomplete");
|
|
501
|
+
const status = !fixedHarnessReady
|
|
502
|
+
? "not_ready"
|
|
503
|
+
: session?.status === "running"
|
|
504
|
+
? "running"
|
|
505
|
+
: projectComplete
|
|
506
|
+
? "complete"
|
|
507
|
+
: projectStarted
|
|
508
|
+
? "incomplete"
|
|
509
|
+
: "not_started";
|
|
510
|
+
return {
|
|
511
|
+
status,
|
|
512
|
+
canStart: fixedHarnessReady && session?.status !== "running",
|
|
513
|
+
checks,
|
|
514
|
+
session,
|
|
515
|
+
warnings: bootstrapWarnings(status, checks, session)
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
async function checkFixedHarness(fs, repoRoot) {
|
|
519
|
+
const requiredPaths = [
|
|
520
|
+
"CLAUDE.md",
|
|
521
|
+
".ai/vcm-harness-manifest.json",
|
|
522
|
+
".claude/skills/vcm-harness-bootstrap/SKILL.md",
|
|
523
|
+
".ai/tools/generate-module-index",
|
|
524
|
+
".ai/tools/generate-public-surface"
|
|
525
|
+
];
|
|
526
|
+
const missing = [];
|
|
527
|
+
for (const relativePath of requiredPaths) {
|
|
528
|
+
if (!await fs.pathExists(resolveHarnessPath(repoRoot, relativePath))) {
|
|
529
|
+
missing.push(relativePath);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
if (missing.length > 0) {
|
|
533
|
+
return {
|
|
534
|
+
key: "fixed-harness",
|
|
535
|
+
label: "Fixed harness",
|
|
536
|
+
status: "missing",
|
|
537
|
+
detail: `Missing ${missing.length}: ${missing.join(", ")}`
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
const rootClaude = await readOptionalText(fs, repoRoot, "CLAUDE.md");
|
|
541
|
+
if (!rootClaude?.includes("<!-- VCM:BEGIN")) {
|
|
542
|
+
return {
|
|
543
|
+
key: "fixed-harness",
|
|
544
|
+
label: "Fixed harness",
|
|
545
|
+
status: "incomplete",
|
|
546
|
+
path: "CLAUDE.md",
|
|
547
|
+
detail: "CLAUDE.md is missing the VCM managed block."
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
return {
|
|
551
|
+
key: "fixed-harness",
|
|
552
|
+
label: "Fixed harness",
|
|
553
|
+
status: "ok",
|
|
554
|
+
detail: "Fixed VCM files are installed."
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
async function checkProjectContext(fs, repoRoot) {
|
|
558
|
+
const content = await readOptionalText(fs, repoRoot, "CLAUDE.md");
|
|
559
|
+
if (!content) {
|
|
560
|
+
return {
|
|
561
|
+
key: "project-context",
|
|
562
|
+
label: "Project context",
|
|
563
|
+
status: "missing",
|
|
564
|
+
path: "CLAUDE.md"
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
const projectOwnedContent = content.split("<!-- VCM:BEGIN")[0]?.trim() ?? "";
|
|
568
|
+
if (!projectOwnedContent) {
|
|
569
|
+
return {
|
|
570
|
+
key: "project-context",
|
|
571
|
+
label: "Project context",
|
|
572
|
+
status: "missing",
|
|
573
|
+
path: "CLAUDE.md",
|
|
574
|
+
detail: "No project-specific content above the VCM managed block."
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
if (!/^## Project Context\b/m.test(projectOwnedContent)) {
|
|
578
|
+
return {
|
|
579
|
+
key: "project-context",
|
|
580
|
+
label: "Project context",
|
|
581
|
+
status: "incomplete",
|
|
582
|
+
path: "CLAUDE.md",
|
|
583
|
+
detail: "Add a Project Context section above the VCM managed block."
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
key: "project-context",
|
|
588
|
+
label: "Project context",
|
|
589
|
+
status: isFilledMarkdown(projectOwnedContent) ? "ok" : "incomplete",
|
|
590
|
+
path: "CLAUDE.md"
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
function checkGeneratedJson(payload, relativePath, kind, label) {
|
|
594
|
+
const key = kind === "module-index" ? "module-index" : "public-surface";
|
|
595
|
+
if (!payload) {
|
|
596
|
+
return {
|
|
597
|
+
key,
|
|
598
|
+
label,
|
|
599
|
+
status: "missing",
|
|
600
|
+
path: relativePath
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
if (payload.kind !== kind) {
|
|
604
|
+
return {
|
|
605
|
+
key,
|
|
606
|
+
label,
|
|
607
|
+
status: "incomplete",
|
|
608
|
+
path: relativePath,
|
|
609
|
+
detail: `Expected kind "${kind}".`
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
if (typeof payload.generatedBy !== "string" || !payload.generatedBy) {
|
|
613
|
+
return {
|
|
614
|
+
key,
|
|
615
|
+
label,
|
|
616
|
+
status: "incomplete",
|
|
617
|
+
path: relativePath,
|
|
618
|
+
detail: "Missing generatedBy."
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
key,
|
|
623
|
+
label,
|
|
624
|
+
status: "ok",
|
|
625
|
+
path: relativePath
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
async function checkFilledMarkdown(fs, repoRoot, relativePath, label, key) {
|
|
629
|
+
const content = await readOptionalText(fs, repoRoot, relativePath);
|
|
630
|
+
if (!content) {
|
|
631
|
+
return {
|
|
632
|
+
key,
|
|
633
|
+
label,
|
|
634
|
+
status: "missing",
|
|
635
|
+
path: relativePath
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
return {
|
|
639
|
+
key,
|
|
640
|
+
label,
|
|
641
|
+
status: isFilledMarkdown(content) ? "ok" : "incomplete",
|
|
642
|
+
path: relativePath,
|
|
643
|
+
detail: isFilledMarkdown(content) ? undefined : "Only a blank or very short template was found."
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
async function checkModuleArchitectureDocs(fs, repoRoot, moduleIndex) {
|
|
647
|
+
if (!moduleIndex) {
|
|
648
|
+
return {
|
|
649
|
+
key: "module-architecture",
|
|
650
|
+
label: "Module architecture docs",
|
|
651
|
+
status: "unknown",
|
|
652
|
+
detail: "Run module-index generation first."
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
const moduleDocs = getModuleArchitectureDocPaths(moduleIndex);
|
|
656
|
+
if (moduleDocs.length === 0) {
|
|
657
|
+
return {
|
|
658
|
+
key: "module-architecture",
|
|
659
|
+
label: "Module architecture docs",
|
|
660
|
+
status: "unknown",
|
|
661
|
+
detail: "module-index.json does not list module architecture docs."
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
let missing = 0;
|
|
665
|
+
let incomplete = 0;
|
|
666
|
+
for (const relativePath of moduleDocs) {
|
|
667
|
+
const content = await readOptionalText(fs, repoRoot, relativePath);
|
|
668
|
+
if (!content) {
|
|
669
|
+
missing += 1;
|
|
670
|
+
}
|
|
671
|
+
else if (!isFilledMarkdown(content)) {
|
|
672
|
+
incomplete += 1;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (missing === 0 && incomplete === 0) {
|
|
676
|
+
return {
|
|
677
|
+
key: "module-architecture",
|
|
678
|
+
label: "Module architecture docs",
|
|
679
|
+
status: "ok",
|
|
680
|
+
detail: `${moduleDocs.length} module docs found.`
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
return {
|
|
684
|
+
key: "module-architecture",
|
|
685
|
+
label: "Module architecture docs",
|
|
686
|
+
status: "incomplete",
|
|
687
|
+
detail: `${missing} missing, ${incomplete} incomplete, ${moduleDocs.length} expected.`
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
function getModuleArchitectureDocPaths(moduleIndex) {
|
|
691
|
+
const layers = Array.isArray(moduleIndex.layers) ? moduleIndex.layers : [];
|
|
692
|
+
const paths = new Set();
|
|
693
|
+
for (const layer of layers) {
|
|
694
|
+
if (!isPlainObject(layer) || !Array.isArray(layer.modules)) {
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
for (const moduleEntry of layer.modules) {
|
|
698
|
+
if (!isPlainObject(moduleEntry)) {
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
const architectureDoc = moduleEntry.architectureDoc;
|
|
702
|
+
if (typeof architectureDoc === "string" && architectureDoc.trim()) {
|
|
703
|
+
paths.add(architectureDoc);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return [...paths].sort();
|
|
708
|
+
}
|
|
709
|
+
function isFilledMarkdown(content) {
|
|
710
|
+
const meaningfulLines = content
|
|
711
|
+
.split(/\r?\n/)
|
|
712
|
+
.map((line) => line.trim())
|
|
713
|
+
.filter((line) => line && !line.startsWith("#"));
|
|
714
|
+
return meaningfulLines.join("\n").length >= 120;
|
|
715
|
+
}
|
|
716
|
+
async function getCurrentHarnessBootstrapSession(deps, repoRoot, now) {
|
|
717
|
+
const persisted = await loadPersistedHarnessBootstrapSession(deps.fs, repoRoot);
|
|
718
|
+
const runtimeSession = deps.runtime
|
|
719
|
+
?.listSessions(BOOTSTRAP_TASK_SLUG)
|
|
720
|
+
.find((session) => session.id === persisted?.id || session.status === "running");
|
|
721
|
+
if (runtimeSession) {
|
|
722
|
+
const session = {
|
|
723
|
+
id: runtimeSession.id,
|
|
724
|
+
claudeSessionId: persisted?.claudeSessionId ?? runtimeSession.id,
|
|
725
|
+
status: runtimeSession.status === "crashed" ? "crashed" : runtimeSession.status === "exited" ? "exited" : "running",
|
|
726
|
+
command: persisted?.command ?? "claude",
|
|
727
|
+
cwd: persisted?.cwd ?? repoRoot,
|
|
728
|
+
logPath: persisted?.logPath ?? BOOTSTRAP_LOG_PATH,
|
|
729
|
+
startedAt: runtimeSession.startedAt,
|
|
730
|
+
updatedAt: now(),
|
|
731
|
+
lastOutputAt: runtimeSession.lastOutputAt,
|
|
732
|
+
exitCode: runtimeSession.exitCode
|
|
733
|
+
};
|
|
734
|
+
await persistHarnessBootstrapSession(deps.fs, repoRoot, session);
|
|
735
|
+
return session;
|
|
736
|
+
}
|
|
737
|
+
if (persisted?.status === "running") {
|
|
738
|
+
return {
|
|
739
|
+
...persisted,
|
|
740
|
+
status: "resumable",
|
|
741
|
+
updatedAt: now()
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
return undefined;
|
|
745
|
+
}
|
|
746
|
+
async function loadPersistedHarnessBootstrapSession(fs, repoRoot) {
|
|
747
|
+
const payload = await readOptionalJsonObject(fs, repoRoot, BOOTSTRAP_SESSION_PATH);
|
|
748
|
+
if (!payload) {
|
|
749
|
+
return undefined;
|
|
750
|
+
}
|
|
751
|
+
if (typeof payload.id !== "string" ||
|
|
752
|
+
typeof payload.claudeSessionId !== "string" ||
|
|
753
|
+
typeof payload.command !== "string" ||
|
|
754
|
+
typeof payload.cwd !== "string" ||
|
|
755
|
+
typeof payload.logPath !== "string" ||
|
|
756
|
+
typeof payload.updatedAt !== "string") {
|
|
757
|
+
return undefined;
|
|
758
|
+
}
|
|
759
|
+
const status = payload.status;
|
|
760
|
+
if (status !== "running" && status !== "exited" && status !== "crashed" && status !== "resumable") {
|
|
761
|
+
return undefined;
|
|
762
|
+
}
|
|
763
|
+
return {
|
|
764
|
+
id: payload.id,
|
|
765
|
+
claudeSessionId: payload.claudeSessionId,
|
|
766
|
+
status,
|
|
767
|
+
command: payload.command,
|
|
768
|
+
cwd: payload.cwd,
|
|
769
|
+
logPath: payload.logPath,
|
|
770
|
+
startedAt: typeof payload.startedAt === "string" ? payload.startedAt : undefined,
|
|
771
|
+
updatedAt: payload.updatedAt,
|
|
772
|
+
lastOutputAt: typeof payload.lastOutputAt === "string" ? payload.lastOutputAt : undefined,
|
|
773
|
+
exitCode: typeof payload.exitCode === "number" || payload.exitCode === null ? payload.exitCode : undefined
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
async function persistHarnessBootstrapSession(fs, repoRoot, session) {
|
|
777
|
+
await fs.writeJsonAtomic(resolveHarnessPath(repoRoot, BOOTSTRAP_SESSION_PATH), session);
|
|
778
|
+
}
|
|
779
|
+
async function readOptionalText(fs, repoRoot, relativePath) {
|
|
780
|
+
const absolutePath = resolveHarnessPath(repoRoot, relativePath);
|
|
781
|
+
try {
|
|
782
|
+
return await fs.readText(absolutePath);
|
|
783
|
+
}
|
|
784
|
+
catch {
|
|
785
|
+
return undefined;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
async function readOptionalJsonObject(fs, repoRoot, relativePath) {
|
|
789
|
+
const content = await readOptionalText(fs, repoRoot, relativePath);
|
|
790
|
+
if (!content?.trim()) {
|
|
791
|
+
return undefined;
|
|
792
|
+
}
|
|
793
|
+
try {
|
|
794
|
+
const parsed = JSON.parse(content);
|
|
795
|
+
return isPlainObject(parsed) ? parsed : undefined;
|
|
796
|
+
}
|
|
797
|
+
catch {
|
|
798
|
+
return undefined;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function bootstrapWarnings(status, checks, session) {
|
|
802
|
+
const warnings = [];
|
|
803
|
+
if (status === "not_ready") {
|
|
804
|
+
warnings.push("Install or update the fixed VCM harness before running harness bootstrap.");
|
|
805
|
+
}
|
|
806
|
+
if (session?.status === "resumable") {
|
|
807
|
+
warnings.push("The previous bootstrap terminal is no longer active; start bootstrap again or finish the remaining files manually.");
|
|
808
|
+
}
|
|
809
|
+
const unknownChecks = checks.filter((check) => check.status === "unknown");
|
|
810
|
+
if (unknownChecks.length > 0) {
|
|
811
|
+
warnings.push(`Some bootstrap checks are waiting on generated context: ${unknownChecks.map((check) => check.label).join(", ")}.`);
|
|
812
|
+
}
|
|
813
|
+
return warnings;
|
|
814
|
+
}
|
|
815
|
+
function buildClaudeStartCommand(command = "claude", permissionMode = "default", claudeSessionId) {
|
|
816
|
+
const args = ["--session-id", claudeSessionId];
|
|
817
|
+
if (permissionMode === "bypassPermissions") {
|
|
818
|
+
args.push("--permission-mode", "bypassPermissions");
|
|
819
|
+
}
|
|
820
|
+
else if (permissionMode === "dangerously-skip-permissions") {
|
|
821
|
+
args.push("--dangerously-skip-permissions");
|
|
822
|
+
}
|
|
823
|
+
return {
|
|
824
|
+
command,
|
|
825
|
+
args,
|
|
826
|
+
display: `${command} ${args.join(" ")}`
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
function buildHarnessBootstrapPrompt(repoRoot) {
|
|
830
|
+
return `Use the vcm-harness-bootstrap skill to finish the VCM harness bootstrap for this repository.
|
|
831
|
+
|
|
832
|
+
Repository root:
|
|
833
|
+
${repoRoot}
|
|
834
|
+
|
|
835
|
+
Required work:
|
|
836
|
+
- Run .ai/tools/generate-module-index when available.
|
|
837
|
+
- Run .ai/tools/generate-public-surface after module-index.json exists.
|
|
838
|
+
- Add or update project-specific Project Context and Project Constraints in CLAUDE.md above the VCM managed block.
|
|
839
|
+
- Fill docs/ARCHITECTURE.md with project-level module overview, responsibilities, relationships, dependency direction, project-wide constraints, and links to module-level architecture docs.
|
|
840
|
+
- Create or update module-level ARCHITECTURE.md files for clear module boundaries listed by module-index.json.
|
|
841
|
+
- Fill docs/TESTING.md with project-native validation levels, commands, test layout, generated-context freshness checks, and known testing gaps.
|
|
842
|
+
|
|
843
|
+
Boundaries:
|
|
844
|
+
- Do not edit product source, product tests, package manifests, lockfiles, deployment config, secrets, or VCM managed blocks.
|
|
845
|
+
- Preserve user-authored content.
|
|
846
|
+
- Do not create new validation wrapper tools.
|
|
847
|
+
|
|
848
|
+
Final response:
|
|
849
|
+
Summarize files reviewed, files updated, generated artifacts, verified claims, inferred claims, unknowns, confirmation-needed items, and suggested validation commands.`;
|
|
850
|
+
}
|
|
851
|
+
export function createScriptFixedHarnessInstaller(scriptPath = path.resolve(process.cwd(), "scripts/install-vcm-harness.mjs")) {
|
|
852
|
+
return async (repoRoot) => {
|
|
853
|
+
try {
|
|
854
|
+
const { stdout, stderr } = await execFileAsync(process.execPath, [scriptPath, repoRoot], {
|
|
855
|
+
cwd: process.cwd(),
|
|
856
|
+
maxBuffer: 10 * 1024 * 1024
|
|
857
|
+
});
|
|
858
|
+
const output = `${String(stdout).trim()}${String(stderr).trim() ? `\n${String(stderr).trim()}` : ""}`.trim();
|
|
859
|
+
return {
|
|
860
|
+
version: VCM_HARNESS_VERSION,
|
|
861
|
+
changedFiles: parseFixedInstallerChanges(String(stdout)),
|
|
862
|
+
message: output || "VCM fixed harness install completed."
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
catch (caught) {
|
|
866
|
+
const error = caught;
|
|
867
|
+
const details = [error.message, String(error.stdout ?? "").trim(), String(error.stderr ?? "").trim()]
|
|
868
|
+
.filter(Boolean)
|
|
869
|
+
.join("\n");
|
|
870
|
+
throw new VcmError({
|
|
871
|
+
code: "HARNESS_INSTALL_FAILED",
|
|
872
|
+
message: "VCM fixed harness install failed.",
|
|
873
|
+
statusCode: 500,
|
|
874
|
+
hint: details || undefined
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
function parseFixedInstallerChanges(output) {
|
|
880
|
+
return output
|
|
881
|
+
.split(/\r?\n/)
|
|
882
|
+
.map((line) => line.match(/^DONE\s+(.+?)\s+-\s+(.+)$/))
|
|
883
|
+
.filter((match) => Boolean(match))
|
|
884
|
+
.map((match) => ({
|
|
885
|
+
path: match[1],
|
|
886
|
+
action: match[2].includes("create") || match[2].includes("created") ? "create" : "update",
|
|
887
|
+
reason: match[2]
|
|
888
|
+
}));
|
|
889
|
+
}
|
|
@@ -30,7 +30,7 @@ export function createProjectService(deps) {
|
|
|
30
30
|
}
|
|
31
31
|
const config = await this.loadConfig(repoRoot);
|
|
32
32
|
await deps.fs.ensureDir(path.join(repoRoot, config.handoffRoot));
|
|
33
|
-
await deps.fs.ensureDir(path.join(repoRoot,
|
|
33
|
+
await deps.fs.ensureDir(path.join(this.getProjectDataRoot(repoRoot), "tasks"));
|
|
34
34
|
await deps.fs.ensureDir(path.join(repoRoot, ".claude", "worktrees"));
|
|
35
35
|
await this.saveConfig(config, true);
|
|
36
36
|
const warnings = [];
|
|
@@ -87,6 +87,9 @@ export function createProjectService(deps) {
|
|
|
87
87
|
},
|
|
88
88
|
getConfigPath(repoRoot) {
|
|
89
89
|
return deps.appSettings.getProjectConfigPath(repoRoot);
|
|
90
|
+
},
|
|
91
|
+
getProjectDataRoot(repoRoot) {
|
|
92
|
+
return path.dirname(deps.appSettings.getProjectConfigPath(repoRoot));
|
|
90
93
|
}
|
|
91
94
|
};
|
|
92
95
|
}
|