vibe-coding-master 0.0.17 → 0.2.0

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