@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
package/src/setup.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { access, readFile, readdir, realpath } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { createInterface } from 'node:readline/promises';
|
|
5
6
|
import { promisify } from 'node:util';
|
|
6
7
|
import { getSupportedAgentFilePaths } from './generators/agent-files/index.js';
|
|
7
8
|
import { END_MARKER, START_MARKER } from './generators/agent-files/markers.js';
|
|
9
|
+
import { parseSimpleYaml } from './simple-yaml.js';
|
|
10
|
+
import { getWorkflowConflictScanPatterns } from './workflows/registry.js';
|
|
8
11
|
|
|
9
12
|
const execFileAsync = promisify(execFile);
|
|
10
13
|
|
|
@@ -22,6 +25,24 @@ export async function resolveInitCliOptions(options, environment = {}) {
|
|
|
22
25
|
let guidedSummary = null;
|
|
23
26
|
|
|
24
27
|
if (resolved.guided) {
|
|
28
|
+
if (!resolved.force) {
|
|
29
|
+
const existingProject = await resolveExistingGuidedProject(resolved, {
|
|
30
|
+
cwd,
|
|
31
|
+
io: environment.io,
|
|
32
|
+
runtime: environment.runtime,
|
|
33
|
+
});
|
|
34
|
+
if (existingProject) {
|
|
35
|
+
return {
|
|
36
|
+
existingProject,
|
|
37
|
+
initOptions: null,
|
|
38
|
+
placementPattern: null,
|
|
39
|
+
guidedSummary: null,
|
|
40
|
+
agentFileSyncPlan: null,
|
|
41
|
+
sessionPlan: null,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
25
46
|
const prompter = createPrompter(environment.io, environment.runtime);
|
|
26
47
|
try {
|
|
27
48
|
const guided = await completeGuidedInitOptions(resolved, {
|
|
@@ -60,6 +81,7 @@ export async function resolveInitCliOptions(options, environment = {}) {
|
|
|
60
81
|
mode: resolved.mode,
|
|
61
82
|
repos: resolved.repos,
|
|
62
83
|
force: resolved.force,
|
|
84
|
+
replaceActiveLanes: resolved.replaceActiveLanes,
|
|
63
85
|
sync: resolved.sync,
|
|
64
86
|
bootstrap: resolved.bootstrap,
|
|
65
87
|
generatedAt: resolved.generatedAt,
|
|
@@ -121,6 +143,212 @@ export async function resolveConnectHostedCliOptions(options, environment = {})
|
|
|
121
143
|
};
|
|
122
144
|
}
|
|
123
145
|
|
|
146
|
+
async function resolveExistingGuidedProject(options, environment) {
|
|
147
|
+
const candidates = await findExistingGuidedProjectRoots(options, environment.cwd);
|
|
148
|
+
if (candidates.length === 0) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (candidates.length === 1) {
|
|
153
|
+
if (candidates[0].discovery === 'ancestor' && canPrompt(environment)) {
|
|
154
|
+
const prompter = createPrompter(environment.io, environment.runtime);
|
|
155
|
+
try {
|
|
156
|
+
const useExistingRoot = await askConfirm(
|
|
157
|
+
prompter,
|
|
158
|
+
`Found existing VibeCompass project memory root at ${candidates[0].displayPath}. Show its info instead of initializing a new root here?`,
|
|
159
|
+
true,
|
|
160
|
+
);
|
|
161
|
+
if (!useExistingRoot) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
} finally {
|
|
165
|
+
await prompter.close();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return readExistingGuidedProject(candidates[0], environment.cwd);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
status: 'ambiguous',
|
|
174
|
+
candidates,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function findExistingGuidedProjectRoots(options, cwd) {
|
|
179
|
+
const candidateRoots = options.rootDir
|
|
180
|
+
? [{
|
|
181
|
+
rootDir: path.resolve(cwd, options.rootDir),
|
|
182
|
+
discovery: 'explicit',
|
|
183
|
+
}]
|
|
184
|
+
: await buildDefaultExistingRootCandidates(cwd);
|
|
185
|
+
|
|
186
|
+
const existing = [];
|
|
187
|
+
const seen = new Set();
|
|
188
|
+
for (const candidate of candidateRoots) {
|
|
189
|
+
const rootDir = candidate.rootDir;
|
|
190
|
+
if (seen.has(rootDir)) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
seen.add(rootDir);
|
|
194
|
+
|
|
195
|
+
if (await fileExists(path.join(rootDir, 'project.yaml'))) {
|
|
196
|
+
existing.push({
|
|
197
|
+
rootDir,
|
|
198
|
+
displayPath: toRelativeIfInsideCwd(rootDir, cwd),
|
|
199
|
+
discovery: candidate.discovery,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return existing;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function buildDefaultExistingRootCandidates(cwd) {
|
|
208
|
+
const candidates = [
|
|
209
|
+
{
|
|
210
|
+
rootDir: path.resolve(cwd, '.compass'),
|
|
211
|
+
discovery: 'direct',
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
rootDir: path.resolve(cwd),
|
|
215
|
+
discovery: 'direct',
|
|
216
|
+
},
|
|
217
|
+
];
|
|
218
|
+
const ancestorStopDir = await resolveAncestorSearchStopDir(cwd);
|
|
219
|
+
if (path.resolve(cwd) === ancestorStopDir) {
|
|
220
|
+
return candidates;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
for (const directory of ancestorDirectories(path.dirname(cwd), ancestorStopDir)) {
|
|
224
|
+
candidates.push(
|
|
225
|
+
{
|
|
226
|
+
rootDir: path.join(directory, '.compass'),
|
|
227
|
+
discovery: 'ancestor',
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
rootDir: directory,
|
|
231
|
+
discovery: 'ancestor',
|
|
232
|
+
},
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return candidates;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function resolveAncestorSearchStopDir(cwd) {
|
|
240
|
+
const homeDir = path.resolve(os.homedir());
|
|
241
|
+
if (path.resolve(cwd) === homeDir) {
|
|
242
|
+
return homeDir;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const gitRoot = await readGitRoot(cwd);
|
|
246
|
+
if (!gitRoot) {
|
|
247
|
+
return isPathInside(cwd, homeDir) || path.resolve(cwd) === homeDir
|
|
248
|
+
? homeDir
|
|
249
|
+
: path.parse(cwd).root;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const gitRootParent = path.dirname(path.resolve(gitRoot));
|
|
253
|
+
if (isPathInside(gitRootParent, homeDir) || gitRootParent === homeDir) {
|
|
254
|
+
return gitRootParent;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return isPathInside(cwd, homeDir) || path.resolve(cwd) === homeDir
|
|
258
|
+
? homeDir
|
|
259
|
+
: path.parse(cwd).root;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function ancestorDirectories(startDir, stopDir) {
|
|
263
|
+
const directories = [];
|
|
264
|
+
let current = path.resolve(startDir);
|
|
265
|
+
const stop = path.resolve(stopDir);
|
|
266
|
+
while (true) {
|
|
267
|
+
directories.push(current);
|
|
268
|
+
if (current === stop || current === path.dirname(current)) {
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
current = path.dirname(current);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return directories;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function readExistingGuidedProject(candidate, cwd) {
|
|
278
|
+
const projectFilePath = path.join(candidate.rootDir, 'project.yaml');
|
|
279
|
+
const activeSummary = await readActiveLaneSummary(candidate.rootDir);
|
|
280
|
+
try {
|
|
281
|
+
const projectConfig = parseSimpleYaml(await readFile(projectFilePath, 'utf8'), {
|
|
282
|
+
sourceName: projectFilePath,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
status: 'ok',
|
|
287
|
+
rootDir: candidate.rootDir,
|
|
288
|
+
displayPath: candidate.displayPath ?? toRelativeIfInsideCwd(candidate.rootDir, cwd),
|
|
289
|
+
projectFilePath,
|
|
290
|
+
projectConfig,
|
|
291
|
+
activeSummary,
|
|
292
|
+
};
|
|
293
|
+
} catch (error) {
|
|
294
|
+
return {
|
|
295
|
+
status: 'unreadable',
|
|
296
|
+
rootDir: candidate.rootDir,
|
|
297
|
+
displayPath: candidate.displayPath ?? toRelativeIfInsideCwd(candidate.rootDir, cwd),
|
|
298
|
+
projectFilePath,
|
|
299
|
+
errorMessage: error instanceof Error ? error.message : 'Unable to read project.yaml.',
|
|
300
|
+
activeSummary,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function readActiveLaneSummary(rootDir) {
|
|
306
|
+
const activeDir = path.join(rootDir, 'sessions', 'active');
|
|
307
|
+
let count = 0;
|
|
308
|
+
try {
|
|
309
|
+
const entries = await readdir(activeDir, { withFileTypes: true });
|
|
310
|
+
for (const entry of entries) {
|
|
311
|
+
if (!entry.isDirectory()) {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const laneDir = path.join(activeDir, entry.name);
|
|
316
|
+
if (
|
|
317
|
+
(await fileExists(path.join(laneDir, 'session.yaml'))) ||
|
|
318
|
+
(await fileExists(path.join(laneDir, 'wip.md')))
|
|
319
|
+
) {
|
|
320
|
+
count += 1;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} catch {
|
|
324
|
+
count = 0;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let current = null;
|
|
328
|
+
try {
|
|
329
|
+
const indexPath = path.join(activeDir, 'index.yaml');
|
|
330
|
+
const activeIndex = parseSimpleYaml(await readFile(indexPath, 'utf8'), {
|
|
331
|
+
sourceName: indexPath,
|
|
332
|
+
});
|
|
333
|
+
current = typeof activeIndex.current === 'string' && activeIndex.current !== 'null'
|
|
334
|
+
? activeIndex.current
|
|
335
|
+
: null;
|
|
336
|
+
} catch {
|
|
337
|
+
current = null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return { count, current };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function canPrompt(environment) {
|
|
344
|
+
if (typeof environment.runtime?.prompt === 'function') {
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const input = environment.runtime?.stdin ?? environment.io?.stdin ?? process.stdin;
|
|
349
|
+
return Boolean(input?.isTTY);
|
|
350
|
+
}
|
|
351
|
+
|
|
124
352
|
export function applyPlacementDefaults(options, environment = {}) {
|
|
125
353
|
const cwd = environment.cwd ? path.resolve(environment.cwd) : process.cwd();
|
|
126
354
|
const resolved = cloneInitOptions(options);
|
|
@@ -494,6 +722,16 @@ async function readGitInfo(cwd) {
|
|
|
494
722
|
}
|
|
495
723
|
}
|
|
496
724
|
|
|
725
|
+
async function readGitRoot(cwd) {
|
|
726
|
+
try {
|
|
727
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', '--show-toplevel'], { cwd });
|
|
728
|
+
const root = stdout.trim();
|
|
729
|
+
return root ? path.resolve(root) : null;
|
|
730
|
+
} catch {
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
497
735
|
async function discoverChildGitRepos(cwd) {
|
|
498
736
|
let entries = [];
|
|
499
737
|
try {
|
|
@@ -622,17 +860,7 @@ function writeAgentAdoptionPreview(prompter, candidates) {
|
|
|
622
860
|
}
|
|
623
861
|
|
|
624
862
|
function scanPotentialAgentWorkflowConflicts(content) {
|
|
625
|
-
const patterns =
|
|
626
|
-
'session',
|
|
627
|
-
'handoff',
|
|
628
|
-
'decisions',
|
|
629
|
-
'wip.md',
|
|
630
|
-
'review handoff',
|
|
631
|
-
'close-session',
|
|
632
|
-
'address review',
|
|
633
|
-
'builder',
|
|
634
|
-
'reviewer',
|
|
635
|
-
];
|
|
863
|
+
const patterns = getWorkflowConflictScanPatterns();
|
|
636
864
|
|
|
637
865
|
return content
|
|
638
866
|
.split(/\r?\n/)
|
|
@@ -1078,6 +1306,15 @@ async function directoryExists(directoryPath) {
|
|
|
1078
1306
|
}
|
|
1079
1307
|
}
|
|
1080
1308
|
|
|
1309
|
+
async function fileExists(filePath) {
|
|
1310
|
+
try {
|
|
1311
|
+
await access(filePath);
|
|
1312
|
+
return true;
|
|
1313
|
+
} catch {
|
|
1314
|
+
return false;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1081
1318
|
async function isSameDirectory(left, right) {
|
|
1082
1319
|
try {
|
|
1083
1320
|
const [leftRealPath, rightRealPath] = await Promise.all([
|
|
@@ -1104,3 +1341,8 @@ function toRelativeIfInsideCwd(targetPath, cwd) {
|
|
|
1104
1341
|
|
|
1105
1342
|
return relative;
|
|
1106
1343
|
}
|
|
1344
|
+
|
|
1345
|
+
function isPathInside(candidatePath, parentPath) {
|
|
1346
|
+
const relative = path.relative(path.resolve(parentPath), path.resolve(candidatePath));
|
|
1347
|
+
return Boolean(relative) && !relative.startsWith('..') && !path.isAbsolute(relative);
|
|
1348
|
+
}
|
package/src/status.js
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { inspectProjectCompatibility } from './compatibility.js';
|
|
4
|
+
import { parseSimpleYaml } from './simple-yaml.js';
|
|
5
|
+
import { listProjectSessions } from './session.js';
|
|
6
|
+
import { syncAgentInstructionFiles } from './generators/agent-files/index.js';
|
|
7
|
+
|
|
8
|
+
export async function getProjectStatus(options = {}) {
|
|
9
|
+
const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
|
|
10
|
+
const rootDir = path.resolve(cwd, options.rootDir ?? '.compass');
|
|
11
|
+
const toolingRootDir = options.toolingRootDir
|
|
12
|
+
? path.resolve(cwd, options.toolingRootDir)
|
|
13
|
+
: cwd;
|
|
14
|
+
|
|
15
|
+
const compatibility = await inspectProjectCompatibility({
|
|
16
|
+
rootDir,
|
|
17
|
+
cwd,
|
|
18
|
+
...(options.packageVersion ? { packageVersion: options.packageVersion } : {}),
|
|
19
|
+
});
|
|
20
|
+
const [project, sessions, docsReview, agentFiles] = await Promise.all([
|
|
21
|
+
readProjectSummary(compatibility.projectFilePath),
|
|
22
|
+
readSessionsStatus(rootDir),
|
|
23
|
+
readDocsReviewStatus(rootDir),
|
|
24
|
+
readAgentFileStatus(rootDir, toolingRootDir),
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
generatedAt: new Date().toISOString(),
|
|
29
|
+
rootDir,
|
|
30
|
+
toolingRootDir,
|
|
31
|
+
project,
|
|
32
|
+
compatibility: {
|
|
33
|
+
cliPackageVersion: compatibility.cliPackageVersion,
|
|
34
|
+
package: compatibility.package,
|
|
35
|
+
state: compatibility.state,
|
|
36
|
+
warnings: compatibility.warnings,
|
|
37
|
+
},
|
|
38
|
+
sessions,
|
|
39
|
+
docsReview,
|
|
40
|
+
agentFiles,
|
|
41
|
+
recommendations: buildRecommendations({
|
|
42
|
+
project,
|
|
43
|
+
compatibility,
|
|
44
|
+
sessions,
|
|
45
|
+
docsReview,
|
|
46
|
+
agentFiles,
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function renderStatusText(status) {
|
|
52
|
+
if (status.project.status === 'unreadable') {
|
|
53
|
+
return [
|
|
54
|
+
'VibeCompass status',
|
|
55
|
+
`Status: not initialized`,
|
|
56
|
+
`Generated: ${status.generatedAt}`,
|
|
57
|
+
`Root: ${status.rootDir}`,
|
|
58
|
+
`Message: Not a VibeCompass project memory root. Run \`vibecompass init --guided\` to set one up.`,
|
|
59
|
+
'',
|
|
60
|
+
'Recommended next commands:',
|
|
61
|
+
'- vibecompass init --guided',
|
|
62
|
+
'',
|
|
63
|
+
].join('\n');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const summary = hasStatusDrift(status) ? 'drift detected' : 'ok';
|
|
67
|
+
const lines = [
|
|
68
|
+
'VibeCompass status',
|
|
69
|
+
`Status: ${summary}`,
|
|
70
|
+
`Generated: ${status.generatedAt}`,
|
|
71
|
+
`Project: ${status.project.name ?? 'Unknown'}`,
|
|
72
|
+
`Mode: ${status.project.mode ?? 'Unknown'}`,
|
|
73
|
+
`Root: ${status.rootDir}`,
|
|
74
|
+
`Repos: ${status.project.repos.length > 0 ? status.project.repos.map((repo) => repo.id).join(', ') : 'None recorded'}`,
|
|
75
|
+
`Active lanes: ${formatActiveLanes(status.sessions)}`,
|
|
76
|
+
'',
|
|
77
|
+
'Package:',
|
|
78
|
+
`- CLI version: ${status.compatibility.cliPackageVersion}`,
|
|
79
|
+
`- Root stamp: ${formatPackageStatus(status.compatibility.package)}`,
|
|
80
|
+
...(status.compatibility.state.packageObservedVersion
|
|
81
|
+
? [`- Local observed: ${status.compatibility.state.packageObservedVersion} (from state/manifest.json)`]
|
|
82
|
+
: []),
|
|
83
|
+
`- State manifest: ${formatStateStatus(status.compatibility.state)}`,
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
if (status.compatibility.warnings.length > 0) {
|
|
87
|
+
lines.push('', 'Warnings:');
|
|
88
|
+
for (const warning of status.compatibility.warnings) {
|
|
89
|
+
lines.push(`- ${warning.message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
lines.push(
|
|
94
|
+
'',
|
|
95
|
+
'Docs review:',
|
|
96
|
+
`- ${formatDocsReviewStatus(status.docsReview)}`,
|
|
97
|
+
'',
|
|
98
|
+
'Agent files:',
|
|
99
|
+
...formatAgentFileStatus(status.agentFiles),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (status.recommendations.length > 0) {
|
|
103
|
+
lines.push('', 'Recommended next commands:');
|
|
104
|
+
for (const recommendation of status.recommendations) {
|
|
105
|
+
lines.push(`- ${recommendation}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return `${lines.join('\n')}\n`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function toStatusJson(status) {
|
|
113
|
+
// Keep this as an explicit public-output shape. Nested status fields must
|
|
114
|
+
// remain free of raw manifests, preview tokens, and resolved env values.
|
|
115
|
+
return {
|
|
116
|
+
generatedAt: status.generatedAt,
|
|
117
|
+
rootDir: status.rootDir,
|
|
118
|
+
toolingRootDir: status.toolingRootDir,
|
|
119
|
+
project: status.project,
|
|
120
|
+
compatibility: status.compatibility,
|
|
121
|
+
sessions: status.sessions,
|
|
122
|
+
docsReview: status.docsReview,
|
|
123
|
+
agentFiles: status.agentFiles,
|
|
124
|
+
recommendations: status.recommendations,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function readProjectSummary(projectFilePath) {
|
|
129
|
+
try {
|
|
130
|
+
const config = parseSimpleYaml(await readFile(projectFilePath, 'utf8'), {
|
|
131
|
+
sourceName: projectFilePath,
|
|
132
|
+
});
|
|
133
|
+
return {
|
|
134
|
+
status: 'ok',
|
|
135
|
+
name: typeof config.name === 'string' ? config.name : null,
|
|
136
|
+
mode: typeof config.mode === 'string' ? config.mode : null,
|
|
137
|
+
repos: Array.isArray(config.repos)
|
|
138
|
+
? config.repos
|
|
139
|
+
.filter((repo) => repo && typeof repo === 'object')
|
|
140
|
+
.map((repo) => ({
|
|
141
|
+
id: typeof repo.id === 'string' ? repo.id : null,
|
|
142
|
+
remote: typeof repo.remote === 'string' ? repo.remote : null,
|
|
143
|
+
defaultBranch: typeof repo.default_branch === 'string' ? repo.default_branch : null,
|
|
144
|
+
}))
|
|
145
|
+
.filter((repo) => repo.id)
|
|
146
|
+
: [],
|
|
147
|
+
sync: config.sync && typeof config.sync === 'object'
|
|
148
|
+
? {
|
|
149
|
+
provider: typeof config.sync.provider === 'string' ? config.sync.provider : null,
|
|
150
|
+
apiUrl: typeof config.sync.api_url === 'string' ? config.sync.api_url : null,
|
|
151
|
+
projectId: typeof config.sync.project_id === 'string' ? config.sync.project_id : null,
|
|
152
|
+
credentialSource: typeof config.sync.credential_source === 'string' ? config.sync.credential_source : null,
|
|
153
|
+
credentialEnvVar: typeof config.sync.credential_env_var === 'string' ? config.sync.credential_env_var : null,
|
|
154
|
+
}
|
|
155
|
+
: null,
|
|
156
|
+
};
|
|
157
|
+
} catch (error) {
|
|
158
|
+
return {
|
|
159
|
+
status: 'unreadable',
|
|
160
|
+
name: null,
|
|
161
|
+
mode: null,
|
|
162
|
+
repos: [],
|
|
163
|
+
sync: null,
|
|
164
|
+
errorMessage: error instanceof Error ? error.message : 'Unable to read project.yaml.',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function readSessionsStatus(rootDir) {
|
|
170
|
+
try {
|
|
171
|
+
const sessions = await listProjectSessions({ rootDir });
|
|
172
|
+
return {
|
|
173
|
+
status: 'ok',
|
|
174
|
+
current: sessions.current,
|
|
175
|
+
lanes: sessions.lanes.map((lane) => ({
|
|
176
|
+
id: lane.id,
|
|
177
|
+
status: lane.status,
|
|
178
|
+
workingOn: lane.workingOn,
|
|
179
|
+
})),
|
|
180
|
+
};
|
|
181
|
+
} catch (error) {
|
|
182
|
+
return {
|
|
183
|
+
status: 'unreadable',
|
|
184
|
+
current: null,
|
|
185
|
+
lanes: [],
|
|
186
|
+
errorMessage: error instanceof Error ? error.message : 'Unable to read active sessions.',
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function readDocsReviewStatus(rootDir) {
|
|
192
|
+
const markerPath = path.join(rootDir, 'state', 'docs-review.json');
|
|
193
|
+
try {
|
|
194
|
+
const marker = JSON.parse(await readFile(markerPath, 'utf8'));
|
|
195
|
+
return {
|
|
196
|
+
status: typeof marker.status === 'string' ? marker.status : 'unknown',
|
|
197
|
+
markerPath,
|
|
198
|
+
completedAt: typeof marker.completed_at === 'string' ? marker.completed_at : null,
|
|
199
|
+
provider: typeof marker.provider === 'string' ? marker.provider : null,
|
|
200
|
+
model: typeof marker.model === 'string' ? marker.model : null,
|
|
201
|
+
};
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
204
|
+
return {
|
|
205
|
+
status: 'missing',
|
|
206
|
+
markerPath,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
status: 'unreadable',
|
|
212
|
+
markerPath,
|
|
213
|
+
errorMessage: error instanceof Error ? error.message : 'Unable to read docs-review marker.',
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function readAgentFileStatus(rootDir, toolingRootDir) {
|
|
219
|
+
try {
|
|
220
|
+
const result = await syncAgentInstructionFiles({
|
|
221
|
+
rootDir,
|
|
222
|
+
toolingRootDir,
|
|
223
|
+
dryRun: true,
|
|
224
|
+
});
|
|
225
|
+
return {
|
|
226
|
+
status: 'ok',
|
|
227
|
+
dryRun: true,
|
|
228
|
+
results: result.results.map((item) => ({
|
|
229
|
+
format: item.format,
|
|
230
|
+
relativePath: item.relativePath,
|
|
231
|
+
status: item.status,
|
|
232
|
+
warning: item.warning,
|
|
233
|
+
changed: item.changed,
|
|
234
|
+
conflicts: item.conflicts ?? [],
|
|
235
|
+
})),
|
|
236
|
+
};
|
|
237
|
+
} catch (error) {
|
|
238
|
+
return {
|
|
239
|
+
status: 'unavailable',
|
|
240
|
+
dryRun: true,
|
|
241
|
+
results: [],
|
|
242
|
+
errorMessage: error instanceof Error ? error.message : 'Unable to inspect agent files.',
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function buildRecommendations({ project, compatibility, sessions, docsReview, agentFiles }) {
|
|
248
|
+
const recommendations = [];
|
|
249
|
+
if (project.status === 'unreadable') {
|
|
250
|
+
recommendations.push('vibecompass init --guided');
|
|
251
|
+
return recommendations;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const packageStatus = compatibility.package.status;
|
|
255
|
+
const stateStatus = compatibility.state.status;
|
|
256
|
+
if (['legacy', 'behind', 'ahead', 'invalid'].includes(packageStatus) || ['missing', 'unreadable', 'unsupported'].includes(stateStatus)) {
|
|
257
|
+
recommendations.push('vibecompass refresh-workflow --dry-run');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (docsReview.status === 'missing') {
|
|
261
|
+
recommendations.push('vibecompass docs-review --guided');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (agentFiles.status === 'ok' && agentFiles.results.some((item) => item.changed || item.warning)) {
|
|
265
|
+
recommendations.push('vibecompass sync-agents --dry-run');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (sessions.status === 'ok' && sessions.lanes.length === 0) {
|
|
269
|
+
recommendations.push('vibecompass start-session --id LANE_ID --working-on "TASK"');
|
|
270
|
+
} else if (sessions.status === 'ok' && sessions.lanes.length >= 2) {
|
|
271
|
+
recommendations.push('vibecompass list-sessions');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return [...new Set(recommendations)];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function hasStatusDrift(status) {
|
|
278
|
+
return (
|
|
279
|
+
status.compatibility.warnings.length > 0 ||
|
|
280
|
+
status.docsReview.status !== 'completed' ||
|
|
281
|
+
status.agentFiles.status !== 'ok' ||
|
|
282
|
+
status.agentFiles.results.some((item) => item.changed || item.warning)
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function formatActiveLanes(sessions) {
|
|
287
|
+
if (sessions.status !== 'ok') {
|
|
288
|
+
return `unavailable (${sessions.errorMessage})`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (sessions.lanes.length === 0) {
|
|
292
|
+
return 'none';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return `${sessions.lanes.length}${sessions.current ? ` (current: ${sessions.current})` : ''}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function formatPackageStatus(packageStatus) {
|
|
299
|
+
if (packageStatus.status === 'legacy') {
|
|
300
|
+
return 'legacy/unknown';
|
|
301
|
+
}
|
|
302
|
+
if (packageStatus.status === 'unknown') {
|
|
303
|
+
return `unknown (${packageStatus.errorMessage})`;
|
|
304
|
+
}
|
|
305
|
+
if (packageStatus.status === 'invalid') {
|
|
306
|
+
return `invalid (${String(packageStatus.rootVersion)})`;
|
|
307
|
+
}
|
|
308
|
+
return `${packageStatus.rootVersion ?? 'none'} (${packageStatus.status})`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function formatStateStatus(stateStatus) {
|
|
312
|
+
if (stateStatus.status === 'current') {
|
|
313
|
+
return `state_version ${stateStatus.observedVersion} (current)`;
|
|
314
|
+
}
|
|
315
|
+
if (stateStatus.status === 'unsupported') {
|
|
316
|
+
return `state_version ${stateStatus.observedVersion ?? 'missing'} (unsupported; expected ${stateStatus.expectedVersion})`;
|
|
317
|
+
}
|
|
318
|
+
if (stateStatus.status === 'unreadable') {
|
|
319
|
+
return `unreadable (${stateStatus.errorMessage})`;
|
|
320
|
+
}
|
|
321
|
+
if (stateStatus.status === 'missing') {
|
|
322
|
+
return 'no manifest yet (will be created on first command that writes state)';
|
|
323
|
+
}
|
|
324
|
+
return stateStatus.status;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function formatDocsReviewStatus(docsReview) {
|
|
328
|
+
if (docsReview.status === 'missing') {
|
|
329
|
+
return 'No docs-review marker found';
|
|
330
|
+
}
|
|
331
|
+
if (docsReview.status === 'unreadable') {
|
|
332
|
+
return `Marker unreadable (${docsReview.errorMessage})`;
|
|
333
|
+
}
|
|
334
|
+
return `${docsReview.status}${docsReview.completedAt ? ` at ${docsReview.completedAt}` : ''}`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function formatAgentFileStatus(agentFiles) {
|
|
338
|
+
if (agentFiles.status !== 'ok') {
|
|
339
|
+
return [`- unavailable (${agentFiles.errorMessage})`];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (agentFiles.results.length === 0) {
|
|
343
|
+
return ['- none configured'];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return agentFiles.results.map((item) => {
|
|
347
|
+
const warning = item.warning ? ` — ${item.warning}` : '';
|
|
348
|
+
return `- ${item.relativePath}: ${item.status}${warning}`;
|
|
349
|
+
});
|
|
350
|
+
}
|