@vibecompass/vibecompass 0.3.3 → 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
@@ -440,12 +456,13 @@ Options:
440
456
  - `--root <path>`: project-memory root; defaults to `.compass`
441
457
  - `--tooling-root <path>`: directory where agent files are written; defaults to cwd
442
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
443
460
  - `--dry-run`: show planned writes without changing files
444
461
 
445
462
  This command generates enabled agent-instruction files from canonical project
446
463
  memory. It creates missing files with managed markers, replaces existing
447
464
  managed regions, preserves content outside markers, and warns instead of
448
- overwriting existing unmarked files.
465
+ overwriting existing unmarked files unless `--adopt-existing` is supplied.
449
466
 
450
467
  ### `vibecompass docs-review`
451
468
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecompass/vibecompass",
3
- "version": "0.3.3",
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
@@ -3,6 +3,8 @@ 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,
@@ -336,6 +346,22 @@ async function completeGuidedInitOptions(options, environment) {
336
346
  }
337
347
  }
338
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
+
339
365
  if (!resolved.startSession) {
340
366
  resolved.startSession = await askConfirm(
341
367
  prompter,
@@ -558,6 +584,65 @@ function reposMatchDetected(repos, detectedRepos) {
558
584
  return repos.every((repo) => detectedKeys.has(`${repo.id}\n${repo.remote}`));
559
585
  }
560
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
+
561
646
  async function promptForRepos(prompter, cwd) {
562
647
  const repoCount = await askInteger(prompter, 'How many repos belong to this logical project?', {
563
648
  defaultValue: 1,