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.
Files changed (42) hide show
  1. package/README.md +60 -28
  2. package/dist/backend/api/artifact-routes.js +5 -5
  3. package/dist/backend/api/harness-routes.js +8 -0
  4. package/dist/backend/server.js +8 -2
  5. package/dist/backend/services/artifact-service.js +12 -12
  6. package/dist/backend/services/harness-service.js +586 -5
  7. package/dist/backend/services/project-service.js +4 -1
  8. package/dist/backend/services/session-service.js +1 -3
  9. package/dist/backend/services/task-service.js +16 -17
  10. package/dist/backend/templates/handoff.js +64 -26
  11. package/dist/backend/templates/harness/architect-agent.js +42 -12
  12. package/dist/backend/templates/harness/claude-root.js +42 -18
  13. package/dist/backend/templates/harness/coder-agent.js +15 -11
  14. package/dist/backend/templates/harness/known-issues-doc.js +22 -0
  15. package/dist/backend/templates/harness/project-manager-agent.js +66 -16
  16. package/dist/backend/templates/harness/pull-request-template.js +29 -0
  17. package/dist/backend/templates/harness/reviewer-agent.js +40 -12
  18. package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +105 -0
  19. package/dist/backend/templates/harness/vcm-harness-bootstrap-skill.js +78 -0
  20. package/dist/backend/templates/harness/vcm-long-running-validation-skill.js +50 -0
  21. package/dist/backend/templates/harness/vcm-route-message-skill.js +86 -0
  22. package/dist/backend/templates/message-envelope.js +1 -0
  23. package/dist/backend/templates/role-command.js +7 -1
  24. package/dist/shared/validation/artifact-check.js +14 -9
  25. package/dist-frontend/assets/index-BmpJxnNx.css +32 -0
  26. package/dist-frontend/assets/index-CGUkhVAP.js +90 -0
  27. package/dist-frontend/index.html +2 -2
  28. package/docs/cc-best-practices.md +433 -192
  29. package/docs/full-harness-baseline.md +258 -0
  30. package/docs/product-design.md +9 -9
  31. package/docs/v0.2-implementation-plan.md +379 -0
  32. package/docs/vcm-cc-best-practices.md +449 -0
  33. package/package.json +3 -1
  34. package/scripts/harness-tools/generate-module-index +298 -0
  35. package/scripts/harness-tools/generate-public-surface +692 -0
  36. package/scripts/install-vcm-harness.mjs +1690 -0
  37. package/scripts/uninstall-vcm-harness.mjs +490 -0
  38. package/scripts/verify-package.mjs +4 -0
  39. package/dist-frontend/assets/index-D40qaonx.css +0 -32
  40. package/dist-frontend/assets/index-DK2F4LFT.js +0 -90
  41. package/docs/v1-architecture-design.md +0 -1014
  42. 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, test contracts, and post-review docs sync."),
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, focused tests, implementation logs, and validation evidence."),
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 expectedBlock = renderManagedBlock(definition, definition.renderRules());
96
- const managedBlockPattern = getManagedBlockPattern(definition);
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, config.stateRoot, "tasks"));
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
  }