@vibecompass/vibecompass 0.2.0 → 0.3.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.
@@ -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.',
39
- '- Use `vibecompass start-session --id <lane-id>` for concurrent lanes. Omitting `--id` opens the compatibility `default` lane only when no other lanes are active.',
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
+ '- 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,6 @@
1
1
  export { STATE_VERSION, generateStateManifest, writeStateManifest } from './manifest.js';
2
2
  export { initializeProjectMemory } from './init.js';
3
+ export { preflightDocsReview } from './docs-review.js';
3
4
  export { scanProjectMemory } from './project-memory.js';
4
5
  export {
5
6
  closeProjectSession,
package/src/init.js CHANGED
@@ -21,10 +21,10 @@ export async function initializeProjectMemory(options) {
21
21
 
22
22
  await writeProjectFile(projectFilePath, normalized.projectConfig, normalized.force);
23
23
  const gitignoreUpdated = await ensureStateIgnored(gitignorePath);
24
+ const scaffoldResult = await scaffoldInitFiles(normalized.bootstrap);
24
25
  const manifestResult = await writeStateManifest(normalized.rootDir, {
25
26
  generatedAt: normalized.generatedAt,
26
27
  });
27
- const scaffoldResult = await scaffoldInitFiles(normalized.bootstrap);
28
28
 
29
29
  return {
30
30
  rootDir: normalized.rootDir,
@@ -137,6 +137,7 @@ function normalizeInitOptions(options) {
137
137
  rootDir,
138
138
  toolingRootDir,
139
139
  projectConfig,
140
+ generatedAt: preparedOptions.generatedAt,
140
141
  });
141
142
 
142
143
  return {
@@ -182,6 +183,7 @@ function normalizeBootstrapOptions(bootstrap, context) {
182
183
  contextRelativeToToolingRoot: toPosix(
183
184
  path.relative(context.toolingRootDir, contextFilePath) || 'context.md',
184
185
  ),
186
+ generatedAt: context.generatedAt,
185
187
  };
186
188
  }
187
189
 
package/src/manifest.js CHANGED
@@ -2,6 +2,7 @@ import { mkdir, 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,18 @@ 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 manifest = generateStateManifest(scanResult, {
63
+ ...options,
64
+ ...(activeSessions ? { activeSessions } : {}),
65
+ });
60
66
  const stateDir = path.join(rootDir, 'state');
61
67
  const manifestPath = path.join(stateDir, 'manifest.json');
62
68
 
@@ -70,6 +76,33 @@ export async function writeStateManifest(rootDir, options = {}) {
70
76
  };
71
77
  }
72
78
 
79
+ async function readActiveSessionsForManifest(rootDir) {
80
+ const sessions = await listProjectSessions({ rootDir });
81
+ if (sessions.lanes.length === 0 && !sessions.current) {
82
+ return null;
83
+ }
84
+
85
+ return {
86
+ current: sessions.current,
87
+ lanes: sessions.lanes.map((lane) => ({
88
+ id: lane.id,
89
+ status: lane.status ?? 'active',
90
+ working_on: lane.workingOn,
91
+ feature_slugs: lane.features ?? [],
92
+ repos: lane.repos ?? [],
93
+ claimed_paths: lane.claims ?? [],
94
+ started_at: lane.startedAt ?? null,
95
+ ...(lane.decisionSnapshot
96
+ ? {
97
+ decision_snapshot: {
98
+ highest_decision_id: lane.decisionSnapshot.highestDecisionId ?? null,
99
+ },
100
+ }
101
+ : {}),
102
+ })),
103
+ };
104
+ }
105
+
73
106
  function toIsoString(value) {
74
107
  return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
75
108
  }
@@ -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']);
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;
@@ -125,7 +197,7 @@ This workspace uses VibeCompass project memory rooted at \`${rootRelativePath}\`
125
197
 
126
198
  ## Session model
127
199
  - VibeCompass active builder sessions are named lanes. Use one lane per active feature or workstream.
128
- - \`vibecompass start-session\` without \`--id\` opens the compatibility \`default\` lane only when no other lanes are active; use \`--id <lane-id>\` for concurrent work.
200
+ - \`vibecompass start-session\` requires \`--id <lane-id>\` so each active lane has a meaningful feature or workstream name.
129
201
  - The active lane scratch files live under \`${rootRelativePath}/sessions/active/<lane-id>/\`.
130
202
  - \`${rootRelativePath}/sessions/active/index.yaml\` is the authoritative current lane pointer; the tool-specific Current session block is a human-readable continuity summary, not the lane-selection source of truth.
131
203
  - Finalized sessions are append-only notes named \`${rootRelativePath}/sessions/YYYY-MM-DD-N-title.md\`; multiple sessions on the same day increment \`N\`.
@@ -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/\`.
@@ -163,7 +241,7 @@ ${renderWorkflowDefaults(workflow)}
163
241
  6. Read the relevant docs under \`${rootRelativePath}/architecture/\` and \`${rootRelativePath}/decisions/\`.
164
242
 
165
243
  ## Builder workflow
166
- At session start, prefer running \`vibecompass start-session --id <lane-id> --working-on "..." \`. Omit \`--id\` only for the first/default lane in a project with no other active lanes.
244
+ At session start, prefer running \`vibecompass start-session --id <lane-id> --working-on "..." \`.
167
245
  If you manage files manually, create \`${rootRelativePath}/sessions/active/<lane-id>/session.yaml\` and \`${rootRelativePath}/sessions/active/index.yaml\`, then create \`${rootRelativePath}/sessions/active/<lane-id>/wip.md\` if it does not exist:
168
246
 
169
247
  \`\`\`md
@@ -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
+ }