@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/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
+ }
package/src/version.js ADDED
@@ -0,0 +1,6 @@
1
+ import { createRequire } from 'node:module';
2
+
3
+ const require = createRequire(import.meta.url);
4
+ const packageJson = require('../package.json');
5
+
6
+ export const PACKAGE_VERSION = packageJson.version;