@vibecompass/vibecompass 0.2.0 → 0.2.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.
package/README.md CHANGED
@@ -28,8 +28,6 @@ Not shipped yet:
28
28
 
29
29
  The package is publishable now as the file-contract and read-model core. The
30
30
  broader local-primary sync workflow is still being built on top of it.
31
- The lane-aware session workflow is intended to publish as `0.2.0`; the local
32
- package may remain at `0.1.0` until the publish pass.
33
31
 
34
32
  ## Requirements
35
33
 
@@ -84,6 +82,7 @@ npx -y @vibecompass/vibecompass init \
84
82
  --close-session-git-publish \
85
83
  --close-session-git-remote origin \
86
84
  --start-session \
85
+ --session-id mcp-dogfood \
87
86
  --session-working-on "Validate the local-first MCP workflow"
88
87
  ```
89
88
 
@@ -143,8 +142,8 @@ the builder closes the session with `vibecompass close-session --session
143
142
 
144
143
  Active builder sessions are named lanes under
145
144
  `sessions/active/<lane-id>/`. Use one lane per active feature or workstream.
146
- `start-session` without `--id` opens the compatibility `default` lane only
147
- when no other lanes are active; concurrent work should pass `--id`.
145
+ `start-session` requires `--id <lane-id>` for every lane, including the first
146
+ one in a project.
148
147
  Finalized sessions are append-only notes named
149
148
  `sessions/YYYY-MM-DD-N-title.md`, so multiple sessions on the same day
150
149
  increment `N`. Decisions remain append-only in `decisions/`; a session note
@@ -234,6 +233,7 @@ Options:
234
233
  - `--with-agents`: create a starter `AGENTS.md` if it does not already exist
235
234
  - `--start-session`: open the first builder session after init
236
235
  - `--session-working-on <text>`: required with `--start-session` outside guided mode
236
+ - `--session-id <lane-id>`: required with `--start-session`; names the first builder lane
237
237
  - `--close-session-git-publish`: store that the close-session workflow includes a Git publish step
238
238
  - `--close-session-git-remote <name>`: optional default Git remote name for that stored publish step
239
239
  - `--force`: overwrite an existing `project.yaml`
@@ -247,7 +247,7 @@ include a Git publish step and which remote name to use for that step.
247
247
  ### `vibecompass start-session`
248
248
 
249
249
  ```text
250
- vibecompass start-session --working-on <text> [options]
250
+ vibecompass start-session --id <lane-id> --working-on <text> [options]
251
251
  ```
252
252
 
253
253
  Options:
@@ -255,7 +255,7 @@ Options:
255
255
  - `--root <path>`: project-memory root; defaults to `.compass`
256
256
  - `--tooling-root <path>`: workspace root that contains `CLAUDE.md`; defaults to cwd
257
257
  - `--working-on <text>`: required active-session summary
258
- - `--id <lane-id>`: optional lane ID; defaults to `default` only when no lanes are active
258
+ - `--id <lane-id>`: required lane ID
259
259
  - `--feature <slug>`: repeatable feature slug for the lane
260
260
  - `--repo <id>`: repeatable repo ID for the lane
261
261
  - `--claim <path>`: repeatable path claim for overlap warnings
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecompass/vibecompass",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Local-first project-memory core for VibeCompass",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -62,6 +62,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
62
62
  const sessionResult = await startProjectSession({
63
63
  rootDir: initPlan.initOptions.rootDir,
64
64
  toolingRootDir: initPlan.initOptions.toolingRootDir,
65
+ sessionId: initPlan.sessionPlan.sessionId,
65
66
  workingOn: initPlan.sessionPlan.workingOn,
66
67
  ...(runtime.cwd ? { cwd: runtime.cwd } : {}),
67
68
  });
@@ -69,6 +70,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
69
70
  io.stdout.write(`Updated ${sessionResult.claudePath}\n`);
70
71
  io.stdout.write(`Created ${sessionResult.wipFilePath}\n`);
71
72
  io.stdout.write(`Created ${sessionResult.handoffFilePath}\n`);
73
+ writeAgentFileSyncResult(io, sessionResult.agentFileSync);
72
74
  }
73
75
 
74
76
  return 0;
@@ -83,6 +85,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
83
85
  io.stdout.write(`Updated ${result.claudePath}\n`);
84
86
  io.stdout.write(`Created ${result.wipFilePath}\n`);
85
87
  io.stdout.write(`Created ${result.handoffFilePath}\n`);
88
+ writeAgentFileSyncResult(io, result.agentFileSync);
86
89
  return 0;
87
90
  }
88
91
 
@@ -127,6 +130,8 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
127
130
  ...(runtime.cwd ? { cwd: runtime.cwd } : {}),
128
131
  });
129
132
  io.stdout.write(`Current session lane: ${result.current}\n`);
133
+ io.stdout.write(`Updated ${result.claudePath}\n`);
134
+ writeAgentFileSyncResult(io, result.agentFileSync);
130
135
  return 0;
131
136
  }
132
137
 
@@ -270,6 +275,9 @@ export function parseCliArgs(argv) {
270
275
  case '--session-working-on':
271
276
  parsed.sessionWorkingOn = value;
272
277
  break;
278
+ case '--session-id':
279
+ parsed.sessionId = value;
280
+ break;
273
281
  case '--close-session-git-remote':
274
282
  parsed.closeSessionGitRemote = value;
275
283
  break;
@@ -306,6 +314,7 @@ export function parseCliArgs(argv) {
306
314
  force: parsed.force,
307
315
  startSession: parsed.startSession,
308
316
  sessionWorkingOn: parsed.sessionWorkingOn,
317
+ sessionId: parsed.sessionId,
309
318
  closeSessionGitPublish: parsed.closeSessionGitPublish,
310
319
  closeSessionGitRemote: parsed.closeSessionGitRemote,
311
320
  ...(parsed.withWorkflow || parsed.withClaude || parsed.withAgents
@@ -604,7 +613,7 @@ function usageText() {
604
613
  return [
605
614
  'Usage:',
606
615
  ' vibecompass init --name <project-name> --mode <local-only|local-primary|hosted-only> --repo <id=remote> [options]',
607
- ' vibecompass start-session --working-on <text> [options]',
616
+ ' vibecompass start-session --id <lane-id> --working-on <text> [options]',
608
617
  ' vibecompass close-session --title <text> --completed <text> --next-step <text> [options]',
609
618
  ' vibecompass end-session --title <text> --completed <text> --next-step <text> [options] # alias',
610
619
  ' vibecompass list-sessions [options]',
@@ -629,6 +638,7 @@ function usageText() {
629
638
  ' --with-agents Create a starter AGENTS.md if missing',
630
639
  ' --start-session Open the first builder session after init',
631
640
  ' --session-working-on <text> Required with --start-session outside guided mode',
641
+ ' --session-id <lane-id> Required with --start-session; names the first builder lane',
632
642
  ' --close-session-git-publish Include a Git publish step in the stored close-session workflow',
633
643
  ' --close-session-git-remote <name> Default Git remote name for that close-session publish step',
634
644
  ' --force Overwrite an existing project.yaml',
@@ -637,7 +647,7 @@ function usageText() {
637
647
  ' --root <path> Project-memory root. Defaults to .compass',
638
648
  ' --tooling-root <path> Tooling root that contains CLAUDE.md. Defaults to cwd',
639
649
  ' --working-on <text> Required active-session summary',
640
- ' --id <lane-id> Optional active session lane ID. Defaults to default when no lanes exist',
650
+ ' --id <lane-id> Required active session lane ID',
641
651
  ' --feature <slug> Repeatable feature slug for the lane',
642
652
  ' --repo <id> Repeatable repo ID for the lane',
643
653
  ' --claim <path> Repeatable path claim for overlap warnings',
@@ -36,7 +36,7 @@ export function renderSharedInstructionBody(context, options = {}) {
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
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.',
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.',
package/src/scaffold.js CHANGED
@@ -125,7 +125,7 @@ This workspace uses VibeCompass project memory rooted at \`${rootRelativePath}\`
125
125
 
126
126
  ## Session model
127
127
  - 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.
128
+ - \`vibecompass start-session\` requires \`--id <lane-id>\` so each active lane has a meaningful feature or workstream name.
129
129
  - The active lane scratch files live under \`${rootRelativePath}/sessions/active/<lane-id>/\`.
130
130
  - \`${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
131
  - Finalized sessions are append-only notes named \`${rootRelativePath}/sessions/YYYY-MM-DD-N-title.md\`; multiple sessions on the same day increment \`N\`.
@@ -163,7 +163,7 @@ ${renderWorkflowDefaults(workflow)}
163
163
  6. Read the relevant docs under \`${rootRelativePath}/architecture/\` and \`${rootRelativePath}/decisions/\`.
164
164
 
165
165
  ## 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.
166
+ At session start, prefer running \`vibecompass start-session --id <lane-id> --working-on "..." \`.
167
167
  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
168
 
169
169
  \`\`\`md
package/src/session.js CHANGED
@@ -99,6 +99,10 @@ export async function startProjectSession(options) {
99
99
  status: 'active',
100
100
  workingOn,
101
101
  }, { current: sessionId });
102
+ const agentFileSync = await syncAgentInstructionFilesSafely({
103
+ rootDir: normalized.rootDir,
104
+ toolingRootDir: normalized.toolingRootDir,
105
+ });
102
106
 
103
107
  return {
104
108
  rootDir: normalized.rootDir,
@@ -110,6 +114,7 @@ export async function startProjectSession(options) {
110
114
  handoffFilePath: lanePaths.handoffFilePath,
111
115
  sessionDate,
112
116
  sessionNumber: nextSessionNumber,
117
+ agentFileSync,
113
118
  };
114
119
  }
115
120
 
@@ -185,9 +190,10 @@ export async function closeProjectSession(options) {
185
190
  );
186
191
 
187
192
  const hadHandoff = await fileExists(lanePaths.handoffFilePath);
193
+ let activeSessionIndex = null;
188
194
  if (sessionId) {
189
195
  await rm(lanePaths.laneDir, { recursive: true, force: true });
190
- await removeActiveSessionFromIndex(normalized, sessionId);
196
+ activeSessionIndex = await removeActiveSessionFromIndex(normalized, sessionId);
191
197
  } else {
192
198
  await rm(lanePaths.wipFilePath, { force: true });
193
199
  if (hadHandoff) {
@@ -195,16 +201,22 @@ export async function closeProjectSession(options) {
195
201
  }
196
202
  }
197
203
 
198
- const updatedClaude = replaceCurrentSessionBlock(claude.content, {
199
- date: `${activeSession.sessionDate} (session ${activeSession.sessionNumber})`,
200
- workingOn: 'Session closed. Ready for the next builder session.',
201
- lastThingCompleted:
202
- normalizeOptionalString(options?.lastThingCompleted) ??
203
- `Closed session ${activeSession.sessionNumber} and wrote \`${sessionRelativePath}\`.`,
204
- blockers: blockers.length > 0 ? summarizeList(blockers) : 'No blocker remains.',
205
- nextSessionShould:
206
- normalizeOptionalString(options?.nextSessionShould) ?? summarizeOrderedList(nextSteps),
207
- });
204
+ const closedSummary = `Closed session ${activeSession.sessionNumber} and wrote \`${sessionRelativePath}\`.`;
205
+ const updatedClaude = replaceCurrentSessionBlock(
206
+ claude.content,
207
+ activeSessionIndex?.current
208
+ ? buildCurrentSessionFieldsForLane(activeSessionIndex.lanes.find((lane) => lane.id === activeSessionIndex.current), {
209
+ lastThingCompleted: normalizeOptionalString(options?.lastThingCompleted) ?? closedSummary,
210
+ })
211
+ : {
212
+ date: `${activeSession.sessionDate} (session ${activeSession.sessionNumber})`,
213
+ workingOn: 'Session closed. Ready for the next builder session.',
214
+ lastThingCompleted: normalizeOptionalString(options?.lastThingCompleted) ?? closedSummary,
215
+ blockers: blockers.length > 0 ? summarizeList(blockers) : 'No blocker remains.',
216
+ nextSessionShould:
217
+ normalizeOptionalString(options?.nextSessionShould) ?? summarizeOrderedList(nextSteps),
218
+ },
219
+ );
208
220
 
209
221
  await writeFile(normalized.claudePath, updatedClaude, 'utf8');
210
222
  const agentFileSync = await syncAgentInstructionFilesSafely({
@@ -248,17 +260,35 @@ export async function switchProjectSession(options = {}) {
248
260
  const normalized = normalizeSessionPaths(options);
249
261
  const sessionId = validateLaneId(requireNonEmptyString(options?.sessionId, 'switch-session requires a session ID.'));
250
262
  const lanes = await listActiveSessionLanes(normalized);
263
+ const lane = lanes.find((item) => item.id === sessionId);
251
264
 
252
- if (!lanes.some((lane) => lane.id === sessionId)) {
265
+ if (!lane) {
253
266
  throw new Error(`Active session lane "${sessionId}" does not exist.`);
254
267
  }
255
268
 
256
269
  await upsertActiveSessionIndex(normalized, null, { current: sessionId });
270
+ const claude = await readClaudeFile(normalized.claudePath);
271
+ await writeFile(
272
+ normalized.claudePath,
273
+ replaceCurrentSessionBlock(
274
+ claude.content,
275
+ buildCurrentSessionFieldsForLane(lane, {
276
+ lastThingCompleted: `Switched current lane to ${sessionId}.`,
277
+ }),
278
+ ),
279
+ 'utf8',
280
+ );
281
+ const agentFileSync = await syncAgentInstructionFilesSafely({
282
+ rootDir: normalized.rootDir,
283
+ toolingRootDir: normalized.toolingRootDir,
284
+ });
257
285
 
258
286
  return {
259
287
  rootDir: normalized.rootDir,
288
+ claudePath: normalized.claudePath,
260
289
  current: sessionId,
261
290
  lanes,
291
+ agentFileSync,
262
292
  };
263
293
  }
264
294
 
@@ -397,6 +427,26 @@ function replaceCurrentSessionBlock(content, fields) {
397
427
  return `${content.slice(0, sessionFence.bodyStart)}${blockBody}${content.slice(sessionFence.bodyEnd)}`;
398
428
  }
399
429
 
430
+ function buildCurrentSessionFieldsForLane(lane, overrides = {}) {
431
+ if (!lane) {
432
+ throw new Error('Cannot update the Current session block without active lane metadata.');
433
+ }
434
+
435
+ const sessionDate = lane.sessionDate ?? normalizeSessionDate();
436
+ const sessionNumber = lane.sessionNumber || 0;
437
+ const workingOn = lane.workingOn ?? 'No summary recorded.';
438
+
439
+ return {
440
+ date: `${sessionDate} (session ${sessionNumber}, lane ${lane.id})`,
441
+ workingOn: `${workingOn} [${lane.id}]`,
442
+ lastThingCompleted:
443
+ overrides.lastThingCompleted ?? 'Current lane selected from `sessions/active/index.yaml`.',
444
+ blockers: overrides.blockers ?? 'No blocker recorded for the selected lane.',
445
+ nextSessionShould:
446
+ overrides.nextSessionShould ?? 'Continue the selected active lane from `sessions/active/index.yaml`.',
447
+ };
448
+ }
449
+
400
450
  async function getNextSessionNumber(sessionsDir, sessionDate) {
401
451
  const sessions = await listFinalizedSessions(sessionsDir);
402
452
  const activeSessions = await listActiveSessionLanes({ sessionsDir, activeSessionsDir: path.join(sessionsDir, 'active') });
@@ -414,18 +464,13 @@ async function resolveStartSessionId(normalized, options) {
414
464
  return validateLaneId(explicitId);
415
465
  }
416
466
 
417
- const lanes = await listActiveSessionLanes(normalized);
418
- if (lanes.length > 0) {
419
- throw new Error('Active session lanes already exist. Pass --id to start another lane explicitly.');
420
- }
421
-
422
467
  if ((await fileExists(normalized.wipFilePath)) || (await fileExists(normalized.handoffFilePath))) {
423
468
  throw new Error(
424
469
  `Legacy active session scratch files already exist under ${normalized.sessionsDir}. Close or recover that session before starting a lane.`,
425
470
  );
426
471
  }
427
472
 
428
- return 'default';
473
+ throw new Error('start-session requires --id <lane-id> so each active lane has a meaningful name.');
429
474
  }
430
475
 
431
476
  async function resolveExistingSessionId(normalized, options) {
@@ -547,16 +592,28 @@ async function upsertActiveSessionIndex(normalized, lane, options = {}) {
547
592
  await writeFile(normalized.activeSessionsIndexPath, renderActiveSessionIndex(current, [...laneMap.values()]), 'utf8');
548
593
  }
549
594
 
595
+ /**
596
+ * Removes a closed lane from the active index and returns the surviving lane
597
+ * state used to refresh the Current session block. `lanes` contains the full
598
+ * lane metadata read from each sibling lane's `session.yaml` / `wip.md`.
599
+ */
550
600
  async function removeActiveSessionFromIndex(normalized, sessionId) {
551
601
  const index = await readActiveSessionIndex(normalized);
552
602
  const lanes = (await listActiveSessionLanes(normalized)).filter((lane) => lane.id !== sessionId);
553
603
  const current = index.current === sessionId ? lanes[0]?.id ?? null : index.current;
554
604
  if (lanes.length === 0) {
555
605
  await rm(normalized.activeSessionsIndexPath, { force: true });
556
- return;
606
+ return {
607
+ current: null,
608
+ lanes,
609
+ };
557
610
  }
558
611
 
559
612
  await writeFile(normalized.activeSessionsIndexPath, renderActiveSessionIndex(current, lanes), 'utf8');
613
+ return {
614
+ current,
615
+ lanes,
616
+ };
560
617
  }
561
618
 
562
619
  async function listFinalizedSessions(sessionsDir) {
package/src/setup.js CHANGED
@@ -40,6 +40,10 @@ export async function resolveInitCliOptions(options, environment = {}) {
40
40
  throw new Error('init requires --session-working-on when --start-session is used without guided setup.');
41
41
  }
42
42
 
43
+ if (resolved.startSession && !resolved.sessionId) {
44
+ throw new Error('init requires --session-id when --start-session is used.');
45
+ }
46
+
43
47
  return {
44
48
  initOptions: {
45
49
  rootDir: resolved.rootDir,
@@ -62,6 +66,7 @@ export async function resolveInitCliOptions(options, environment = {}) {
62
66
  guidedSummary,
63
67
  sessionPlan: resolved.startSession
64
68
  ? {
69
+ sessionId: resolved.sessionId,
65
70
  workingOn: resolved.sessionWorkingOn,
66
71
  }
67
72
  : null,
@@ -278,6 +283,15 @@ async function completeGuidedInitOptions(options, environment) {
278
283
  if (!resolved.sessionWorkingOn) {
279
284
  resolved.sessionWorkingOn = await askInput(prompter, 'What are you working on?');
280
285
  }
286
+
287
+ if (!resolved.sessionId) {
288
+ resolved.sessionId = await askInput(prompter, 'Session lane id', {
289
+ defaultValue: suggestLaneId(resolved.sessionWorkingOn),
290
+ validate(value) {
291
+ return validateLaneIdForPrompt(value);
292
+ },
293
+ });
294
+ }
281
295
  }
282
296
 
283
297
  if (resolved.closeSessionGitPublish === undefined && (resolved.bootstrap.workflow || resolved.startSession)) {
@@ -669,6 +683,46 @@ function suggestRepoId(value) {
669
683
  return slug || 'repo';
670
684
  }
671
685
 
686
+ function suggestLaneId(value) {
687
+ const slug = String(value ?? '')
688
+ .toLowerCase()
689
+ .replace(/[^a-z0-9]+/g, '-')
690
+ .replace(/^-+|-+$/g, '')
691
+ .slice(0, 63)
692
+ .replace(/-+$/g, '');
693
+
694
+ return slug.length >= 3 ? slug : 'builder-session';
695
+ }
696
+
697
+ function validateLaneIdForPrompt(value) {
698
+ if (!/^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/.test(value)) {
699
+ return 'Lane id must be 3-64 lowercase letters, numbers, or hyphens, and must start and end with a letter or number.';
700
+ }
701
+
702
+ if ([
703
+ 'active',
704
+ 'current',
705
+ 'default',
706
+ 'false',
707
+ 'handoff',
708
+ 'index',
709
+ 'new',
710
+ 'no',
711
+ 'null',
712
+ 'off',
713
+ 'on',
714
+ 'sessions',
715
+ 'state',
716
+ 'true',
717
+ 'wip',
718
+ 'yes',
719
+ ].includes(value)) {
720
+ return `Lane id "${value}" is reserved.`;
721
+ }
722
+
723
+ return null;
724
+ }
725
+
672
726
  function getRepoDirectoryHints(repo) {
673
727
  const hints = new Set();
674
728