@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/README.md +151 -7
- package/package.json +1 -1
- package/src/cli.js +149 -0
- package/src/docs-review.js +813 -0
- package/src/generators/agent-files/template.js +2 -2
- package/src/index.js +1 -0
- package/src/init.js +3 -1
- package/src/manifest.js +34 -1
- package/src/project-memory.js +1 -1
- package/src/scaffold.js +218 -12
- package/src/session.js +180 -6
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
|
|
72
|
-
repos
|
|
73
|
-
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()
|
|
837
|
+
`started_at: ${formatLocalDateTime(new Date())}`,
|
|
782
838
|
'decision_snapshot:',
|
|
783
|
-
|
|
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) {
|