@vibecompass/vibecompass 0.3.1 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,9 @@
1
+ <p align="center">
2
+ <a href="https://vibecompass.dev">
3
+ <img src="https://raw.githubusercontent.com/jack-whimvy/vibecompass/main/brand/logo-a4.png" alt="VibeCompass" height="96" />
4
+ </a>
5
+ </p>
6
+
1
7
  # @vibecompass/vibecompass
2
8
 
3
9
  Local-first project-memory core for [VibeCompass](https://vibecompass.dev).
@@ -243,7 +249,16 @@ npx -y @vibecompass/vibecompass sync-agents --root .compass
243
249
  ```
244
250
 
245
251
  Content outside those markers is preserved. Existing files without markers are
246
- reported as warnings and left untouched.
252
+ reported as warnings and left untouched unless you explicitly adopt them:
253
+
254
+ ```bash
255
+ npx -y @vibecompass/vibecompass sync-agents --root .compass --adopt-existing
256
+ ```
257
+
258
+ Adoption appends a managed VibeCompass block to the end of existing unmarked
259
+ agent files, preserves everything outside the markers, and reports possible
260
+ workflow-overlap lines before you rely on the result. Guided init may offer the
261
+ same adoption step for existing files, but the prompt defaults to no.
247
262
 
248
263
  Connect hosted VibeCompass later when you want browser views, team sync, or hosted docs-review:
249
264
 
@@ -314,6 +329,7 @@ Options:
314
329
  - `--with-workflow`: generate `context.md` plus workflow guide files
315
330
  - `--with-claude`: create a starter `CLAUDE.md` if it does not already exist
316
331
  - `--with-agents`: create a starter `AGENTS.md` if it does not already exist
332
+ - `--adopt-existing-agent-files`: append managed blocks to existing unmarked agent files
317
333
  - `--start-session`: open the first builder session after init
318
334
  - `--session-working-on <text>`: required with `--start-session` outside guided mode
319
335
  - `--session-id <lane-id>`: required with `--start-session`; names the first builder lane
@@ -327,11 +343,14 @@ Guided init can also record workflow defaults in
327
343
  `project.yaml.metadata.workflow`, including whether close-session should
328
344
  include a Git publish step and which remote name to use for that step.
329
345
 
330
- Guided init is inference-first for the common single-repo case: it reads
331
- `package.json`, detects the current Git `origin`, asks you to confirm the
332
- detected repo, and maps a friendly setup goal to the stored project mode.
333
- Hosted credentials are deferred by default; run `vibecompass connect-hosted`
334
- when you are ready to connect the local root to `vibecompass.dev`.
346
+ Guided init is inference-first for common repo layouts: it reads `package.json`,
347
+ detects the current Git `origin` when you run it inside a repo, or scans a
348
+ parent workspace for immediate child repos when the parent is not itself a Git
349
+ checkout. It asks once before using detected repos, maps a friendly setup goal
350
+ to the stored project mode, places single-child repo memory under that child
351
+ repo, and uses workspace-root placement for multi-repo parent folders. Hosted
352
+ credentials are deferred by default; run `vibecompass connect-hosted` when you
353
+ are ready to connect the local root to `vibecompass.dev`.
335
354
 
336
355
  ### `vibecompass connect-hosted`
337
356
 
@@ -437,12 +456,13 @@ Options:
437
456
  - `--root <path>`: project-memory root; defaults to `.compass`
438
457
  - `--tooling-root <path>`: directory where agent files are written; defaults to cwd
439
458
  - `--format <name>`: optional format filter: `claude_md`, `agents_md`, `cursor_rules`, or `copilot_instructions`
459
+ - `--adopt-existing`: append managed blocks to existing unmarked files
440
460
  - `--dry-run`: show planned writes without changing files
441
461
 
442
462
  This command generates enabled agent-instruction files from canonical project
443
463
  memory. It creates missing files with managed markers, replaces existing
444
464
  managed regions, preserves content outside markers, and warns instead of
445
- overwriting existing unmarked files.
465
+ overwriting existing unmarked files unless `--adopt-existing` is supplied.
446
466
 
447
467
  ### `vibecompass docs-review`
448
468
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecompass/vibecompass",
3
- "version": "0.3.1",
3
+ "version": "0.3.4",
4
4
  "description": "Local-first project-memory core for VibeCompass",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -78,6 +78,14 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
78
78
  io.stdout.write(`Left existing ${filePath} untouched\n`);
79
79
  }
80
80
 
81
+ if (initPlan.agentFileSyncPlan) {
82
+ const agentFileSync = await syncAgentInstructionFiles({
83
+ ...initPlan.agentFileSyncPlan,
84
+ ...(runtime.cwd ? { cwd: runtime.cwd } : {}),
85
+ });
86
+ writeAgentFileSyncResult(io, agentFileSync);
87
+ }
88
+
81
89
  if (initPlan.sessionPlan) {
82
90
  const sessionResult = await startProjectSession({
83
91
  rootDir: initPlan.initOptions.rootDir,
@@ -402,6 +410,11 @@ export function parseCliArgs(argv) {
402
410
  continue;
403
411
  }
404
412
 
413
+ if (token === '--adopt-existing-agent-files') {
414
+ parsed.adoptExistingAgentFiles = true;
415
+ continue;
416
+ }
417
+
405
418
  if (token === '--start-session') {
406
419
  parsed.startSession = true;
407
420
  continue;
@@ -508,6 +521,7 @@ export function parseCliArgs(argv) {
508
521
  sessionId: parsed.sessionId,
509
522
  closeSessionGitPublish: parsed.closeSessionGitPublish,
510
523
  closeSessionGitRemote: parsed.closeSessionGitRemote,
524
+ adoptExistingAgentFiles: parsed.adoptExistingAgentFiles,
511
525
  ...(parsed.withWorkflow || parsed.withClaude || parsed.withAgents
512
526
  ? {
513
527
  bootstrap: {
@@ -804,6 +818,11 @@ function parseSyncAgentsArgs(argv) {
804
818
  continue;
805
819
  }
806
820
 
821
+ if (token === '--adopt-existing') {
822
+ parsed.adoptExisting = true;
823
+ continue;
824
+ }
825
+
807
826
  if (!token.startsWith('--')) {
808
827
  throw new Error(`Unexpected argument "${token}".`);
809
828
  }
@@ -1087,6 +1106,10 @@ function writeAgentFileSyncResult(io, result) {
1087
1106
  const suffix = item.warning ? ` — ${item.warning}` : '';
1088
1107
  const stream = item.warning ? io.stderr : io.stdout;
1089
1108
  stream.write(`- ${item.relativePath}: ${item.status}${suffix}\n`);
1109
+ if (item.conflicts?.length > 0) {
1110
+ const lineNumbers = item.conflicts.map((conflict) => conflict.line).join(', ');
1111
+ io.stderr.write(` possible workflow overlap on lines ${lineNumbers}; review existing instructions if behavior conflicts.\n`);
1112
+ }
1090
1113
  }
1091
1114
  }
1092
1115
 
@@ -1138,6 +1161,7 @@ function usageText() {
1138
1161
  ' --with-workflow Scaffold context.md and workflow guide files',
1139
1162
  ' --with-claude Create a starter CLAUDE.md if missing',
1140
1163
  ' --with-agents Create a starter AGENTS.md if missing',
1164
+ ' --adopt-existing-agent-files Adopt existing unmarked agent files after init',
1141
1165
  ' --start-session Open the first builder session after init',
1142
1166
  ' --session-working-on <text> Required with --start-session outside guided mode',
1143
1167
  ' --session-id <lane-id> Required with --start-session; names the first builder lane',
@@ -1187,6 +1211,7 @@ function usageText() {
1187
1211
  ' --root <path> Project-memory root. Defaults to .compass',
1188
1212
  ' --tooling-root <path> Directory where agent files are written. Defaults to cwd',
1189
1213
  ' --format <name> Optional format: claude_md, agents_md, cursor_rules, copilot_instructions',
1214
+ ' --adopt-existing Append managed blocks to existing unmarked files',
1190
1215
  ' --dry-run Show planned writes without changing files',
1191
1216
  '',
1192
1217
  'Sync transport options:',
@@ -17,6 +17,17 @@ const FORMATS = [
17
17
  ];
18
18
 
19
19
  const FORMAT_MAP = new Map(FORMATS.map((format) => [format.name, format]));
20
+ const CONFLICT_SCAN_PATTERNS = [
21
+ 'session',
22
+ 'handoff',
23
+ 'decisions',
24
+ 'wip.md',
25
+ 'review handoff',
26
+ 'close-session',
27
+ 'address review',
28
+ 'builder',
29
+ 'reviewer',
30
+ ];
20
31
 
21
32
  export async function syncAgentInstructionFiles(options = {}) {
22
33
  const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
@@ -26,6 +37,8 @@ export async function syncAgentInstructionFiles(options = {}) {
26
37
  : cwd;
27
38
  const requestedFormat = normalizeOptionalString(options.format);
28
39
  const dryRun = Boolean(options.dryRun);
40
+ const adoptExisting = Boolean(options.adoptExisting);
41
+ const existingOnly = Boolean(options.existingOnly);
29
42
 
30
43
  const projectConfig = await readProjectConfig(rootDir);
31
44
  const selection = resolveEnabledFormats(projectConfig, requestedFormat);
@@ -56,8 +69,27 @@ export async function syncAgentInstructionFiles(options = {}) {
56
69
  for (const format of selection.formats) {
57
70
  const outputPath = path.join(toolingRootDir, format.path);
58
71
  const existingContent = await readExistingFile(outputPath);
72
+
73
+ if (existingOnly && existingContent === null) {
74
+ results.push({
75
+ format: format.name,
76
+ path: outputPath,
77
+ relativePath: toPosix(path.relative(toolingRootDir, outputPath)),
78
+ status: 'skipped-missing',
79
+ warning: null,
80
+ changed: false,
81
+ conflicts: [],
82
+ });
83
+ continue;
84
+ }
85
+
59
86
  const generatedContent = format.render(context);
60
- const applied = applyManagedBlock(existingContent, generatedContent);
87
+ const applied = applyManagedBlock(existingContent, generatedContent, {
88
+ adoptExisting,
89
+ });
90
+ const conflicts = applied.status === 'adopt'
91
+ ? scanPotentialWorkflowConflicts(existingContent)
92
+ : [];
61
93
 
62
94
  if (!dryRun && applied.warning === null && applied.content !== existingContent) {
63
95
  await mkdir(path.dirname(outputPath), { recursive: true });
@@ -71,6 +103,7 @@ export async function syncAgentInstructionFiles(options = {}) {
71
103
  status: dryRun && applied.warning === null ? `dry-run-${applied.status}` : applied.status,
72
104
  warning: applied.warning,
73
105
  changed: applied.warning === null && applied.content !== existingContent,
106
+ conflicts,
74
107
  });
75
108
  }
76
109
 
@@ -86,6 +119,10 @@ export function getSupportedAgentFormats() {
86
119
  return FORMATS.map((format) => format.name);
87
120
  }
88
121
 
122
+ export function getSupportedAgentFilePaths() {
123
+ return FORMATS.map((format) => format.path);
124
+ }
125
+
89
126
  async function readProjectConfig(rootDir) {
90
127
  const source = await readFile(path.join(rootDir, 'project.yaml'), 'utf8');
91
128
  return parseSimpleYaml(source, { sourceName: 'project.yaml' });
@@ -145,3 +182,22 @@ function normalizeOptionalString(value) {
145
182
  function toPosix(value) {
146
183
  return value.split(path.sep).join('/');
147
184
  }
185
+
186
+ function scanPotentialWorkflowConflicts(content) {
187
+ if (typeof content !== 'string' || content.trim() === '') {
188
+ return [];
189
+ }
190
+
191
+ return content
192
+ .split(/\r?\n/)
193
+ .map((line, index) => ({
194
+ line: index + 1,
195
+ text: line,
196
+ normalized: line.toLowerCase(),
197
+ }))
198
+ .filter((entry) => CONFLICT_SCAN_PATTERNS.some((pattern) => entry.normalized.includes(pattern)))
199
+ .map(({ line, text }) => ({
200
+ line,
201
+ text: text.trim(),
202
+ }));
203
+ }
@@ -6,7 +6,7 @@ export function renderManagedBlock(content) {
6
6
  return `${START_MARKER}\n${trimmed}\n${END_MARKER}\n`;
7
7
  }
8
8
 
9
- export function applyManagedBlock(existingContent, generatedContent) {
9
+ export function applyManagedBlock(existingContent, generatedContent, options = {}) {
10
10
  const block = renderManagedBlock(generatedContent);
11
11
 
12
12
  if (existingContent === null) {
@@ -21,6 +21,15 @@ export function applyManagedBlock(existingContent, generatedContent) {
21
21
  const endIndex = existingContent.indexOf(END_MARKER);
22
22
 
23
23
  if (startIndex === -1 && endIndex === -1) {
24
+ if (options.adoptExisting) {
25
+ const separator = existingContent === '' || existingContent.endsWith('\n') ? '' : '\n';
26
+ return {
27
+ status: 'adopt',
28
+ content: `${existingContent}${separator}${block}`,
29
+ warning: null,
30
+ };
31
+ }
32
+
24
33
  return {
25
34
  status: 'warning',
26
35
  content: existingContent,
@@ -22,6 +22,10 @@ export function renderSharedInstructionBody(context, options = {}) {
22
22
  '',
23
23
  intro,
24
24
  '',
25
+ `Read \`${context.rootDir}/context.md\` for the full VibeCompass workflow protocol when it exists.`,
26
+ '',
27
+ 'Existing project-specific coding, style, framework, and safety instructions outside this managed block remain authoritative. This block owns only VibeCompass project-memory workflow: sessions, handoffs, decisions, docs-review, and close-out.',
28
+ '',
25
29
  '## Read First',
26
30
  `- Project memory root: \`${context.rootDir}\``,
27
31
  '- Read `project.yaml` for project identity, repos, mode, and workflow defaults.',
package/src/setup.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { execFile } from 'node:child_process';
2
- import { access, readFile } from 'node:fs/promises';
2
+ import { access, readFile, readdir, realpath } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { createInterface } from 'node:readline/promises';
5
5
  import { promisify } from 'node:util';
6
+ import { getSupportedAgentFilePaths } from './generators/agent-files/index.js';
7
+ import { END_MARKER, START_MARKER } from './generators/agent-files/markers.js';
6
8
 
7
9
  const execFileAsync = promisify(execFile);
8
10
 
@@ -68,6 +70,14 @@ export async function resolveInitCliOptions(options, environment = {}) {
68
70
  },
69
71
  placementPattern: resolved.placementPattern ?? null,
70
72
  guidedSummary,
73
+ agentFileSyncPlan: resolved.adoptExistingAgentFiles
74
+ ? {
75
+ rootDir: resolved.rootDir,
76
+ toolingRootDir: resolved.toolingRootDir,
77
+ adoptExisting: true,
78
+ existingOnly: true,
79
+ }
80
+ : null,
71
81
  sessionPlan: resolved.startSession
72
82
  ? {
73
83
  sessionId: resolved.sessionId,
@@ -220,14 +230,18 @@ async function completeGuidedInitOptions(options, environment) {
220
230
  resolved.mode = setupGoal === 'share-later' ? 'local-primary' : setupGoal;
221
231
  }
222
232
 
233
+ const detectedRepos = inferredProject.repos ?? [];
234
+
223
235
  if (!Array.isArray(resolved.repos) || resolved.repos.length === 0) {
224
- if (inferredProject.repo) {
225
- const useDetectedRepo = await askConfirm(
236
+ if (detectedRepos.length > 0) {
237
+ const useDetectedRepos = await askConfirm(
226
238
  prompter,
227
- `Use detected Git repo ${inferredProject.repo.remote}?`,
239
+ formatDetectedReposPrompt(detectedRepos),
228
240
  true,
229
241
  );
230
- resolved.repos = useDetectedRepo ? [inferredProject.repo] : await promptForRepos(prompter, cwd);
242
+ resolved.repos = useDetectedRepos
243
+ ? detectedRepos.map((repo) => toProjectRepo(repo))
244
+ : await promptForRepos(prompter, cwd);
231
245
  } else {
232
246
  resolved.repos = await promptForRepos(prompter, cwd);
233
247
  }
@@ -235,7 +249,15 @@ async function completeGuidedInitOptions(options, environment) {
235
249
 
236
250
  let recommendation = null;
237
251
  if (!resolved.placementPattern) {
238
- if (inferredProject.repo && resolved.repos.length === 1) {
252
+ if (inferredProject.source === 'child-repos' && detectedRepos.length > 1 && reposMatchDetected(resolved.repos, detectedRepos)) {
253
+ recommendation = recommendPlacementPattern({
254
+ repoCount: detectedRepos.length,
255
+ usesSharedWorkspace: true,
256
+ });
257
+ resolved.placementPattern = recommendation.pattern;
258
+ prompter.write(`Recommended placement: ${recommendation.pattern}\n`);
259
+ prompter.write(`${recommendation.reason}\n`);
260
+ } else if (detectedRepos.length === 1 && reposMatchDetected(resolved.repos, detectedRepos)) {
239
261
  recommendation = recommendPlacementPattern({ repoCount: 1 });
240
262
  resolved.placementPattern = recommendation.pattern;
241
263
  } else {
@@ -248,7 +270,9 @@ async function completeGuidedInitOptions(options, environment) {
248
270
  }
249
271
 
250
272
  if (!resolved.toolingRootDir) {
251
- if (inferredProject.repo && resolved.repos.length === 1 && resolved.placementPattern === PLACEMENT_PATTERNS.PRIMARY_REPO) {
273
+ if (resolved.placementPattern === PLACEMENT_PATTERNS.PRIMARY_REPO && detectedRepos.length === 1 && reposMatchDetected(resolved.repos, detectedRepos)) {
274
+ resolved.toolingRootDir = detectedRepos[0].directory;
275
+ } else if (resolved.placementPattern === PLACEMENT_PATTERNS.WORKSPACE_ROOT && inferredProject.source === 'child-repos' && detectedRepos.length > 1 && reposMatchDetected(resolved.repos, detectedRepos)) {
252
276
  resolved.toolingRootDir = '.';
253
277
  } else {
254
278
  const ownerLabel = describePlacementOwner(resolved.placementPattern);
@@ -322,6 +346,22 @@ async function completeGuidedInitOptions(options, environment) {
322
346
  }
323
347
  }
324
348
 
349
+ if (resolved.adoptExistingAgentFiles === undefined) {
350
+ const adoptionCandidates = await findExistingUnmarkedAgentFiles({
351
+ cwd,
352
+ toolingRootDir: resolved.toolingRootDir,
353
+ });
354
+
355
+ if (adoptionCandidates.length > 0) {
356
+ writeAgentAdoptionPreview(prompter, adoptionCandidates);
357
+ resolved.adoptExistingAgentFiles = await askConfirm(
358
+ prompter,
359
+ 'Append VibeCompass managed sections to existing agent instruction files?',
360
+ false,
361
+ );
362
+ }
363
+ }
364
+
325
365
  if (!resolved.startSession) {
326
366
  resolved.startSession = await askConfirm(
327
367
  prompter,
@@ -381,26 +421,43 @@ async function completeGuidedInitOptions(options, environment) {
381
421
  }
382
422
 
383
423
  async function inferProjectFromCwd(cwd) {
384
- const [packageName, gitInfo] = await Promise.all([
424
+ const [packageName, gitInfo, childRepos] = await Promise.all([
385
425
  readPackageName(cwd),
386
426
  readGitInfo(cwd),
427
+ discoverChildGitRepos(cwd),
387
428
  ]);
388
429
  const projectName = packageName ?? path.basename(cwd);
389
430
 
390
- if (!gitInfo?.remote) {
391
- return { projectName, repo: null };
431
+ if (gitInfo?.remote) {
432
+ const remoteSlug = extractRemoteSlug(gitInfo.remote);
433
+ const repoId = suggestRepoId(remoteSlug ?? packageName ?? path.basename(gitInfo.root ?? cwd));
434
+
435
+ return {
436
+ projectName,
437
+ source: 'current-repo',
438
+ repos: [
439
+ {
440
+ id: repoId,
441
+ remote: gitInfo.remote,
442
+ directory: '.',
443
+ ...(gitInfo.branch ? { defaultBranch: gitInfo.branch } : {}),
444
+ },
445
+ ],
446
+ };
392
447
  }
393
448
 
394
- const remoteSlug = extractRemoteSlug(gitInfo.remote);
395
- const repoId = suggestRepoId(remoteSlug ?? packageName ?? path.basename(gitInfo.root ?? cwd));
449
+ if (childRepos.length > 0) {
450
+ return {
451
+ projectName,
452
+ source: 'child-repos',
453
+ repos: childRepos,
454
+ };
455
+ }
396
456
 
397
457
  return {
398
458
  projectName,
399
- repo: {
400
- id: repoId,
401
- remote: gitInfo.remote,
402
- ...(gitInfo.branch ? { defaultBranch: gitInfo.branch } : {}),
403
- },
459
+ source: 'none',
460
+ repos: [],
404
461
  };
405
462
  }
406
463
 
@@ -437,6 +494,155 @@ async function readGitInfo(cwd) {
437
494
  }
438
495
  }
439
496
 
497
+ async function discoverChildGitRepos(cwd) {
498
+ let entries = [];
499
+ try {
500
+ entries = await readdir(cwd, { withFileTypes: true });
501
+ } catch {
502
+ return [];
503
+ }
504
+
505
+ const candidates = entries
506
+ .filter((entry) => entry.isDirectory() && isScannableWorkspaceChild(entry.name))
507
+ .sort((left, right) => left.name.localeCompare(right.name));
508
+
509
+ const discovered = await Promise.all(candidates.map(async (entry) => {
510
+ const directory = entry.name;
511
+ const absoluteDirectory = path.join(cwd, directory);
512
+ const [gitInfo, packageName] = await Promise.all([
513
+ readGitInfo(absoluteDirectory),
514
+ readPackageName(absoluteDirectory),
515
+ ]);
516
+
517
+ if (!gitInfo?.remote) {
518
+ return null;
519
+ }
520
+
521
+ if (gitInfo.root && !(await isSameDirectory(gitInfo.root, absoluteDirectory))) {
522
+ return null;
523
+ }
524
+
525
+ const remoteSlug = extractRemoteSlug(gitInfo.remote);
526
+ return {
527
+ id: suggestRepoId(remoteSlug ?? packageName ?? directory),
528
+ remote: gitInfo.remote,
529
+ directory,
530
+ ...(gitInfo.branch ? { defaultBranch: gitInfo.branch } : {}),
531
+ };
532
+ }));
533
+
534
+ const reposByDirectory = new Map();
535
+ for (const repo of discovered) {
536
+ if (repo && !reposByDirectory.has(repo.directory)) {
537
+ reposByDirectory.set(repo.directory, repo);
538
+ }
539
+ }
540
+
541
+ return [...reposByDirectory.values()].sort((left, right) => left.directory.localeCompare(right.directory));
542
+ }
543
+
544
+ function isScannableWorkspaceChild(name) {
545
+ if (!name || name.startsWith('.')) {
546
+ return false;
547
+ }
548
+
549
+ return !new Set([
550
+ 'build',
551
+ 'coverage',
552
+ 'dist',
553
+ 'node_modules',
554
+ 'out',
555
+ 'target',
556
+ 'vendor',
557
+ ]).has(name);
558
+ }
559
+
560
+ function formatDetectedReposPrompt(repos) {
561
+ if (repos.length === 1) {
562
+ const repo = repos[0];
563
+ const location = repo.directory && repo.directory !== '.' ? ` in ${repo.directory}` : '';
564
+ return `Use detected Git repo${location} (${repo.remote})?`;
565
+ }
566
+
567
+ return `Use ${repos.length} detected Git repos (${repos.map((repo) => repo.directory).join(', ')})?`;
568
+ }
569
+
570
+ function toProjectRepo(repo) {
571
+ return {
572
+ id: repo.id,
573
+ remote: repo.remote,
574
+ ...(repo.defaultBranch ? { defaultBranch: repo.defaultBranch } : {}),
575
+ };
576
+ }
577
+
578
+ function reposMatchDetected(repos, detectedRepos) {
579
+ if (!Array.isArray(repos) || repos.length !== detectedRepos.length) {
580
+ return false;
581
+ }
582
+
583
+ const detectedKeys = new Set(detectedRepos.map((repo) => `${repo.id}\n${repo.remote}`));
584
+ return repos.every((repo) => detectedKeys.has(`${repo.id}\n${repo.remote}`));
585
+ }
586
+
587
+ async function findExistingUnmarkedAgentFiles(context) {
588
+ const toolingRootDir = path.resolve(context.cwd, context.toolingRootDir ?? '.');
589
+ const candidates = [];
590
+
591
+ for (const relativePath of getSupportedAgentFilePaths()) {
592
+ const filePath = path.join(toolingRootDir, relativePath);
593
+ let content = null;
594
+ try {
595
+ content = await readFile(filePath, 'utf8');
596
+ } catch {
597
+ continue;
598
+ }
599
+
600
+ if (content.includes(START_MARKER) || content.includes(END_MARKER)) {
601
+ continue;
602
+ }
603
+
604
+ candidates.push({
605
+ relativePath,
606
+ conflicts: scanPotentialAgentWorkflowConflicts(content),
607
+ });
608
+ }
609
+
610
+ return candidates;
611
+ }
612
+
613
+ function writeAgentAdoptionPreview(prompter, candidates) {
614
+ prompter.write('Existing agent instruction files without VibeCompass markers:\n');
615
+ for (const candidate of candidates) {
616
+ prompter.write(`- ${candidate.relativePath}: VibeCompass can append a managed block at the end of this file.\n`);
617
+ if (candidate.conflicts.length > 0) {
618
+ prompter.write(` possible workflow overlap on lines ${candidate.conflicts.map((conflict) => conflict.line).join(', ')}\n`);
619
+ }
620
+ }
621
+ prompter.write('Content outside the managed block stays user-owned and will not be rewritten.\n');
622
+ }
623
+
624
+ function scanPotentialAgentWorkflowConflicts(content) {
625
+ const patterns = [
626
+ 'session',
627
+ 'handoff',
628
+ 'decisions',
629
+ 'wip.md',
630
+ 'review handoff',
631
+ 'close-session',
632
+ 'address review',
633
+ 'builder',
634
+ 'reviewer',
635
+ ];
636
+
637
+ return content
638
+ .split(/\r?\n/)
639
+ .map((line, index) => ({
640
+ line: index + 1,
641
+ normalized: line.toLowerCase(),
642
+ }))
643
+ .filter((entry) => patterns.some((pattern) => entry.normalized.includes(pattern)));
644
+ }
645
+
440
646
  async function promptForRepos(prompter, cwd) {
441
647
  const repoCount = await askInteger(prompter, 'How many repos belong to this logical project?', {
442
648
  defaultValue: 1,
@@ -872,6 +1078,18 @@ async function directoryExists(directoryPath) {
872
1078
  }
873
1079
  }
874
1080
 
1081
+ async function isSameDirectory(left, right) {
1082
+ try {
1083
+ const [leftRealPath, rightRealPath] = await Promise.all([
1084
+ realpath(left),
1085
+ realpath(right),
1086
+ ]);
1087
+ return leftRealPath === rightRealPath;
1088
+ } catch {
1089
+ return path.resolve(left) === path.resolve(right);
1090
+ }
1091
+ }
1092
+
875
1093
  function toRelativeIfInsideCwd(targetPath, cwd) {
876
1094
  const absolute = path.resolve(cwd, targetPath);
877
1095
  const relative = path.relative(cwd, absolute);