@vibecompass/vibecompass 0.15.0 → 0.16.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.
package/src/cli.js CHANGED
@@ -5,6 +5,9 @@ import { pathToFileURL } from 'node:url';
5
5
  import { connectHostedProjectMemory, initializeProjectMemory, setDefaultSyncTarget } from './init.js';
6
6
  import { preflightDocsReview } from './docs-review.js';
7
7
  import { planDocsUpdate, renderDocsUpdatePlan } from './docs-update.js';
8
+ import { buildSessionBrief, resolveBriefRequest } from './brief.js';
9
+ import { generateLaneBrief, inspectLaneBrief } from './brief-lifecycle.js';
10
+ import { renderBrief } from './brief-render.js';
8
11
  import { resolveConnectHostedCliOptions, resolveInitCliOptions } from './setup.js';
9
12
  import { readFile } from 'node:fs/promises';
10
13
  import path from 'node:path';
@@ -167,6 +170,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
167
170
  io.stdout.write(`Runtime: port ${result.runtime.port}, temp dir ${result.runtime.tmpDir}\n`);
168
171
  io.stdout.write('Export into a shell with: eval "$(vibecompass lane-env)" (D-282; pass --session <lane-id> when multiple lanes are active; use --worktree for independently runnable same-repo lanes).\n');
169
172
  }
173
+ writeLifecycleBriefResult(io, result.brief);
170
174
  writeWarnings(io, result.warnings);
171
175
  writeAgentFileSyncResult(io, result.agentFileSync);
172
176
  return 0;
@@ -314,6 +318,94 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
314
318
  return 0;
315
319
  }
316
320
 
321
+ if (parsed.command === 'brief' && (parsed.options.write || parsed.options.check)) {
322
+ // Lane brief (D-364): --write regenerates the lane's brief.md under the
323
+ // lane root's lock; --check reports its freshness and writes nothing. A
324
+ // --source-root is only ever read.
325
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
326
+ const cwd = runtime.cwd ? path.resolve(runtime.cwd) : process.cwd();
327
+ const request = await resolveBriefRequest({
328
+ rootDir: parsed.options.rootDir,
329
+ sessionId: parsed.options.sessionId,
330
+ cwd,
331
+ });
332
+ if (!request.laneId) {
333
+ throw new Error(`brief ${parsed.options.write ? '--write' : '--check'} needs an active lane: pass --session <lane-id>, or run it from a lane worktree.`);
334
+ }
335
+ const sourceRootDir = parsed.options.sourceRootDir ? path.resolve(cwd, parsed.options.sourceRootDir) : undefined;
336
+ if (parsed.options.check) {
337
+ const inspection = await inspectLaneBrief({
338
+ rootDir: request.rootDir,
339
+ laneId: request.laneId,
340
+ sourceRootDir,
341
+ sourceSessionId: parsed.options.sourceSessionId,
342
+ });
343
+ if (parsed.options.json) {
344
+ io.stdout.write(`${JSON.stringify({
345
+ lane_id: request.laneId,
346
+ path: inspection.path,
347
+ exists: inspection.exists,
348
+ stale: inspection.stale,
349
+ reasons: inspection.reasons,
350
+ status: inspection.status,
351
+ generated_at: inspection.generatedAt,
352
+ }, null, 2)}\n`);
353
+ } else {
354
+ const state = !inspection.exists ? 'missing' : inspection.stale ? 'stale' : 'current';
355
+ io.stdout.write(`Brief: ${state}${inspection.status ? ` (status ${inspection.status}${inspection.generatedAt ? `, generated ${inspection.generatedAt}` : ''})` : ''} — ${inspection.path}\n`);
356
+ for (const reason of inspection.exists ? inspection.reasons : []) {
357
+ io.stdout.write(`- ${reason}\n`);
358
+ }
359
+ }
360
+ writeWarnings(io, request.warnings);
361
+ return 0;
362
+ }
363
+
364
+ const generated = await generateLaneBrief({
365
+ rootDir: request.rootDir,
366
+ laneId: request.laneId,
367
+ sourceRootDir,
368
+ sourceSessionId: parsed.options.sourceSessionId,
369
+ ...(parsed.options.budget !== undefined ? { budget: parsed.options.budget } : {}),
370
+ });
371
+ if (parsed.options.json) {
372
+ io.stdout.write(`${JSON.stringify({ lane_id: request.laneId, ...generated }, null, 2)}\n`);
373
+ } else {
374
+ const from = generated.source?.adapter ? ` from ${generated.source.root_dir}` : '';
375
+ io.stdout.write(`Brief: ${generated.status}${generated.state === 'written' ? '' : ` (${generated.state}: ${generated.reason})`} — wrote ${generated.path}${from}\n`);
376
+ }
377
+ writeWarnings(io, [...request.warnings, ...generated.warnings]);
378
+ return generated.state === 'written' ? 0 : 1;
379
+ }
380
+
381
+ if (parsed.command === 'brief') {
382
+ // Strictly read-only (plan task A3): no manifest refresh, state write,
383
+ // lane write, or agent-file sync, so it is safe against a legacy root.
384
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
385
+ const request = await resolveBriefRequest({
386
+ rootDir: parsed.options.rootDir,
387
+ sessionId: parsed.options.sessionId,
388
+ ...(runtime.cwd ? { cwd: runtime.cwd } : {}),
389
+ });
390
+ const result = await buildSessionBrief({
391
+ rootDir: request.rootDir,
392
+ laneId: request.laneId,
393
+ task: parsed.options.task,
394
+ files: parsed.options.files,
395
+ featureSlugs: parsed.options.featureSlugs,
396
+ claims: parsed.options.claims,
397
+ budget: parsed.options.budget,
398
+ });
399
+ const markdown = renderBrief(result);
400
+ if (parsed.options.json) {
401
+ io.stdout.write(`${JSON.stringify({ ...result, lane_source: request.laneSource, cli_warnings: request.warnings, markdown }, null, 2)}\n`);
402
+ } else {
403
+ io.stdout.write(markdown);
404
+ }
405
+ writeWarnings(io, request.warnings);
406
+ return 0;
407
+ }
408
+
317
409
  if (parsed.command === 'close-session' || parsed.command === 'end-session') {
318
410
  await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
319
411
  const result = await closeProjectSession({
@@ -360,6 +452,9 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
360
452
  io.stdout.write('Read next:\n');
361
453
  io.stdout.write(`- ${result.wipFilePath}\n`);
362
454
  io.stdout.write(`- ${result.handoffFilePath}\n`);
455
+ if (result.brief?.path && result.brief.state !== 'off' && !(result.brief.state === 'failed' && !result.brief.status)) {
456
+ io.stdout.write(`- ${result.brief.path} (session brief — read before planning)\n`);
457
+ }
363
458
  if (result.latestFinalizedSessionPath) {
364
459
  io.stdout.write(`- ${result.latestFinalizedSessionPath} (latest finalized note)\n`);
365
460
  }
@@ -377,6 +472,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
377
472
  io.stdout.write(`- ${lane.id}: ${lane.workingOn ?? 'No summary recorded'}\n`);
378
473
  }
379
474
  }
475
+ writeLifecycleBriefResult(io, result.brief);
380
476
  }
381
477
  writeWarnings(io, result.warnings);
382
478
  writeAgentFileSyncResult(io, result.agentFileSync);
@@ -824,6 +920,10 @@ export function parseCliArgs(argv) {
824
920
  return parseDocsUpdateArgs(rest);
825
921
  }
826
922
 
923
+ if (command === 'brief') {
924
+ return parseBriefArgs(rest);
925
+ }
926
+
827
927
  if (command === 'close-session' || command === 'end-session') {
828
928
  return parseCloseSessionArgs(rest);
829
929
  }
@@ -1100,6 +1200,10 @@ function parseStartSessionArgs(argv) {
1100
1200
  if (token.startsWith('--worktree=')) {
1101
1201
  throw new Error('Flag "--worktree" takes no value; the worktree container is fixed to <workspace>/worktrees/<lane-id> (D-279).');
1102
1202
  }
1203
+ if (token === '--no-brief') {
1204
+ parsed.brief = false;
1205
+ continue;
1206
+ }
1103
1207
 
1104
1208
  const value = argv[index + 1];
1105
1209
  if (value === undefined) {
@@ -1857,6 +1961,105 @@ function parseDocsUpdateArgs(argv) {
1857
1961
  };
1858
1962
  }
1859
1963
 
1964
+ function parseBriefArgs(argv) {
1965
+ const parsed = {
1966
+ files: [],
1967
+ featureSlugs: [],
1968
+ claims: [],
1969
+ };
1970
+
1971
+ for (let index = 0; index < argv.length; index += 1) {
1972
+ const token = argv[index];
1973
+ if (token === '--json') {
1974
+ parsed.json = true;
1975
+ continue;
1976
+ }
1977
+ if (token === '--write' || token === '--check') {
1978
+ parsed[token.slice(2)] = true;
1979
+ continue;
1980
+ }
1981
+
1982
+ if (!token.startsWith('--')) {
1983
+ throw new Error(`Unexpected argument "${token}".`);
1984
+ }
1985
+
1986
+ const value = argv[index + 1];
1987
+ if (value === undefined || value.startsWith('--')) {
1988
+ throw new Error(`Flag "${token}" requires a value.`);
1989
+ }
1990
+ index += 1;
1991
+
1992
+ switch (token) {
1993
+ case '--root':
1994
+ parsed.rootDir = value;
1995
+ break;
1996
+ case '--session':
1997
+ parsed.sessionId = value;
1998
+ break;
1999
+ case '--source-root':
2000
+ parsed.sourceRootDir = value;
2001
+ break;
2002
+ case '--source-session':
2003
+ parsed.sourceSessionId = value;
2004
+ break;
2005
+ case '--task':
2006
+ parsed.task = value;
2007
+ break;
2008
+ case '--files':
2009
+ case '--file':
2010
+ // `--files a b c` and repeated `--files a --files b` both work.
2011
+ parsed.files.push(value);
2012
+ while (argv[index + 1] !== undefined && !argv[index + 1].startsWith('--')) {
2013
+ index += 1;
2014
+ parsed.files.push(argv[index]);
2015
+ }
2016
+ break;
2017
+ case '--feature':
2018
+ parsed.featureSlugs.push(value);
2019
+ break;
2020
+ case '--claim':
2021
+ parsed.claims.push(value);
2022
+ break;
2023
+ case '--budget':
2024
+ if (!/^\d+$/.test(value)) {
2025
+ throw new Error(`--budget must be a whole number of estimated tokens (got "${value}").`);
2026
+ }
2027
+ parsed.budget = Number(value);
2028
+ break;
2029
+ default:
2030
+ throw new Error(`Unknown flag "${token}".`);
2031
+ }
2032
+ }
2033
+
2034
+ if (parsed.write && parsed.check) {
2035
+ throw new Error('Choose one of --write (regenerate the lane brief) or --check (report its freshness).');
2036
+ }
2037
+ if (parsed.write || parsed.check) {
2038
+ const laneDerived = [
2039
+ parsed.task !== undefined && '--task',
2040
+ parsed.files.length > 0 && '--files',
2041
+ parsed.featureSlugs.length > 0 && '--feature',
2042
+ parsed.claims.length > 0 && '--claim',
2043
+ parsed.check && parsed.budget !== undefined && '--budget',
2044
+ ].filter(Boolean);
2045
+ if (laneDerived.length > 0) {
2046
+ throw new Error(
2047
+ `${laneDerived.join(', ')} cannot be combined with ${parsed.write ? '--write' : '--check'}: a lane brief takes its task from the lane's working-on, its files from the lane's claims, and its features from the lane's feature slugs (update them with continue-session --working-on or the lane metadata).`,
2048
+ );
2049
+ }
2050
+ } else if (parsed.sourceRootDir || parsed.sourceSessionId) {
2051
+ throw new Error('--source-root and --source-session apply only to --write or --check; to print a brief of another root, pass --root.');
2052
+ }
2053
+ if (parsed.sourceSessionId && !parsed.sourceRootDir) {
2054
+ throw new Error('--source-session requires --source-root.');
2055
+ }
2056
+
2057
+ return {
2058
+ command: 'brief',
2059
+ options: parsed,
2060
+ };
2061
+ }
2062
+
1860
2063
  function parseListSessionsArgs(argv) {
1861
2064
  const parsed = {};
1862
2065
 
@@ -2130,6 +2333,10 @@ function parseContinueSessionArgs(argv) {
2130
2333
  parsed.json = true;
2131
2334
  continue;
2132
2335
  }
2336
+ if (token === '--no-brief') {
2337
+ parsed.brief = false;
2338
+ continue;
2339
+ }
2133
2340
 
2134
2341
  const value = argv[index + 1];
2135
2342
  if (value === undefined) {
@@ -2147,6 +2354,12 @@ function parseContinueSessionArgs(argv) {
2147
2354
  case '--session':
2148
2355
  parsed.sessionId = value;
2149
2356
  break;
2357
+ case '--source-root':
2358
+ parsed.sourceRootDir = value;
2359
+ break;
2360
+ case '--source-session':
2361
+ parsed.sourceSessionId = value;
2362
+ break;
2150
2363
  case '--working-on':
2151
2364
  parsed.workingOn = value;
2152
2365
  break;
@@ -2179,6 +2392,13 @@ function parseContinueSessionArgs(argv) {
2179
2392
  parsed.sessionId = positional[0];
2180
2393
  }
2181
2394
 
2395
+ if (parsed.brief === false && (parsed.sourceRootDir || parsed.sourceSessionId)) {
2396
+ throw new Error('--no-brief cannot be combined with --source-root or --source-session.');
2397
+ }
2398
+ if (parsed.sourceSessionId && !parsed.sourceRootDir) {
2399
+ throw new Error('--source-session requires --source-root.');
2400
+ }
2401
+
2182
2402
  return {
2183
2403
  command: 'continue-session',
2184
2404
  options: parsed,
@@ -2890,6 +3110,31 @@ function writeWarnings(io, warnings = []) {
2890
3110
  }
2891
3111
  }
2892
3112
 
3113
+ /** One status line for the lifecycle brief (D-364); failures also arrive as warnings. */
3114
+ function writeLifecycleBriefResult(io, brief) {
3115
+ if (!brief) return;
3116
+ const location = brief.path ? ` — ${brief.path}` : '';
3117
+ const from = brief.source?.adapter ? ` (from ${brief.source.root_dir})` : '';
3118
+ switch (brief.state) {
3119
+ case 'off':
3120
+ io.stdout.write(`Brief: off (${brief.reason})\n`);
3121
+ return;
3122
+ case 'kept':
3123
+ io.stdout.write(`Brief: current, kept (status ${brief.status ?? 'unknown'}${brief.generatedAt ? `, generated ${brief.generatedAt}` : ''})${location}\n`);
3124
+ return;
3125
+ case 'regenerated':
3126
+ io.stdout.write(`Brief: regenerated, ${brief.status}${from} (was stale: ${(brief.staleReasons ?? []).join('; ') || 'unknown'})${location}\n`);
3127
+ return;
3128
+ case 'written':
3129
+ io.stdout.write(`Brief: ${brief.status}${from}${location}\n`);
3130
+ return;
3131
+ default:
3132
+ io.stdout.write(brief.status
3133
+ ? `Brief: incomplete (${brief.state}: ${brief.reason})${location}\n`
3134
+ : 'Brief: not written (see warning)\n');
3135
+ }
3136
+ }
3137
+
2893
3138
  function splitAssignment(value, flagName) {
2894
3139
  const separatorIndex = value.indexOf('=');
2895
3140
  if (separatorIndex <= 0 || separatorIndex === value.length - 1) {
@@ -2919,10 +3164,14 @@ function usageText() {
2919
3164
  ' vibecompass status [options]',
2920
3165
  ' vibecompass refresh-workflow [--dry-run|--apply] [options]',
2921
3166
  ' vibecompass docs-update [--session <lane-id>] [options]',
2922
- ' vibecompass start-session --id <lane-id> --working-on <text> [--branch <name> [--worktree]] [options]',
3167
+ ' vibecompass brief --task <text> [--files <repo:path>...] [--feature <slug>] [--claim <path>] [--session <lane-id>] [--budget <n>] [--json] [options]',
3168
+ ' Read-only session brief: relevant memory, packed into an estimated token budget',
3169
+ ' vibecompass brief --write|--check [--session <lane-id>] [--source-root <path> [--source-session <lane-id>]] [--budget <n>] [--json] [options]',
3170
+ ' Regenerate the lane\'s sessions/active/<lane-id>/brief.md, or report whether it is stale (D-364)',
3171
+ ' vibecompass start-session --id <lane-id> --working-on <text> [--branch <name> [--worktree]] [--no-brief] [options]',
2923
3172
  ' vibecompass close-session --title <text> --completed <text> --architecture-docs <status> --decision-log <status> --session-maintenance <status> [options]',
2924
3173
  ' vibecompass end-session --title <text> --completed <text> --architecture-docs <status> --decision-log <status> --session-maintenance <status> [options] # alias',
2925
- ' vibecompass continue-session [<lane-id>] [--session <lane-id>] [--working-on <text>] [--json] [options]',
3174
+ ' vibecompass continue-session [<lane-id>] [--session <lane-id>] [--working-on <text>] [--no-brief | --source-root <path>] [--json] [options]',
2926
3175
  ' vibecompass list-sessions [--json] [options]',
2927
3176
  ' vibecompass switch-session <id> [options]',
2928
3177
  ' vibecompass rebuild-active-index [--current <lane-id>] [options]',
@@ -3048,6 +3297,7 @@ function usageText() {
3048
3297
  ' --last-thing-completed <text> Optional override for the CLAUDE.md current-session block',
3049
3298
  ' --blockers <text> Optional current blockers summary',
3050
3299
  ' --next-session-should <text> Optional current-session handoff summary',
3300
+ ' --no-brief Skip the lane brief for this command (sessions/active/<lane-id>/brief.md; D-359, D-364); lifecycle commands write it only when project.yaml sets brief.enabled: true (opt-in, D-368)',
3051
3301
  '',
3052
3302
  'Close-session options (also accepted by end-session):',
3053
3303
  ' --root <path> Project-memory root. Explicit --root wins; otherwise the nearest worktree lane marker supplies it, else .compass',
@@ -3075,7 +3325,23 @@ function usageText() {
3075
3325
  ' --last-thing-completed <text> Optional override for the CLAUDE.md current-session block',
3076
3326
  ' --blockers <text> Optional current blockers summary',
3077
3327
  ' --next-session-should <text> Optional current-session handoff summary',
3078
- ' --json Print the resume result (lane, paths, runtime, other lanes, warnings) as JSON',
3328
+ ' --json Print the resume result (lane, paths, runtime, other lanes, brief, warnings) as JSON',
3329
+ ' --no-brief Leave the lane brief as it is; with project.yaml brief.enabled: true (opt-in, D-368) resume keeps a current brief and regenerates a missing or stale one',
3330
+ ' --source-root <path> D-255 dual-root dogfood: build the brief from this memory root\'s matching lane (same id, session date, and number) and write it only into this lane; the source root is only read',
3331
+ ' --source-session <lane-id> With --source-root: the source lane id, which must equal the resumed lane',
3332
+ '',
3333
+ 'Brief options:',
3334
+ ' --root <path> Project-memory root. Explicit --root wins; otherwise the nearest worktree lane marker supplies it, else .compass',
3335
+ ' --session <lane-id> Lane to brief. Omitted: nearest worktree lane marker, else the single active lane; 2+ active lanes require --session or a marker (D-277)',
3336
+ ' --task <text> Task text (print mode). Omitted: the lane\'s working-on',
3337
+ ' --files <repo:path>... Files the task touches (print mode)',
3338
+ ' --feature <slug>, --claim <path> Repeatable feature slug and path claim (print mode)',
3339
+ ' --budget <n> Estimated-token budget (default 6000, minimum 800)',
3340
+ ' --write Regenerate the lane\'s brief.md from its working-on, claims, and feature slugs, under the root lock',
3341
+ ' --check Report whether the lane\'s brief.md is current or stale against its header bindings; writes nothing',
3342
+ ' --source-root <path> With --write/--check: read this memory root\'s matching lane instead (D-255 dogfood); never written',
3343
+ ' --source-session <lane-id> With --source-root: the source lane id, which must equal the lane being briefed',
3344
+ ' --json Print the result as JSON',
3079
3345
  '',
3080
3346
  'List/switch-session options:',
3081
3347
  ' --root <path> Project-memory root. Defaults to .compass',