@vibecompass/vibecompass 0.2.1 → 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.
package/src/session.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { access, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { scanProjectMemory } from './project-memory.js';
3
4
  import { parseSimpleYaml } from './simple-yaml.js';
4
5
  import { buildCloseSessionGuidance, resolveWorkflowSettings } from './workflow.js';
5
6
  import { syncAgentInstructionFiles } from './generators/agent-files/index.js';
@@ -36,6 +37,18 @@ export async function startProjectSession(options) {
36
37
  const claude = await readClaudeFile(normalized.claudePath);
37
38
  const sessionId = await resolveStartSessionId(normalized, options);
38
39
  const lanePaths = getLanePaths(normalized, sessionId);
40
+ const features = normalizeStringArray(options?.features);
41
+ const repos = normalizeStringArray(options?.repos);
42
+ const claims = normalizeStringArray(options?.claims);
43
+ const existingLanes = await listActiveSessionLanes(normalized);
44
+ const overlapWarnings = buildOverlapWarnings({
45
+ sessionId,
46
+ features,
47
+ repos,
48
+ claims,
49
+ existingLanes,
50
+ });
51
+ const highestDecisionId = await readHighestDecisionId(normalized.rootDir);
39
52
 
40
53
  const nextSessionNumber = await getNextSessionNumber(normalized.sessionsDir, sessionDate);
41
54
  const currentSession = parseCurrentSessionBlock(claude.content);
@@ -68,9 +81,10 @@ export async function startProjectSession(options) {
68
81
  sessionDate,
69
82
  sessionNumber: nextSessionNumber,
70
83
  workingOn,
71
- features: normalizeStringArray(options?.features),
72
- repos: normalizeStringArray(options?.repos),
73
- claims: normalizeStringArray(options?.claims),
84
+ features,
85
+ repos,
86
+ claims,
87
+ highestDecisionId,
74
88
  }),
75
89
  'utf8',
76
90
  );
@@ -99,6 +113,8 @@ export async function startProjectSession(options) {
99
113
  status: 'active',
100
114
  workingOn,
101
115
  }, { current: sessionId });
116
+ const auditWarnings = await buildDocsReviewWarnings(normalized);
117
+ const metadataWarnings = collectLaneMetadataWarnings(existingLanes);
102
118
  const agentFileSync = await syncAgentInstructionFilesSafely({
103
119
  rootDir: normalized.rootDir,
104
120
  toolingRootDir: normalized.toolingRootDir,
@@ -114,6 +130,11 @@ export async function startProjectSession(options) {
114
130
  handoffFilePath: lanePaths.handoffFilePath,
115
131
  sessionDate,
116
132
  sessionNumber: nextSessionNumber,
133
+ warnings: [
134
+ ...metadataWarnings,
135
+ ...overlapWarnings,
136
+ ...auditWarnings,
137
+ ],
117
138
  agentFileSync,
118
139
  };
119
140
  }
@@ -314,6 +335,26 @@ async function syncAgentInstructionFilesSafely(options) {
314
335
  }
315
336
  }
316
337
 
338
+ async function buildDocsReviewWarnings(normalized) {
339
+ const markerPath = path.join(normalized.rootDir, 'state', 'docs-review.json');
340
+ try {
341
+ const marker = JSON.parse(await readFile(markerPath, 'utf8'));
342
+ if (marker && marker.status === 'completed') {
343
+ return [];
344
+ }
345
+ } catch (error) {
346
+ if (!(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT')) {
347
+ return [
348
+ `Docs-review marker at ${markerPath} is unreadable. Starter docs are not a comprehensive architecture review; run "vibecompass docs-review --guided" before risky implementation work.`,
349
+ ];
350
+ }
351
+ }
352
+
353
+ return [
354
+ `No docs-review marker found at ${markerPath}. Starter docs are not a comprehensive architecture review; run "vibecompass docs-review --guided" before risky implementation work.`,
355
+ ];
356
+ }
357
+
317
358
  function normalizeSessionPaths(options) {
318
359
  const cwd = options?.cwd ? path.resolve(options.cwd) : process.cwd();
319
360
  const rootDir = path.resolve(cwd, options?.rootDir ?? '.compass');
@@ -511,6 +552,9 @@ async function listActiveSessionLanes(normalized) {
511
552
  features: metadata.features,
512
553
  repos: metadata.repos,
513
554
  claims: metadata.claims,
555
+ startedAt: metadata.startedAt,
556
+ decisionSnapshot: metadata.decisionSnapshot,
557
+ warnings: metadata.warnings,
514
558
  sessionDate: metadata.sessionDate ?? wipSession?.sessionDate ?? null,
515
559
  sessionNumber: metadata.sessionNumber ?? wipSession?.sessionNumber ?? 0,
516
560
  });
@@ -544,18 +588,30 @@ async function readLaneMetadata(sessionFilePath) {
544
588
  features: normalizeStringArray(data.feature_slugs),
545
589
  repos: normalizeStringArray(data.repos),
546
590
  claims: normalizeStringArray(data.claimed_paths),
591
+ startedAt: normalizeOptionalString(data.started_at),
592
+ decisionSnapshot: {
593
+ highestDecisionId: parseNullableNumber(data.decision_snapshot?.highest_decision_id),
594
+ },
547
595
  sessionDate: normalizeOptionalString(data.session_date),
548
596
  sessionNumber: typeof data.session_number === 'number' ? data.session_number : Number(data.session_number) || null,
597
+ warnings: [],
549
598
  };
550
- } catch {
599
+ } catch (error) {
551
600
  return {
552
601
  status: null,
553
602
  workingOn: null,
554
603
  features: [],
555
604
  repos: [],
556
605
  claims: [],
606
+ startedAt: null,
607
+ decisionSnapshot: {
608
+ highestDecisionId: null,
609
+ },
557
610
  sessionDate: null,
558
611
  sessionNumber: null,
612
+ warnings: [
613
+ `Could not parse ${sessionFilePath}: ${error instanceof Error ? error.message : String(error)}`,
614
+ ],
559
615
  };
560
616
  }
561
617
  }
@@ -778,9 +834,9 @@ function renderLaneMetadata(options) {
778
834
  renderYamlArray('feature_slugs', options.features),
779
835
  renderYamlArray('repos', options.repos),
780
836
  renderYamlArray('claimed_paths', options.claims),
781
- `started_at: ${new Date().toISOString()}`,
837
+ `started_at: ${formatLocalDateTime(new Date())}`,
782
838
  'decision_snapshot:',
783
- ' highest_decision_id: null',
839
+ ` highest_decision_id: ${options.highestDecisionId ?? 'null'}`,
784
840
  '',
785
841
  ].join('\n');
786
842
  }
@@ -892,6 +948,124 @@ function normalizeSessionDate(value) {
892
948
  ].join('-');
893
949
  }
894
950
 
951
+ async function readHighestDecisionId(rootDir) {
952
+ try {
953
+ const scanResult = await scanProjectMemory(rootDir);
954
+ const decisionIds = scanResult.documents.flatMap((document) => document.extracted?.decision_ids ?? []);
955
+ return decisionIds.reduce((max, id) => Math.max(max, id), 0) || null;
956
+ } catch {
957
+ return null;
958
+ }
959
+ }
960
+
961
+ function buildOverlapWarnings(options) {
962
+ const warnings = [];
963
+ for (const lane of options.existingLanes) {
964
+ const sharedFeatures = intersect(options.features, lane.features ?? []);
965
+ if (sharedFeatures.length > 0) {
966
+ warnings.push(
967
+ `Active session lane "${options.sessionId}" overlaps "${lane.id}" on feature(s): ${sharedFeatures.join(', ')}.`,
968
+ );
969
+ }
970
+
971
+ const claimOverlaps = findClaimOverlaps(options.claims, lane.claims ?? []);
972
+ for (const overlap of claimOverlaps) {
973
+ warnings.push(
974
+ `Active session lane "${options.sessionId}" overlaps "${lane.id}" on claimed path ${overlap.repo}:${overlap.path}.`,
975
+ );
976
+ }
977
+
978
+ const sharedRepos = intersect(options.repos, lane.repos ?? []);
979
+ if (sharedRepos.length > 0 && options.claims.length === 0 && (lane.claims ?? []).length === 0) {
980
+ warnings.push(
981
+ `Active session lane "${options.sessionId}" shares repo(s) ${sharedRepos.join(', ')} with "${lane.id}" without path claims; add --claim values to clarify ownership.`,
982
+ );
983
+ }
984
+ }
985
+
986
+ return warnings;
987
+ }
988
+
989
+ function collectLaneMetadataWarnings(lanes) {
990
+ return lanes.flatMap((lane) => lane.warnings ?? []);
991
+ }
992
+
993
+ function intersect(left, right) {
994
+ const rightSet = new Set(right);
995
+ return left.filter((item) => rightSet.has(item));
996
+ }
997
+
998
+ function findClaimOverlaps(leftClaims, rightClaims) {
999
+ const overlaps = [];
1000
+ for (const left of leftClaims.map(parseClaim).filter(Boolean)) {
1001
+ for (const right of rightClaims.map(parseClaim).filter(Boolean)) {
1002
+ if (left.repo !== right.repo) {
1003
+ continue;
1004
+ }
1005
+
1006
+ if (pathPrefixesOverlap(left.path, right.path)) {
1007
+ overlaps.push({
1008
+ repo: left.repo,
1009
+ path: left.path.length <= right.path.length ? left.path : right.path,
1010
+ });
1011
+ }
1012
+ }
1013
+ }
1014
+
1015
+ return overlaps;
1016
+ }
1017
+
1018
+ function parseClaim(value) {
1019
+ const separatorIndex = value.indexOf(':');
1020
+ if (separatorIndex <= 0 || separatorIndex === value.length - 1) {
1021
+ return null;
1022
+ }
1023
+
1024
+ return {
1025
+ repo: value.slice(0, separatorIndex),
1026
+ path: normalizeClaimPath(value.slice(separatorIndex + 1)),
1027
+ };
1028
+ }
1029
+
1030
+ function normalizeClaimPath(value) {
1031
+ return value.replace(/^\/+/, '').replace(/\/+$/, '');
1032
+ }
1033
+
1034
+ function pathPrefixesOverlap(left, right) {
1035
+ return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
1036
+ }
1037
+
1038
+ function parseNullableNumber(value) {
1039
+ if (value === null || value === undefined) {
1040
+ return null;
1041
+ }
1042
+
1043
+ const number = typeof value === 'number' ? value : Number(value);
1044
+ return Number.isFinite(number) ? number : null;
1045
+ }
1046
+
1047
+ function formatLocalDateTime(date) {
1048
+ const offsetMinutes = -date.getTimezoneOffset();
1049
+ const sign = offsetMinutes >= 0 ? '+' : '-';
1050
+ const absOffset = Math.abs(offsetMinutes);
1051
+ const offset = `${sign}${String(Math.floor(absOffset / 60)).padStart(2, '0')}:${String(absOffset % 60).padStart(2, '0')}`;
1052
+
1053
+ return [
1054
+ date.getFullYear(),
1055
+ '-',
1056
+ String(date.getMonth() + 1).padStart(2, '0'),
1057
+ '-',
1058
+ String(date.getDate()).padStart(2, '0'),
1059
+ 'T',
1060
+ String(date.getHours()).padStart(2, '0'),
1061
+ ':',
1062
+ String(date.getMinutes()).padStart(2, '0'),
1063
+ ':',
1064
+ String(date.getSeconds()).padStart(2, '0'),
1065
+ offset,
1066
+ ].join('');
1067
+ }
1068
+
895
1069
  function requireNonEmptyString(value, message) {
896
1070
  const normalized = normalizeOptionalString(value);
897
1071
  if (!normalized) {