@vibecompass/vibecompass 0.3.4 → 0.5.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 +198 -14
- package/package.json +1 -1
- package/src/cli.js +263 -0
- package/src/compatibility.js +308 -0
- package/src/docs-review.js +51 -4
- package/src/generators/agent-files/index.js +2 -11
- package/src/generators/agent-files/template.js +33 -34
- package/src/index.js +5 -1
- package/src/init.js +42 -2
- package/src/manifest.js +16 -4
- package/src/project-memory.js +3 -1
- package/src/read-model.js +17 -1
- package/src/refresh-workflow.js +236 -0
- package/src/scaffold.js +81 -102
- package/src/setup.js +253 -11
- package/src/status.js +350 -0
- package/src/version.js +6 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { STATE_VERSION } from './manifest.js';
|
|
4
|
+
import { parseSimpleYaml } from './simple-yaml.js';
|
|
5
|
+
import { PACKAGE_VERSION } from './version.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Inspect root compatibility without writing files.
|
|
9
|
+
* `packageVersion` is a test override; production callers should omit it.
|
|
10
|
+
*/
|
|
11
|
+
export async function inspectProjectCompatibility(options = {}) {
|
|
12
|
+
const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
|
|
13
|
+
const rootDir = path.resolve(cwd, options.rootDir ?? '.compass');
|
|
14
|
+
const cliPackageVersion = normalizePackageVersion(options.packageVersion ?? PACKAGE_VERSION);
|
|
15
|
+
const projectFilePath = path.join(rootDir, 'project.yaml');
|
|
16
|
+
const manifestPath = path.join(rootDir, 'state', 'manifest.json');
|
|
17
|
+
const warnings = [];
|
|
18
|
+
|
|
19
|
+
const project = await readProjectStamp(projectFilePath);
|
|
20
|
+
const manifest = await readManifestState(manifestPath);
|
|
21
|
+
const packageStatus = project.status === 'unreadable'
|
|
22
|
+
? {
|
|
23
|
+
status: 'unknown',
|
|
24
|
+
rootVersion: null,
|
|
25
|
+
cliVersion: cliPackageVersion,
|
|
26
|
+
errorMessage: project.errorMessage,
|
|
27
|
+
}
|
|
28
|
+
: buildPackageStatus(project.packageVersion, cliPackageVersion);
|
|
29
|
+
const stateStatus = buildStateStatus(manifest);
|
|
30
|
+
// Package and state statuses intentionally use different vocabularies:
|
|
31
|
+
// canonical package-stamp drift is distinct from derived-state drift.
|
|
32
|
+
|
|
33
|
+
if (packageStatus.status === 'unknown') {
|
|
34
|
+
warnings.push(warning({
|
|
35
|
+
code: 'project-yaml-unreadable',
|
|
36
|
+
message: `project.yaml is unreadable. ${packageStatus.errorMessage}`,
|
|
37
|
+
errorMessage: packageStatus.errorMessage,
|
|
38
|
+
}));
|
|
39
|
+
} else if (packageStatus.status === 'legacy') {
|
|
40
|
+
warnings.push(warning({
|
|
41
|
+
code: 'package-version-legacy-root',
|
|
42
|
+
message: 'project.yaml has no metadata.package_version stamp; treating the root as legacy/unknown.',
|
|
43
|
+
cliVersion: cliPackageVersion,
|
|
44
|
+
}));
|
|
45
|
+
} else if (packageStatus.status === 'behind') {
|
|
46
|
+
warnings.push(warning({
|
|
47
|
+
code: 'package-version-behind',
|
|
48
|
+
message: `project.yaml metadata.package_version ${packageStatus.rootVersion} is older than CLI package ${cliPackageVersion}.`,
|
|
49
|
+
rootVersion: packageStatus.rootVersion,
|
|
50
|
+
cliVersion: cliPackageVersion,
|
|
51
|
+
}));
|
|
52
|
+
} else if (packageStatus.status === 'ahead') {
|
|
53
|
+
warnings.push(warning({
|
|
54
|
+
code: 'package-version-ahead',
|
|
55
|
+
message: `project.yaml metadata.package_version ${packageStatus.rootVersion} is newer than CLI package ${cliPackageVersion}.`,
|
|
56
|
+
rootVersion: packageStatus.rootVersion,
|
|
57
|
+
cliVersion: cliPackageVersion,
|
|
58
|
+
}));
|
|
59
|
+
} else if (packageStatus.status === 'invalid') {
|
|
60
|
+
warnings.push(warning({
|
|
61
|
+
code: 'package-version-invalid',
|
|
62
|
+
message: 'project.yaml metadata.package_version is present but is not a non-empty semver string.',
|
|
63
|
+
rootVersion: packageStatus.rootVersion,
|
|
64
|
+
cliVersion: cliPackageVersion,
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (stateStatus.status === 'missing') {
|
|
69
|
+
warnings.push(warning({
|
|
70
|
+
code: 'state-manifest-missing',
|
|
71
|
+
message: 'state/manifest.json is missing; commands that need derived state can regenerate it.',
|
|
72
|
+
}));
|
|
73
|
+
} else if (stateStatus.status === 'unreadable') {
|
|
74
|
+
warnings.push(warning({
|
|
75
|
+
code: 'state-manifest-unreadable',
|
|
76
|
+
message: `state/manifest.json is unreadable; commands that need derived state can discard and regenerate it. ${stateStatus.errorMessage}`,
|
|
77
|
+
errorMessage: stateStatus.errorMessage,
|
|
78
|
+
}));
|
|
79
|
+
} else if (stateStatus.status === 'unsupported') {
|
|
80
|
+
warnings.push(warning({
|
|
81
|
+
code: 'state-version-unsupported',
|
|
82
|
+
message: `state/manifest.json state_version ${stateStatus.observedVersion} is unsupported by this CLI (expected ${STATE_VERSION}); commands that need derived state can regenerate it.`,
|
|
83
|
+
observedVersion: stateStatus.observedVersion,
|
|
84
|
+
expectedVersion: STATE_VERSION,
|
|
85
|
+
packageObservedVersion: stateStatus.packageObservedVersion,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
rootDir,
|
|
91
|
+
projectFilePath,
|
|
92
|
+
manifestPath,
|
|
93
|
+
cliPackageVersion,
|
|
94
|
+
package: packageStatus,
|
|
95
|
+
state: stateStatus,
|
|
96
|
+
warnings,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function formatCompatibilityWarnings(result) {
|
|
101
|
+
return (result?.warnings ?? []).map((warning) => warning.message);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function readProjectStamp(projectFilePath) {
|
|
105
|
+
try {
|
|
106
|
+
const config = parseSimpleYaml(await readFile(projectFilePath, 'utf8'), {
|
|
107
|
+
sourceName: projectFilePath,
|
|
108
|
+
});
|
|
109
|
+
const metadata = isPlainObject(config.metadata) ? config.metadata : {};
|
|
110
|
+
return {
|
|
111
|
+
status: 'ok',
|
|
112
|
+
packageVersion: Object.hasOwn(metadata, 'package_version')
|
|
113
|
+
? metadata.package_version
|
|
114
|
+
: undefined,
|
|
115
|
+
};
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return {
|
|
118
|
+
status: 'unreadable',
|
|
119
|
+
packageVersion: undefined,
|
|
120
|
+
errorMessage: error instanceof Error ? error.message : 'Unable to read project.yaml.',
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function readManifestState(manifestPath) {
|
|
126
|
+
try {
|
|
127
|
+
return {
|
|
128
|
+
status: 'ok',
|
|
129
|
+
manifest: JSON.parse(await readFile(manifestPath, 'utf8')),
|
|
130
|
+
};
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
133
|
+
return { status: 'missing' };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
status: 'unreadable',
|
|
138
|
+
errorMessage: error instanceof Error ? error.message : 'Unable to read state/manifest.json.',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function buildPackageStatus(rootVersion, cliPackageVersion) {
|
|
144
|
+
if (rootVersion === undefined) {
|
|
145
|
+
return {
|
|
146
|
+
status: 'legacy',
|
|
147
|
+
rootVersion: null,
|
|
148
|
+
cliVersion: cliPackageVersion,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (typeof rootVersion !== 'string' || rootVersion.trim() === '') {
|
|
153
|
+
return {
|
|
154
|
+
status: 'invalid',
|
|
155
|
+
rootVersion,
|
|
156
|
+
cliVersion: cliPackageVersion,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const normalizedRootVersion = normalizePackageVersion(rootVersion);
|
|
161
|
+
if (!parseSemver(normalizedRootVersion)) {
|
|
162
|
+
return {
|
|
163
|
+
status: 'invalid',
|
|
164
|
+
rootVersion,
|
|
165
|
+
cliVersion: cliPackageVersion,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const comparison = comparePackageVersions(normalizedRootVersion, cliPackageVersion);
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
status: comparison === 0 ? 'current' : comparison < 0 ? 'behind' : 'ahead',
|
|
173
|
+
rootVersion: normalizedRootVersion,
|
|
174
|
+
cliVersion: cliPackageVersion,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildStateStatus(manifestResult) {
|
|
179
|
+
if (manifestResult.status === 'missing') {
|
|
180
|
+
return { status: 'missing' };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (manifestResult.status === 'unreadable') {
|
|
184
|
+
return {
|
|
185
|
+
status: 'unreadable',
|
|
186
|
+
errorMessage: manifestResult.errorMessage,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const manifest = manifestResult.manifest;
|
|
191
|
+
if (manifest?.state_version !== STATE_VERSION) {
|
|
192
|
+
return {
|
|
193
|
+
status: 'unsupported',
|
|
194
|
+
observedVersion: manifest?.state_version ?? null,
|
|
195
|
+
expectedVersion: STATE_VERSION,
|
|
196
|
+
packageObservedVersion:
|
|
197
|
+
typeof manifest?.package?.observed_version === 'string'
|
|
198
|
+
? manifest.package.observed_version
|
|
199
|
+
: null,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
status: 'current',
|
|
205
|
+
observedVersion: manifest.state_version,
|
|
206
|
+
expectedVersion: STATE_VERSION,
|
|
207
|
+
packageObservedVersion:
|
|
208
|
+
typeof manifest?.package?.observed_version === 'string'
|
|
209
|
+
? manifest.package.observed_version
|
|
210
|
+
: null,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function comparePackageVersions(left, right) {
|
|
215
|
+
const leftSemver = parseSemver(left);
|
|
216
|
+
const rightSemver = parseSemver(right);
|
|
217
|
+
if (!leftSemver || !rightSemver) {
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const leftParts = [leftSemver.major, leftSemver.minor, leftSemver.patch];
|
|
222
|
+
const rightParts = [rightSemver.major, rightSemver.minor, rightSemver.patch];
|
|
223
|
+
for (let index = 0; index < 3; index += 1) {
|
|
224
|
+
if (leftParts[index] < rightParts[index]) {
|
|
225
|
+
return -1;
|
|
226
|
+
}
|
|
227
|
+
if (leftParts[index] > rightParts[index]) {
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return comparePrerelease(leftSemver.prerelease, rightSemver.prerelease);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function parseSemver(version) {
|
|
236
|
+
const match = String(version).match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
|
|
237
|
+
if (!match) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
major: Number(match[1]),
|
|
243
|
+
minor: Number(match[2]),
|
|
244
|
+
patch: Number(match[3]),
|
|
245
|
+
prerelease: match[4] ? match[4].split('.') : [],
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function comparePrerelease(left, right) {
|
|
250
|
+
if (left.length === 0 && right.length === 0) {
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (left.length === 0) {
|
|
255
|
+
return 1;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (right.length === 0) {
|
|
259
|
+
return -1;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const length = Math.max(left.length, right.length);
|
|
263
|
+
for (let index = 0; index < length; index += 1) {
|
|
264
|
+
const leftPart = left[index];
|
|
265
|
+
const rightPart = right[index];
|
|
266
|
+
if (leftPart === undefined) {
|
|
267
|
+
return -1;
|
|
268
|
+
}
|
|
269
|
+
if (rightPart === undefined) {
|
|
270
|
+
return 1;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const leftNumeric = /^\d+$/.test(leftPart);
|
|
274
|
+
const rightNumeric = /^\d+$/.test(rightPart);
|
|
275
|
+
if (leftNumeric && rightNumeric) {
|
|
276
|
+
const difference = Number(leftPart) - Number(rightPart);
|
|
277
|
+
if (difference !== 0) {
|
|
278
|
+
return difference < 0 ? -1 : 1;
|
|
279
|
+
}
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (leftNumeric !== rightNumeric) {
|
|
284
|
+
return leftNumeric ? -1 : 1;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const comparison = leftPart.localeCompare(rightPart);
|
|
288
|
+
if (comparison !== 0) {
|
|
289
|
+
return comparison < 0 ? -1 : 1;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function normalizePackageVersion(version) {
|
|
297
|
+
return String(version).trim();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function isPlainObject(value) {
|
|
301
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function warning(fields) {
|
|
305
|
+
return Object.fromEntries(
|
|
306
|
+
Object.entries(fields).filter(([, value]) => value !== undefined),
|
|
307
|
+
);
|
|
308
|
+
}
|
package/src/docs-review.js
CHANGED
|
@@ -59,8 +59,10 @@ export async function preflightDocsReview(options = {}, environment = {}) {
|
|
|
59
59
|
const localProvider = resolveLocalProvider(options);
|
|
60
60
|
const runtime = resolveDocsReviewRuntime(project, {
|
|
61
61
|
env,
|
|
62
|
+
guided: Boolean(options.guided),
|
|
62
63
|
localProvider,
|
|
63
64
|
runLocal: shouldRunLocal(options),
|
|
65
|
+
anthropicEnvVar: options.anthropicEnvVar ?? 'ANTHROPIC_API_KEY',
|
|
64
66
|
});
|
|
65
67
|
const reviewPrompt = renderReviewPrompt({
|
|
66
68
|
project,
|
|
@@ -180,14 +182,19 @@ async function applyDocsReviewOutput(options) {
|
|
|
180
182
|
const blocks = parseArchitectureDocBlocks(source);
|
|
181
183
|
|
|
182
184
|
if (blocks.length === 0) {
|
|
185
|
+
const malformedFenceCount = countMalformedArchitectureDocFences(source);
|
|
186
|
+
if (malformedFenceCount > 0) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Malformed architecture doc fence found in ${outputPath}. Expected fence openings like \`\`\`vibecompass-architecture-doc path=architecture/domain/feature/component.md\` with exactly one path attribute and no extra attributes.`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
183
192
|
throw new Error(
|
|
184
193
|
`No accepted architecture doc blocks found in ${outputPath}. Expected fenced blocks like \`\`\`vibecompass-architecture-doc path=architecture/domain/feature/component.md\`.`,
|
|
185
194
|
);
|
|
186
195
|
}
|
|
187
196
|
|
|
188
|
-
|
|
189
|
-
validateArchitectureDocBlock(block);
|
|
190
|
-
}
|
|
197
|
+
validateArchitectureDocBlocks(blocks);
|
|
191
198
|
|
|
192
199
|
const applied = [];
|
|
193
200
|
for (const block of blocks) {
|
|
@@ -357,6 +364,13 @@ function validateDocsReviewMode(options) {
|
|
|
357
364
|
}
|
|
358
365
|
|
|
359
366
|
async function promptForReviewConfig(options, environment) {
|
|
367
|
+
if (options.llm && options.model) {
|
|
368
|
+
return normalizeReviewConfig({
|
|
369
|
+
llm: options.llm,
|
|
370
|
+
model: options.model,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
360
374
|
const prompter = createPrompter(environment.io, environment.runtime);
|
|
361
375
|
try {
|
|
362
376
|
if (!options.llm) {
|
|
@@ -485,6 +499,10 @@ function resolveDocsReviewRuntime(project, options) {
|
|
|
485
499
|
};
|
|
486
500
|
}
|
|
487
501
|
|
|
502
|
+
if (options.guided) {
|
|
503
|
+
return { provider: 'external' };
|
|
504
|
+
}
|
|
505
|
+
|
|
488
506
|
throw new Error(
|
|
489
507
|
`docs-review for local-primary projects requires a hosted sync binding or ${options.anthropicEnvVar}.`,
|
|
490
508
|
);
|
|
@@ -601,6 +619,32 @@ function parseArchitectureDocBlocks(source) {
|
|
|
601
619
|
return blocks;
|
|
602
620
|
}
|
|
603
621
|
|
|
622
|
+
function countMalformedArchitectureDocFences(source) {
|
|
623
|
+
const fencePattern = /^```vibecompass-architecture-doc\b.*$/gm;
|
|
624
|
+
const acceptedOpeningPattern = /^```vibecompass-architecture-doc\s+path=[^\s`]+\s*$/;
|
|
625
|
+
let count = 0;
|
|
626
|
+
let match;
|
|
627
|
+
|
|
628
|
+
while ((match = fencePattern.exec(source)) !== null) {
|
|
629
|
+
if (!acceptedOpeningPattern.test(match[0])) {
|
|
630
|
+
count += 1;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return count;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function validateArchitectureDocBlocks(blocks) {
|
|
638
|
+
const seenPaths = new Set();
|
|
639
|
+
for (const block of blocks) {
|
|
640
|
+
if (seenPaths.has(block.path)) {
|
|
641
|
+
throw new Error(`Accepted docs-review output contains duplicate architecture doc path: ${block.path}`);
|
|
642
|
+
}
|
|
643
|
+
seenPaths.add(block.path);
|
|
644
|
+
validateArchitectureDocBlock(block);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
604
648
|
function normalizeBlockPath(value) {
|
|
605
649
|
return String(value ?? '').trim().replaceAll('\\', '/');
|
|
606
650
|
}
|
|
@@ -625,7 +669,10 @@ function validateArchitectureDocBlock(block) {
|
|
|
625
669
|
throw new Error(`Accepted architecture doc ${block.path} must include frontmatter.`);
|
|
626
670
|
}
|
|
627
671
|
|
|
628
|
-
|
|
672
|
+
const requiredFields = block.path === 'architecture/overview/project-shape.md'
|
|
673
|
+
? ['status']
|
|
674
|
+
: ['domain', 'feature', 'component', 'status'];
|
|
675
|
+
for (const field of requiredFields) {
|
|
629
676
|
if (typeof frontmatter.data[field] !== 'string' || frontmatter.data[field].trim() === '') {
|
|
630
677
|
throw new Error(`Accepted architecture doc ${block.path} requires non-empty frontmatter field "${field}".`);
|
|
631
678
|
}
|
|
@@ -8,6 +8,7 @@ import { claudeMdFormat } from './claude-md.js';
|
|
|
8
8
|
import { agentsMdFormat } from './agents-md.js';
|
|
9
9
|
import { cursorRulesFormat } from './cursor-rules.js';
|
|
10
10
|
import { copilotInstructionsFormat } from './copilot-instructions.js';
|
|
11
|
+
import { getWorkflowConflictScanPatterns } from '../../workflows/registry.js';
|
|
11
12
|
|
|
12
13
|
const FORMATS = [
|
|
13
14
|
claudeMdFormat,
|
|
@@ -17,17 +18,7 @@ const FORMATS = [
|
|
|
17
18
|
];
|
|
18
19
|
|
|
19
20
|
const FORMAT_MAP = new Map(FORMATS.map((format) => [format.name, format]));
|
|
20
|
-
const CONFLICT_SCAN_PATTERNS =
|
|
21
|
-
'session',
|
|
22
|
-
'handoff',
|
|
23
|
-
'decisions',
|
|
24
|
-
'wip.md',
|
|
25
|
-
'review handoff',
|
|
26
|
-
'close-session',
|
|
27
|
-
'address review',
|
|
28
|
-
'builder',
|
|
29
|
-
'reviewer',
|
|
30
|
-
];
|
|
21
|
+
const CONFLICT_SCAN_PATTERNS = getWorkflowConflictScanPatterns();
|
|
31
22
|
|
|
32
23
|
export async function syncAgentInstructionFiles(options = {}) {
|
|
33
24
|
const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { renderPromptCommandLines } from '../../workflows/registry.js';
|
|
2
|
+
|
|
1
3
|
export function buildAgentContext(readModel) {
|
|
2
4
|
return {
|
|
3
5
|
projectName: readModel.project.name ?? 'Unnamed project',
|
|
@@ -16,47 +18,40 @@ export function renderSharedInstructionBody(context, options = {}) {
|
|
|
16
18
|
const intro =
|
|
17
19
|
options.intro ??
|
|
18
20
|
'Use VibeCompass project memory as the source of truth before making code changes.';
|
|
21
|
+
const contextPath = `${context.rootDir}/context.md`;
|
|
19
22
|
|
|
20
23
|
return [
|
|
21
24
|
`# ${heading}`,
|
|
22
25
|
'',
|
|
23
26
|
intro,
|
|
24
27
|
'',
|
|
25
|
-
`Read \`${
|
|
28
|
+
`This managed block is a compact bootloader. Read \`${contextPath}\` for the full VibeCompass workflow protocol; workflow-specific details live under \`${context.rootDir}/workflows/\`.`,
|
|
26
29
|
'',
|
|
27
30
|
'Existing project-specific coding, style, framework, and safety instructions outside this managed block remain authoritative. This block owns only VibeCompass project-memory workflow: sessions, handoffs, decisions, docs-review, and close-out.',
|
|
28
31
|
'',
|
|
29
|
-
'##
|
|
32
|
+
'## Project Snapshot',
|
|
30
33
|
`- Project memory root: \`${context.rootDir}\``,
|
|
31
|
-
|
|
32
|
-
'- Read the latest finalized session note under `sessions/` before resuming work.',
|
|
33
|
-
'- Read `sessions/active/index.yaml` when it exists, then read the selected lane under `sessions/active/<lane-id>/`.',
|
|
34
|
-
'- Read the selected lane `wip.md` and `handoff.md` when they exist; they are the active builder/reviewer scratch files.',
|
|
35
|
-
'- Read relevant `architecture/` and `decisions/` files before changing implementation.',
|
|
34
|
+
...renderProjectSnapshot(context),
|
|
36
35
|
'',
|
|
37
|
-
'##
|
|
38
|
-
|
|
36
|
+
'## Read Order',
|
|
37
|
+
`1. Read \`${contextPath}\`.`,
|
|
38
|
+
`2. Read the latest finalized session note under \`${context.rootDir}/sessions/\`.`,
|
|
39
|
+
`3. If present, read \`${context.rootDir}/sessions/active/index.yaml\`, then the selected lane's \`wip.md\` and \`handoff.md\`.`,
|
|
40
|
+
`4. Read relevant \`${context.rootDir}/architecture/\` and \`${context.rootDir}/decisions/\` docs before editing implementation.`,
|
|
39
41
|
'',
|
|
40
|
-
'##
|
|
41
|
-
|
|
42
|
-
'-
|
|
43
|
-
'- Use `vibecompass start-session --id <lane-id>` to open a named lane for every active feature or workstream.',
|
|
44
|
-
'- Use `vibecompass list-sessions` and `vibecompass switch-session <lane-id>` to inspect or change the current lane.',
|
|
45
|
-
'- Treat `sessions/active/index.yaml` as the current lane source of truth; tool-specific Current session blocks are continuity summaries.',
|
|
46
|
-
'- 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.',
|
|
47
|
-
'- Use `review handoff` to ask a reviewer to inspect the active handoff.',
|
|
48
|
-
'- Use `address review` to process reviewer feedback from the selected lane `wip.md` and `handoff.md`.',
|
|
49
|
-
'- 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.',
|
|
50
|
-
'- If stale scratch files block `start-session`, read them first, then close, recover, move, or delete them intentionally before retrying.',
|
|
51
|
-
'- Keep decisions append-only in `decisions/`. Session notes may reference decisions, but decisions are the durable source of truth.',
|
|
52
|
-
'- Treat generated agent files as views. Update canonical VibeCompass project memory instead of editing managed regions.',
|
|
42
|
+
'## Prompt Commands',
|
|
43
|
+
...renderPromptCommandLines({ rootRelativePath: context.rootDir }),
|
|
44
|
+
'- The prompt commands above are agent behaviors. The `vibecompass` CLI commands remain the filesystem mechanics behind them.',
|
|
53
45
|
'',
|
|
54
|
-
'##
|
|
55
|
-
|
|
46
|
+
'## Hard Rules',
|
|
47
|
+
'- Treat `sessions/active/index.yaml` as the current lane source of truth; tool-specific Current session blocks are continuity summaries.',
|
|
48
|
+
'- Keep the selected lane `wip.md` and `handoff.md` current during active builder/reviewer work.',
|
|
49
|
+
'- Keep decisions append-only in `decisions/`; append accepted architectural decisions before implementing them.',
|
|
50
|
+
'- Do not edit this managed block directly. Update canonical VibeCompass memory, then rerun package commands such as `vibecompass sync-agents`.',
|
|
56
51
|
].join('\n');
|
|
57
52
|
}
|
|
58
53
|
|
|
59
|
-
function
|
|
54
|
+
function renderProjectSnapshot(context) {
|
|
60
55
|
const lines = [];
|
|
61
56
|
|
|
62
57
|
if (context.description) {
|
|
@@ -71,18 +66,22 @@ function renderProjectShape(context) {
|
|
|
71
66
|
lines.push(`- Repo \`${repo.id}\`: ${repo.remote}`);
|
|
72
67
|
}
|
|
73
68
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
lines.push(`-
|
|
69
|
+
const domainCount = context.domains.length;
|
|
70
|
+
if (domainCount > 0) {
|
|
71
|
+
lines.push(`- Architecture domains recorded: ${domainCount}`);
|
|
77
72
|
}
|
|
78
73
|
|
|
79
|
-
|
|
80
|
-
}
|
|
74
|
+
if (context.recentDecisions.length > 0) {
|
|
75
|
+
lines.push(`- Latest decision: ${renderDecisionLine(context.recentDecisions[0])}`);
|
|
76
|
+
}
|
|
81
77
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return ['- No decisions recorded yet.'];
|
|
78
|
+
if (context.recentSessions.length > 0) {
|
|
79
|
+
lines.push(`- Latest session: ${context.recentSessions[0].title ?? context.recentSessions[0].path}`);
|
|
85
80
|
}
|
|
86
81
|
|
|
87
|
-
return
|
|
82
|
+
return lines.length > 0 ? lines : ['- No project details recorded yet.'];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderDecisionLine(decision) {
|
|
86
|
+
return `D-${String(decision.decision_id).padStart(3, '0')} — ${decision.title}`;
|
|
88
87
|
}
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
export { STATE_VERSION, generateStateManifest, writeStateManifest } from './manifest.js';
|
|
1
|
+
export { STATE_VERSION, generateStateManifest, prepareStateManifest, writeStateManifest } from './manifest.js';
|
|
2
|
+
export { inspectProjectCompatibility, formatCompatibilityWarnings } from './compatibility.js';
|
|
3
|
+
export { PACKAGE_VERSION } from './version.js';
|
|
4
|
+
export { refreshWorkflow } from './refresh-workflow.js';
|
|
5
|
+
export { getProjectStatus, renderStatusText, toStatusJson } from './status.js';
|
|
2
6
|
export { initializeProjectMemory } from './init.js';
|
|
3
7
|
export { preflightDocsReview } from './docs-review.js';
|
|
4
8
|
export {
|
package/src/init.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { access, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { serializeProjectConfig } from './project-yaml.js';
|
|
4
4
|
import { writeStateManifest } from './manifest.js';
|
|
@@ -6,12 +6,17 @@ import { scaffoldInitFiles } from './scaffold.js';
|
|
|
6
6
|
import { applyPlacementDefaults } from './setup.js';
|
|
7
7
|
import { buildWorkflowMetadata } from './workflow.js';
|
|
8
8
|
import { parseSimpleYaml } from './simple-yaml.js';
|
|
9
|
+
import { PACKAGE_VERSION } from './version.js';
|
|
9
10
|
|
|
10
11
|
const VALID_MODES = new Set(['local-only', 'local-primary', 'hosted-only']);
|
|
11
12
|
|
|
12
13
|
export async function initializeProjectMemory(options) {
|
|
13
14
|
const normalized = normalizeInitOptions(options);
|
|
14
15
|
|
|
16
|
+
if (normalized.force && !normalized.replaceActiveLanes) {
|
|
17
|
+
await assertNoActiveSessionLanes(normalized.rootDir);
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
await mkdir(normalized.rootDir, { recursive: true });
|
|
16
21
|
await mkdir(path.join(normalized.rootDir, 'architecture'), { recursive: true });
|
|
17
22
|
await mkdir(path.join(normalized.rootDir, 'decisions'), { recursive: true });
|
|
@@ -137,7 +142,10 @@ function normalizeInitOptions(options) {
|
|
|
137
142
|
mode: preparedOptions.mode,
|
|
138
143
|
repos,
|
|
139
144
|
...(sync ? { sync } : {}),
|
|
140
|
-
metadata: buildAgentFilesMetadata(buildWorkflowMetadata(
|
|
145
|
+
metadata: buildAgentFilesMetadata(buildWorkflowMetadata({
|
|
146
|
+
...(preparedOptions.metadata && typeof preparedOptions.metadata === 'object' ? preparedOptions.metadata : {}),
|
|
147
|
+
package_version: PACKAGE_VERSION,
|
|
148
|
+
}, {
|
|
141
149
|
gitPublish:
|
|
142
150
|
typeof preparedOptions.closeSessionGitPublish === 'boolean'
|
|
143
151
|
? preparedOptions.closeSessionGitPublish
|
|
@@ -156,12 +164,44 @@ function normalizeInitOptions(options) {
|
|
|
156
164
|
return {
|
|
157
165
|
rootDir,
|
|
158
166
|
force: Boolean(preparedOptions.force),
|
|
167
|
+
replaceActiveLanes: Boolean(preparedOptions.replaceActiveLanes),
|
|
159
168
|
generatedAt: preparedOptions.generatedAt,
|
|
160
169
|
projectConfig,
|
|
161
170
|
bootstrap: bootstrapOptions,
|
|
162
171
|
};
|
|
163
172
|
}
|
|
164
173
|
|
|
174
|
+
async function assertNoActiveSessionLanes(rootDir) {
|
|
175
|
+
const activeDir = path.join(rootDir, 'sessions', 'active');
|
|
176
|
+
let entries = [];
|
|
177
|
+
try {
|
|
178
|
+
entries = await readdir(activeDir, { withFileTypes: true });
|
|
179
|
+
} catch {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const activeLaneIds = [];
|
|
184
|
+
for (const entry of entries) {
|
|
185
|
+
if (!entry.isDirectory()) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const laneDir = path.join(activeDir, entry.name);
|
|
190
|
+
if (
|
|
191
|
+
(await fileExists(path.join(laneDir, 'session.yaml'))) ||
|
|
192
|
+
(await fileExists(path.join(laneDir, 'wip.md')))
|
|
193
|
+
) {
|
|
194
|
+
activeLaneIds.push(entry.name);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (activeLaneIds.length > 0) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`Cannot overwrite project memory while active session lanes exist: ${activeLaneIds.join(', ')}. Close the lanes first or pass --replace-active-lanes.`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
165
205
|
function normalizeSyncOptions(syncOptions, mode) {
|
|
166
206
|
if (!syncOptions || typeof syncOptions !== 'object') {
|
|
167
207
|
throw new Error('sync configuration must be an object.');
|
package/src/manifest.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { localRevisionFromManifestHash, stableHash } from './hash.js';
|
|
4
4
|
import { scanProjectMemory } from './project-memory.js';
|
|
5
5
|
import { listProjectSessions } from './session.js';
|
|
6
|
+
import { PACKAGE_VERSION } from './version.js';
|
|
6
7
|
|
|
7
8
|
export const STATE_VERSION = 1;
|
|
8
9
|
|
|
@@ -50,13 +51,17 @@ export function generateStateManifest(scanResult, options = {}) {
|
|
|
50
51
|
document_count: manifestInventory.length,
|
|
51
52
|
warning_count: warningCount,
|
|
52
53
|
},
|
|
54
|
+
package: {
|
|
55
|
+
observed_version: options.packageVersion ?? PACKAGE_VERSION,
|
|
56
|
+
observed_at: generatedAt,
|
|
57
|
+
},
|
|
53
58
|
documents: manifestDocuments,
|
|
54
59
|
...(options.activeSessions ? { active_sessions: options.activeSessions } : {}),
|
|
55
60
|
...(options.sync ? { sync: options.sync } : {}),
|
|
56
61
|
};
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
export async function
|
|
64
|
+
export async function prepareStateManifest(rootDir, options = {}) {
|
|
60
65
|
const scanResult = await scanProjectMemory(rootDir);
|
|
61
66
|
const activeSessions = await readActiveSessionsForManifest(rootDir);
|
|
62
67
|
const existingSync = Object.hasOwn(options, 'sync')
|
|
@@ -70,9 +75,6 @@ export async function writeStateManifest(rootDir, options = {}) {
|
|
|
70
75
|
const stateDir = path.join(rootDir, 'state');
|
|
71
76
|
const manifestPath = path.join(stateDir, 'manifest.json');
|
|
72
77
|
|
|
73
|
-
await mkdir(stateDir, { recursive: true });
|
|
74
|
-
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
75
|
-
|
|
76
78
|
return {
|
|
77
79
|
manifest,
|
|
78
80
|
manifestPath,
|
|
@@ -80,6 +82,16 @@ export async function writeStateManifest(rootDir, options = {}) {
|
|
|
80
82
|
};
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
export async function writeStateManifest(rootDir, options = {}) {
|
|
86
|
+
const prepared = await prepareStateManifest(rootDir, options);
|
|
87
|
+
const stateDir = path.dirname(prepared.manifestPath);
|
|
88
|
+
|
|
89
|
+
await mkdir(stateDir, { recursive: true });
|
|
90
|
+
await writeFile(prepared.manifestPath, `${JSON.stringify(prepared.manifest, null, 2)}\n`, 'utf8');
|
|
91
|
+
|
|
92
|
+
return prepared;
|
|
93
|
+
}
|
|
94
|
+
|
|
83
95
|
async function readExistingSyncState(rootDir) {
|
|
84
96
|
try {
|
|
85
97
|
const existing = JSON.parse(
|