@vibecompass/vibecompass 0.2.1 → 0.3.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.
@@ -35,14 +35,14 @@ export function renderSharedInstructionBody(context, options = {}) {
35
35
  '',
36
36
  '## Workflow Rules',
37
37
  '- VibeCompass active builder sessions are named lanes. Use one lane per active feature or workstream; finalized session notes are append-only history under `sessions/YYYY-MM-DD-N-title.md`.',
38
- '- Use `start session` for builder work and `join as reviewer` for review work.',
38
+ '- Use `start session` for builder work, `join as reviewer` for review work, and `close session` / `end session` when the builder is ready to close the selected lane.',
39
39
  '- Use `vibecompass start-session --id <lane-id>` to open a named lane for every active feature or workstream.',
40
40
  '- Use `vibecompass list-sessions` and `vibecompass switch-session <lane-id>` to inspect or change the current lane.',
41
41
  '- Treat `sessions/active/index.yaml` as the current lane source of truth; tool-specific Current session blocks are continuity summaries.',
42
42
  '- Use optional planning mode for risky or ambiguous work: read the same context, propose scope, and record the agreed plan in the selected lane `wip.md` before implementation.',
43
43
  '- Use `review handoff` to ask a reviewer to inspect the active handoff.',
44
44
  '- Use `address review` to process reviewer feedback from the selected lane `wip.md` and `handoff.md`.',
45
- '- End the active builder session with `vibecompass close-session --session <lane-id>`; `vibecompass end-session` is an accepted alias.',
45
+ '- End the active builder session with the `close session` prompt, which runs close-out and ends with `vibecompass close-session --session <lane-id>`; `end session` and `vibecompass end-session` are accepted aliases.',
46
46
  '- If stale scratch files block `start-session`, read them first, then close, recover, move, or delete them intentionally before retrying.',
47
47
  '- Keep decisions append-only in `decisions/`. Session notes may reference decisions, but decisions are the durable source of truth.',
48
48
  '- Treat generated agent files as views. Update canonical VibeCompass project memory instead of editing managed regions.',
package/src/index.js CHANGED
@@ -1,5 +1,12 @@
1
1
  export { STATE_VERSION, generateStateManifest, writeStateManifest } from './manifest.js';
2
2
  export { initializeProjectMemory } from './init.js';
3
+ export { preflightDocsReview } from './docs-review.js';
4
+ export {
5
+ applyPullExport,
6
+ pullExportProjectMemory,
7
+ pullPreviewProjectMemory,
8
+ pushProjectMemory,
9
+ } from './sync.js';
3
10
  export { scanProjectMemory } from './project-memory.js';
4
11
  export {
5
12
  closeProjectSession,
package/src/init.js CHANGED
@@ -5,6 +5,7 @@ import { writeStateManifest } from './manifest.js';
5
5
  import { scaffoldInitFiles } from './scaffold.js';
6
6
  import { applyPlacementDefaults } from './setup.js';
7
7
  import { buildWorkflowMetadata } from './workflow.js';
8
+ import { parseSimpleYaml } from './simple-yaml.js';
8
9
 
9
10
  const VALID_MODES = new Set(['local-only', 'local-primary', 'hosted-only']);
10
11
 
@@ -21,10 +22,10 @@ export async function initializeProjectMemory(options) {
21
22
 
22
23
  await writeProjectFile(projectFilePath, normalized.projectConfig, normalized.force);
23
24
  const gitignoreUpdated = await ensureStateIgnored(gitignorePath);
25
+ const scaffoldResult = await scaffoldInitFiles(normalized.bootstrap);
24
26
  const manifestResult = await writeStateManifest(normalized.rootDir, {
25
27
  generatedAt: normalized.generatedAt,
26
28
  });
27
- const scaffoldResult = await scaffoldInitFiles(normalized.bootstrap);
28
29
 
29
30
  return {
30
31
  rootDir: normalized.rootDir,
@@ -41,6 +42,39 @@ export async function initializeProjectMemory(options) {
41
42
  };
42
43
  }
43
44
 
45
+ export async function connectHostedProjectMemory(options) {
46
+ const cwd = options?.cwd ? path.resolve(options.cwd) : process.cwd();
47
+ const rootDir = path.resolve(cwd, options?.rootDir ?? '.compass');
48
+ const projectFilePath = path.join(rootDir, 'project.yaml');
49
+ let projectConfig;
50
+ try {
51
+ projectConfig = parseSimpleYaml(await readFile(projectFilePath, 'utf8'), {
52
+ sourceName: projectFilePath,
53
+ });
54
+ } catch (error) {
55
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
56
+ throw new Error(`No project.yaml found in ${rootDir}. Run "vibecompass init" first.`);
57
+ }
58
+
59
+ throw error;
60
+ }
61
+
62
+ if (!['local-primary', 'hosted-only'].includes(projectConfig.mode)) {
63
+ throw new Error('connect-hosted requires project.yaml mode to be local-primary or hosted-only.');
64
+ }
65
+
66
+ const sync = normalizeSyncOptions(options?.sync, projectConfig.mode);
67
+ projectConfig.sync = sync;
68
+ await writeFile(projectFilePath, serializeProjectConfig(projectConfig), 'utf8');
69
+
70
+ return {
71
+ rootDir,
72
+ projectFilePath,
73
+ mode: projectConfig.mode,
74
+ syncEnvVar: sync.credential_env_var,
75
+ };
76
+ }
77
+
44
78
  function normalizeInitOptions(options) {
45
79
  if (!options || typeof options !== 'object') {
46
80
  throw new Error('initializeProjectMemory requires an options object.');
@@ -91,30 +125,9 @@ function normalizeInitOptions(options) {
91
125
  };
92
126
  });
93
127
 
94
- const syncOptions = preparedOptions.sync ?? null;
95
- let sync = undefined;
96
- if (syncOptions) {
97
- if (preparedOptions.mode !== 'local-primary') {
98
- throw new Error('sync configuration is only supported when mode is local-primary.');
99
- }
100
-
101
- const requiredFields = ['apiUrl', 'projectId', 'credentialEnvVar'];
102
- const missing = requiredFields.filter((field) => typeof syncOptions[field] !== 'string' || syncOptions[field].trim() === '');
103
-
104
- if (missing.length > 0) {
105
- throw new Error(
106
- `sync configuration is incomplete. Missing: ${missing.join(', ')}.`,
107
- );
108
- }
109
-
110
- sync = {
111
- provider: 'vibecompass',
112
- api_url: syncOptions.apiUrl,
113
- project_id: syncOptions.projectId,
114
- credential_source: 'env',
115
- credential_env_var: syncOptions.credentialEnvVar,
116
- };
117
- }
128
+ const sync = preparedOptions.sync
129
+ ? normalizeSyncOptions(preparedOptions.sync, preparedOptions.mode)
130
+ : undefined;
118
131
 
119
132
  const projectConfig = {
120
133
  format_version: 1,
@@ -137,6 +150,7 @@ function normalizeInitOptions(options) {
137
150
  rootDir,
138
151
  toolingRootDir,
139
152
  projectConfig,
153
+ generatedAt: preparedOptions.generatedAt,
140
154
  });
141
155
 
142
156
  return {
@@ -148,6 +162,33 @@ function normalizeInitOptions(options) {
148
162
  };
149
163
  }
150
164
 
165
+ function normalizeSyncOptions(syncOptions, mode) {
166
+ if (!syncOptions || typeof syncOptions !== 'object') {
167
+ throw new Error('sync configuration must be an object.');
168
+ }
169
+
170
+ if (!['local-primary', 'hosted-only'].includes(mode)) {
171
+ throw new Error('sync configuration is only supported when mode is local-primary or hosted-only.');
172
+ }
173
+
174
+ const requiredFields = ['apiUrl', 'projectId', 'credentialEnvVar'];
175
+ const missing = requiredFields.filter((field) => typeof syncOptions[field] !== 'string' || syncOptions[field].trim() === '');
176
+
177
+ if (missing.length > 0) {
178
+ throw new Error(
179
+ `sync configuration is incomplete. Missing: ${missing.join(', ')}.`,
180
+ );
181
+ }
182
+
183
+ return {
184
+ provider: 'vibecompass',
185
+ api_url: syncOptions.apiUrl,
186
+ project_id: syncOptions.projectId,
187
+ credential_source: 'env',
188
+ credential_env_var: syncOptions.credentialEnvVar,
189
+ };
190
+ }
191
+
151
192
  function buildAgentFilesMetadata(metadata) {
152
193
  const existingAgentFiles = metadata?.agent_files && typeof metadata.agent_files === 'object'
153
194
  ? metadata.agent_files
@@ -182,6 +223,7 @@ function normalizeBootstrapOptions(bootstrap, context) {
182
223
  contextRelativeToToolingRoot: toPosix(
183
224
  path.relative(context.toolingRootDir, contextFilePath) || 'context.md',
184
225
  ),
226
+ generatedAt: context.generatedAt,
185
227
  };
186
228
  }
187
229
 
package/src/manifest.js CHANGED
@@ -1,7 +1,8 @@
1
- import { mkdir, writeFile } from 'node:fs/promises';
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { localRevisionFromManifestHash, stableHash } from './hash.js';
4
4
  import { scanProjectMemory } from './project-memory.js';
5
+ import { listProjectSessions } from './session.js';
5
6
 
6
7
  export const STATE_VERSION = 1;
7
8
 
@@ -50,13 +51,22 @@ export function generateStateManifest(scanResult, options = {}) {
50
51
  warning_count: warningCount,
51
52
  },
52
53
  documents: manifestDocuments,
54
+ ...(options.activeSessions ? { active_sessions: options.activeSessions } : {}),
53
55
  ...(options.sync ? { sync: options.sync } : {}),
54
56
  };
55
57
  }
56
58
 
57
59
  export async function writeStateManifest(rootDir, options = {}) {
58
60
  const scanResult = await scanProjectMemory(rootDir);
59
- const manifest = generateStateManifest(scanResult, options);
61
+ const activeSessions = await readActiveSessionsForManifest(rootDir);
62
+ const existingSync = Object.hasOwn(options, 'sync')
63
+ ? options.sync
64
+ : await readExistingSyncState(rootDir);
65
+ const manifest = generateStateManifest(scanResult, {
66
+ ...options,
67
+ ...(existingSync ? { sync: existingSync } : {}),
68
+ ...(activeSessions ? { activeSessions } : {}),
69
+ });
60
70
  const stateDir = path.join(rootDir, 'state');
61
71
  const manifestPath = path.join(stateDir, 'manifest.json');
62
72
 
@@ -70,6 +80,46 @@ export async function writeStateManifest(rootDir, options = {}) {
70
80
  };
71
81
  }
72
82
 
83
+ async function readExistingSyncState(rootDir) {
84
+ try {
85
+ const existing = JSON.parse(
86
+ await readFile(path.join(rootDir, 'state', 'manifest.json'), 'utf8'),
87
+ );
88
+ return existing?.sync && typeof existing.sync === 'object'
89
+ ? existing.sync
90
+ : null;
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ async function readActiveSessionsForManifest(rootDir) {
97
+ const sessions = await listProjectSessions({ rootDir });
98
+ if (sessions.lanes.length === 0 && !sessions.current) {
99
+ return null;
100
+ }
101
+
102
+ return {
103
+ current: sessions.current,
104
+ lanes: sessions.lanes.map((lane) => ({
105
+ id: lane.id,
106
+ status: lane.status ?? 'active',
107
+ working_on: lane.workingOn,
108
+ feature_slugs: lane.features ?? [],
109
+ repos: lane.repos ?? [],
110
+ claimed_paths: lane.claims ?? [],
111
+ started_at: lane.startedAt ?? null,
112
+ ...(lane.decisionSnapshot
113
+ ? {
114
+ decision_snapshot: {
115
+ highest_decision_id: lane.decisionSnapshot.highestDecisionId ?? null,
116
+ },
117
+ }
118
+ : {}),
119
+ })),
120
+ };
121
+ }
122
+
73
123
  function toIsoString(value) {
74
124
  return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
75
125
  }
@@ -5,7 +5,7 @@ import { sha256Text } from './hash.js';
5
5
  import { parseSimpleYaml } from './simple-yaml.js';
6
6
 
7
7
  const PROJECT_FILE = 'project.yaml';
8
- const NON_CANONICAL_DECISION_FILES = new Set(['INDEX.md', 'README.md']);
8
+ const NON_CANONICAL_DECISION_FILES = new Set(['EXAMPLE.md', 'INDEX.md', 'README.md']);
9
9
  const NON_CANONICAL_SESSION_FILES = new Set(['wip.md', 'handoff.md', 'README.md']);
10
10
  const NON_CANONICAL_ARCHITECTURE_FILES = new Set(['README.md']);
11
11
  const VALID_MODES = new Set(['local-only', 'local-primary', 'hosted-only']);
@@ -468,8 +468,23 @@ function parseDecisionDocument(options) {
468
468
  const warnings = [];
469
469
  const errors = [];
470
470
  const entries = extractDecisionEntries(options.content);
471
+ const malformedHeadings = extractMalformedDecisionHeadings(options.content);
472
+
473
+ for (const heading of malformedHeadings) {
474
+ errors.push(
475
+ createError(
476
+ options.relativePath,
477
+ 'decision-invalid-id',
478
+ `Decision heading "${heading}" must use zero-padded D-NNN or higher.`,
479
+ ),
480
+ );
481
+ }
471
482
 
472
483
  if (entries.length === 0) {
484
+ if (malformedHeadings.length > 0) {
485
+ return { kind: 'decision', extracted: null, warnings, errors };
486
+ }
487
+
473
488
  errors.push(
474
489
  createError(options.relativePath, 'decision-missing-entries', 'Decision files must contain at least one resolvable decision entry.'),
475
490
  );
@@ -479,7 +494,7 @@ function parseDecisionDocument(options) {
479
494
  const decisionIds = [];
480
495
  for (const entry of entries) {
481
496
  if (!entry.decisionId || entry.decisionId <= 0) {
482
- errors.push(createError(options.relativePath, 'decision-invalid-id', 'Decision heading must include D-<positive integer>.'));
497
+ errors.push(createError(options.relativePath, 'decision-invalid-id', 'Decision heading must include zero-padded D-<NNN>.'));
483
498
  continue;
484
499
  }
485
500
 
@@ -576,7 +591,7 @@ function parseSessionDocument(options) {
576
591
  }
577
592
 
578
593
  function extractDecisionEntries(content) {
579
- const matches = [...content.matchAll(/^###\s+D-(\d+)\b.*$/gm)];
594
+ const matches = [...content.matchAll(/^###\s+D-(\d{3,})\b.*$/gm)];
580
595
 
581
596
  return matches.map((match, index) => {
582
597
  const bodyStart = match.index ?? 0;
@@ -590,6 +605,10 @@ function extractDecisionEntries(content) {
590
605
  });
591
606
  }
592
607
 
608
+ function extractMalformedDecisionHeadings(content) {
609
+ return [...content.matchAll(/^###\s+(D-\d{1,2})\b.*$/gm)].map((match) => match[1]);
610
+ }
611
+
593
612
  function findDuplicateDecisionErrors(documents) {
594
613
  const seen = new Map();
595
614
  const errors = [];
package/src/read-model.js CHANGED
@@ -5,7 +5,7 @@ import { generateStateManifest } from './manifest.js';
5
5
  import { scanProjectMemory } from './project-memory.js';
6
6
 
7
7
  const SECTION_PATTERN = /^##\s+(.+)$/gm;
8
- const DECISION_HEADING_PATTERN = /^###\s+D-(\d+)\s+—\s+(.+)$/gm;
8
+ const DECISION_HEADING_PATTERN = /^###\s+D-(\d{3,})\s+—\s+(.+)$/gm;
9
9
 
10
10
  export async function loadProjectReadModel(rootDir) {
11
11
  const scanResult = await scanProjectMemory(rootDir);
package/src/scaffold.js CHANGED
@@ -1,4 +1,4 @@
1
- import { access, mkdir, writeFile } from 'node:fs/promises';
1
+ import { access, mkdir, readdir, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { resolveWorkflowSettings } from './workflow.js';
4
4
 
@@ -6,6 +6,45 @@ export async function scaffoldInitFiles(options) {
6
6
  const createdFiles = [];
7
7
  const skippedFiles = [];
8
8
 
9
+ await mkdir(path.join(options.rootDir, 'architecture'), { recursive: true });
10
+ await mkdir(path.join(options.rootDir, 'decisions'), { recursive: true });
11
+ await mkdir(path.join(options.rootDir, 'sessions'), { recursive: true });
12
+
13
+ const starterFiles = [
14
+ {
15
+ path: path.join(options.rootDir, 'decisions', 'EXAMPLE.md'),
16
+ content: generateDecisionExample(options.projectConfig),
17
+ },
18
+ ];
19
+
20
+ if (!(await hasCanonicalMarkdown(path.join(options.rootDir, 'architecture'), {
21
+ ignoredNames: new Set(['README.md']),
22
+ }))) {
23
+ starterFiles.push({
24
+ path: path.join(options.rootDir, 'architecture', 'overview', 'project-shape.md'),
25
+ content: generateStarterArchitectureDoc(options.projectConfig),
26
+ });
27
+ }
28
+
29
+ if (!(await hasCanonicalMarkdown(path.join(options.rootDir, 'sessions'), {
30
+ ignoredNames: new Set(['README.md', 'wip.md', 'handoff.md']),
31
+ ignoredDirectories: new Set(['active']),
32
+ }))) {
33
+ starterFiles.push({
34
+ path: path.join(options.rootDir, 'sessions', `${sessionDateFromGeneratedAt(options.generatedAt)}-1-project-memory-initialized.md`),
35
+ content: generateStarterSessionNote(options),
36
+ });
37
+ }
38
+
39
+ for (const file of starterFiles) {
40
+ const outcome = await writeIfMissing(file.path, file.content);
41
+ if (outcome.created) {
42
+ createdFiles.push(file.path);
43
+ } else {
44
+ skippedFiles.push(file.path);
45
+ }
46
+ }
47
+
9
48
  if (!options.workflow) {
10
49
  return {
11
50
  contextFilePath: null,
@@ -14,10 +53,6 @@ export async function scaffoldInitFiles(options) {
14
53
  };
15
54
  }
16
55
 
17
- await mkdir(path.join(options.rootDir, 'architecture'), { recursive: true });
18
- await mkdir(path.join(options.rootDir, 'decisions'), { recursive: true });
19
- await mkdir(path.join(options.rootDir, 'sessions'), { recursive: true });
20
-
21
56
  const contextFilePath = path.join(options.rootDir, 'context.md');
22
57
  // context.md is a derived artifact owned by the package; regenerate it on rerun.
23
58
  await writeFile(contextFilePath, generateContextMarkdown(options), 'utf8');
@@ -97,6 +132,43 @@ async function fileExists(filePath) {
97
132
  }
98
133
  }
99
134
 
135
+ async function hasCanonicalMarkdown(directoryPath, options = {}) {
136
+ const ignoredNames = options.ignoredNames ?? new Set();
137
+ const ignoredDirectories = options.ignoredDirectories ?? new Set();
138
+
139
+ try {
140
+ const entries = await readdir(directoryPath, { withFileTypes: true });
141
+ for (const entry of entries) {
142
+ if (entry.name.startsWith('.')) {
143
+ continue;
144
+ }
145
+
146
+ if (entry.isDirectory()) {
147
+ if (ignoredDirectories.has(entry.name)) {
148
+ continue;
149
+ }
150
+
151
+ if (await hasCanonicalMarkdown(path.join(directoryPath, entry.name), options)) {
152
+ return true;
153
+ }
154
+ continue;
155
+ }
156
+
157
+ if (entry.isFile() && entry.name.endsWith('.md') && !ignoredNames.has(entry.name)) {
158
+ return true;
159
+ }
160
+ }
161
+ } catch (error) {
162
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
163
+ return false;
164
+ }
165
+
166
+ throw error;
167
+ }
168
+
169
+ return false;
170
+ }
171
+
100
172
  function generateContextMarkdown(options) {
101
173
  const projectName = options.projectConfig.name;
102
174
  const rootRelativePath = options.rootRelativePath;
@@ -135,7 +207,7 @@ This workspace uses VibeCompass project memory rooted at \`${rootRelativePath}\`
135
207
  ${repos}
136
208
 
137
209
  ## Session roles
138
- | Trigger | Role | Owns session lifecycle? |
210
+ | Entry trigger | Role | Owns session lifecycle? |
139
211
  |---|---|---|
140
212
  | "start session" | Builder | Yes — opens the session, keeps scratch files current, and writes the final session note |
141
213
  | "join as reviewer" | Reviewer | No — reviews the builder's work, appends findings, and updates handoff guidance |
@@ -146,14 +218,20 @@ ${repos}
146
218
  - \`planning mode\` — optional prompt-level mode for scoping work before implementation
147
219
  - \`review handoff\` — reviewer reads the selected lane's \`wip.md\`, \`handoff.md\`, latest finalized note, and relevant diffs before appending findings
148
220
  - \`address review\` — builder reads reviewer feedback in the selected lane's \`wip.md\` / \`handoff.md\`, responds inline, applies accepted changes, and refreshes the builder handoff
221
+ - \`close session\` — builder runs the close-out checklist and ends with \`vibecompass close-session --session <lane-id>\`; \`end session\` is accepted as a synonym
149
222
 
150
223
  These are prompt commands for agent behavior, not \`vibecompass\` CLI subcommands.
151
- Reviewer handback is explicit: the reviewer ends the pass by updating the selected lane's \`wip.md\` + \`handoff.md\` and then stopping. Builder close-out uses \`vibecompass close-session --session <lane-id>\`, which follows the workflow defaults recorded in \`${rootRelativePath}/project.yaml\`.
224
+ Reviewer handback is explicit: the reviewer ends the pass by updating the selected lane's \`wip.md\` + \`handoff.md\` and then stopping. The \`close session\` prompt runs builder close-out and ends with \`vibecompass close-session --session <lane-id>\`, which follows the workflow defaults recorded in \`${rootRelativePath}/project.yaml\`.
152
225
  \`vibecompass end-session\` is also accepted as an alias for \`vibecompass close-session\`; the canonical command name remains \`close-session\`.
153
226
 
154
227
  ## Workflow defaults
155
228
  ${renderWorkflowDefaults(workflow)}
156
229
 
230
+ ## Documentation coverage
231
+ - The architecture, decision, and session files created by init are an initial scaffold, not a comprehensive codebase review.
232
+ - Before risky implementation work, inspect the relevant code and update the affected architecture docs.
233
+ - Run \`vibecompass docs-review --guided\` when you want a comprehensive documentation review. That command is explicit because it may inspect source files and use AI or hosted scan infrastructure.
234
+
157
235
  ## Session startup
158
236
  1. Read \`${rootRelativePath}/project.yaml\`.
159
237
  2. Read the latest finalized session note in \`${rootRelativePath}/sessions/\`.
@@ -240,6 +318,119 @@ automatically by package commands.
240
318
  `;
241
319
  }
242
320
 
321
+ function generateStarterArchitectureDoc(projectConfig) {
322
+ const repos = projectConfig.repos.map((repo) => repo.id);
323
+ const repoLines = projectConfig.repos
324
+ .map((repo) => `- \`${repo.id}\` — ${repo.remote}${repo.default_branch ? ` (${repo.default_branch})` : ''}`)
325
+ .join('\n');
326
+ const repoEvidence = projectConfig.repos
327
+ .map((repo) => `- \`${repo.id}:/\` — declared repository root`)
328
+ .join('\n');
329
+
330
+ return `---
331
+ domain: Project
332
+ feature: Overview
333
+ component: Project Shape
334
+ status: In progress
335
+ repos:
336
+ ${repos.map((repoId) => ` - ${quoteYamlString(repoId)}`).join('\n')}
337
+ confidence: Initial scaffold
338
+ coverage: Initial scaffold
339
+ ---
340
+
341
+ ## Description
342
+ Initial project-memory scaffold for ${projectConfig.name}.
343
+
344
+ ${projectConfig.description ? `${projectConfig.description}\n` : ''}This document is a starting map, not a comprehensive architecture review.
345
+
346
+ ## Details
347
+ - Mode: \`${projectConfig.mode}\`
348
+ - Documentation coverage: initial scaffold
349
+ - Repo inventory:
350
+ ${repoLines}
351
+
352
+ ## Coverage
353
+ Confirmed:
354
+ - Project identity and mode from \`project.yaml\`
355
+ - Declared repository IDs and remotes from \`project.yaml\`
356
+ - Project-memory root layout created by \`vibecompass init\`
357
+
358
+ Not yet documented:
359
+ - Runtime architecture and data flow
360
+ - Auth/session model
361
+ - Database or persistence ownership
362
+ - External integrations
363
+ - Deployment, jobs, and observability
364
+ - Test strategy
365
+ - Feature/domain/component map beyond this placeholder
366
+
367
+ Before changing any undocumented area, inspect the relevant code and update or add architecture docs.
368
+
369
+ ## Next steps
370
+ - Run \`vibecompass docs-review --guided\` for a comprehensive documentation review.
371
+ - Replace this placeholder with domain/feature/component docs as the project map becomes evidence-backed.
372
+ - Keep architecture docs aligned with implementation changes during each builder session.
373
+
374
+ ## Involved files
375
+ - \`project.yaml\`
376
+ ${repoEvidence}
377
+ `;
378
+ }
379
+
380
+ function generateDecisionExample(projectConfig) {
381
+ return `# Decision Examples
382
+
383
+ This file is example-only guidance for ${projectConfig.name}. It is intentionally ignored by VibeCompass canonical decision parsing and must not be treated as a real decision log.
384
+
385
+ Real decisions belong in domain-grouped files such as \`cross-cutting.md\`, with append-only entries:
386
+
387
+ \`\`\`md
388
+ ### D-001 — Example decision title
389
+ **Timestamp:** YYYY-MM-DD HH:MM TZ
390
+ **Decision:** What was decided.
391
+ **Rationale:** Why this was the correct tradeoff.
392
+ \`\`\`
393
+
394
+ Do not copy this example as-is. Allocate real decision IDs from the current canonical decision log when a decision is accepted.
395
+ `;
396
+ }
397
+
398
+ function generateStarterSessionNote(options) {
399
+ const sessionDate = sessionDateFromGeneratedAt(options.generatedAt);
400
+ const projectConfig = options.projectConfig;
401
+ const repoLines = projectConfig.repos.map((repo) => `- \`${repo.id}\` — ${repo.remote}`).join('\n');
402
+
403
+ return `# Session — ${sessionDate}-1 — Project Memory Initialized
404
+
405
+ Generated by \`vibecompass init\`; this is initialization history, not a builder/reviewer work session.
406
+
407
+ ## What we worked on
408
+ Initialized VibeCompass project memory for ${projectConfig.name}.
409
+
410
+ ## Completed
411
+ - Created the project-memory root and \`project.yaml\`.
412
+ - Created lightweight starter architecture memory.
413
+ - Created example-only decision guidance without seeding real decisions.
414
+ - Generated local state manifest data from canonical files.
415
+
416
+ ## Decisions made
417
+ - No real project decisions were seeded by init. Use \`decisions/EXAMPLE.md\` only as format guidance.
418
+
419
+ ## Models used
420
+ - VibeCompass init scaffold.
421
+
422
+ ## Blockers / open questions
423
+ - Comprehensive architecture documentation has not been reviewed yet.
424
+ - Declared repos:
425
+ ${repoLines}
426
+
427
+ ## Next session should start with
428
+ 1. Run \`vibecompass docs-review --guided\` if you want a comprehensive documentation review.
429
+ 2. Inspect relevant code before making implementation changes in areas not covered by architecture docs.
430
+ 3. Replace starter placeholders with evidence-backed domain/feature/component docs.
431
+ `;
432
+ }
433
+
243
434
  function generateArchitectureGuide(projectConfig) {
244
435
  return `# Architecture
245
436
 
@@ -256,7 +447,7 @@ architecture/<domain-slug>/<feature-slug>/<component>.md
256
447
  - \`feature\`
257
448
  - \`component\`
258
449
  - \`status\`
259
- - optional \`repo\` or \`repos\`
450
+ - \`repos\`: array of repo IDs, even for single-repo projects
260
451
 
261
452
  ## Recommended sections
262
453
  - \`## Description\`
@@ -328,7 +519,7 @@ Read \`${options.contextRelativeToToolingRoot}\` before doing substantive work.
328
519
 
329
520
  ## Session roles
330
521
 
331
- | Trigger | Role | Owns session lifecycle? |
522
+ | Entry trigger | Role | Owns session lifecycle? |
332
523
  |---|---|---|
333
524
  | "start session" | Builder | Yes |
334
525
  | "join as reviewer" | Reviewer | No |
@@ -341,9 +532,10 @@ Read \`${options.contextRelativeToToolingRoot}\` before doing substantive work.
341
532
  - \`planning mode\` — optional prompt-level mode for scoping risky or ambiguous work before implementation
342
533
  - \`review handoff\` — reviewer reads the selected lane's \`wip.md\`, \`handoff.md\`, latest finalized note, and relevant diffs before appending findings
343
534
  - \`address review\` — builder reads reviewer feedback in the selected lane's \`wip.md\` / \`handoff.md\`, responds inline, applies accepted changes, and refreshes the builder handoff
535
+ - \`close session\` — builder runs the close-out checklist and ends with \`vibecompass close-session --session <lane-id>\`; \`end session\` is accepted as a synonym
344
536
 
345
537
  These are prompt commands for agent behavior, not \`vibecompass\` CLI subcommands.
346
- Reviewer handback is explicit: the reviewer ends the pass by updating the selected lane's \`wip.md\` + \`handoff.md\` and then stopping. Builder close-out uses \`vibecompass close-session --session <lane-id>\`, which follows the workflow defaults recorded in \`${options.rootRelativePath}/project.yaml\`.
538
+ Reviewer handback is explicit: the reviewer ends the pass by updating the selected lane's \`wip.md\` + \`handoff.md\` and then stopping. The \`close session\` prompt runs builder close-out and ends with \`vibecompass close-session --session <lane-id>\`, which follows the workflow defaults recorded in \`${options.rootRelativePath}/project.yaml\`.
347
539
  \`vibecompass end-session\` is accepted as an alias for \`vibecompass close-session\`; \`close-session\` remains the canonical command name.
348
540
 
349
541
  ## Current session
@@ -380,7 +572,7 @@ This workspace uses VibeCompass project memory rooted at \`${options.rootRelativ
380
572
 
381
573
  ## Session roles
382
574
 
383
- | Trigger | Role | Owns session lifecycle? |
575
+ | Entry trigger | Role | Owns session lifecycle? |
384
576
  |---|---|---|
385
577
  | "start session" | Builder | Yes — opens and closes the session lifecycle |
386
578
  | "join as reviewer" | Reviewer | No — reviews the builder's work and updates the handoff |
@@ -393,9 +585,10 @@ This workspace uses VibeCompass project memory rooted at \`${options.rootRelativ
393
585
  - \`planning mode\` — optional prompt-level mode for scoping risky or ambiguous work before implementation
394
586
  - \`review handoff\` — reviewer reads the selected lane's \`wip.md\`, \`handoff.md\`, latest finalized note, and relevant diffs before appending findings
395
587
  - \`address review\` — builder reads reviewer feedback in the selected lane's \`wip.md\` / \`handoff.md\`, responds inline, applies accepted changes, and refreshes the builder handoff
588
+ - \`close session\` — builder runs the close-out checklist and ends with \`vibecompass close-session --session <lane-id>\`; \`end session\` is accepted as a synonym
396
589
 
397
590
  These are prompt commands for agent behavior, not \`vibecompass\` CLI subcommands.
398
- Reviewer handback is explicit: the reviewer ends the pass by updating the selected lane's \`wip.md\` + \`handoff.md\` and then stopping. Builder close-out uses \`vibecompass close-session --session <lane-id>\`, which follows the workflow defaults recorded in \`${options.rootRelativePath}/project.yaml\`.
591
+ Reviewer handback is explicit: the reviewer ends the pass by updating the selected lane's \`wip.md\` + \`handoff.md\` and then stopping. The \`close session\` prompt runs builder close-out and ends with \`vibecompass close-session --session <lane-id>\`, which follows the workflow defaults recorded in \`${options.rootRelativePath}/project.yaml\`.
399
592
  \`vibecompass end-session\` is accepted as an alias for \`vibecompass close-session\`; \`close-session\` remains the canonical command name.
400
593
 
401
594
  ## Startup
@@ -442,3 +635,16 @@ function describeReviewerHandback(workflow) {
442
635
 
443
636
  return workflow.reviewerHandback;
444
637
  }
638
+
639
+ function sessionDateFromGeneratedAt(value) {
640
+ const date = value ? new Date(value) : new Date();
641
+ if (Number.isNaN(date.getTime())) {
642
+ return new Date().toISOString().slice(0, 10);
643
+ }
644
+
645
+ return date.toISOString().slice(0, 10);
646
+ }
647
+
648
+ function quoteYamlString(value) {
649
+ return JSON.stringify(value);
650
+ }