@vibecompass/vibecompass 0.2.1 → 0.3.1
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 +202 -19
- package/package.json +1 -1
- package/src/cli.js +548 -3
- package/src/docs-review.js +1220 -0
- package/src/generators/agent-files/template.js +2 -2
- package/src/index.js +7 -0
- package/src/init.js +67 -25
- package/src/manifest.js +52 -2
- package/src/project-memory.js +22 -3
- package/src/read-model.js +1 -1
- package/src/scaffold.js +218 -12
- package/src/session.js +180 -6
- package/src/setup.js +149 -34
- package/src/sync.js +395 -0
|
@@ -35,14 +35,14 @@ export function renderSharedInstructionBody(context, options = {}) {
|
|
|
35
35
|
'',
|
|
36
36
|
'## Workflow Rules',
|
|
37
37
|
'- VibeCompass active builder sessions are named lanes. Use one lane per active feature or workstream; finalized session notes are append-only history under `sessions/YYYY-MM-DD-N-title.md`.',
|
|
38
|
-
'- Use `start session` for builder work
|
|
38
|
+
'- Use `start session` for builder work, `join as reviewer` for review work, and `close session` / `end session` when the builder is ready to close the selected lane.',
|
|
39
39
|
'- Use `vibecompass start-session --id <lane-id>` to open a named lane for every active feature or workstream.',
|
|
40
40
|
'- Use `vibecompass list-sessions` and `vibecompass switch-session <lane-id>` to inspect or change the current lane.',
|
|
41
41
|
'- Treat `sessions/active/index.yaml` as the current lane source of truth; tool-specific Current session blocks are continuity summaries.',
|
|
42
42
|
'- 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.',
|
|
43
43
|
'- Use `review handoff` to ask a reviewer to inspect the active handoff.',
|
|
44
44
|
'- Use `address review` to process reviewer feedback from the selected lane `wip.md` and `handoff.md`.',
|
|
45
|
-
'- End the active builder session with `vibecompass close-session --session <lane-id>`; `vibecompass end-session`
|
|
45
|
+
'- 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.',
|
|
46
46
|
'- If stale scratch files block `start-session`, read them first, then close, recover, move, or delete them intentionally before retrying.',
|
|
47
47
|
'- Keep decisions append-only in `decisions/`. Session notes may reference decisions, but decisions are the durable source of truth.',
|
|
48
48
|
'- Treat generated agent files as views. Update canonical VibeCompass project memory instead of editing managed regions.',
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
export { STATE_VERSION, generateStateManifest, writeStateManifest } from './manifest.js';
|
|
2
2
|
export { initializeProjectMemory } from './init.js';
|
|
3
|
+
export { preflightDocsReview } from './docs-review.js';
|
|
4
|
+
export {
|
|
5
|
+
applyPullExport,
|
|
6
|
+
pullExportProjectMemory,
|
|
7
|
+
pullPreviewProjectMemory,
|
|
8
|
+
pushProjectMemory,
|
|
9
|
+
} from './sync.js';
|
|
3
10
|
export { scanProjectMemory } from './project-memory.js';
|
|
4
11
|
export {
|
|
5
12
|
closeProjectSession,
|
package/src/init.js
CHANGED
|
@@ -5,6 +5,7 @@ import { writeStateManifest } from './manifest.js';
|
|
|
5
5
|
import { scaffoldInitFiles } from './scaffold.js';
|
|
6
6
|
import { applyPlacementDefaults } from './setup.js';
|
|
7
7
|
import { buildWorkflowMetadata } from './workflow.js';
|
|
8
|
+
import { parseSimpleYaml } from './simple-yaml.js';
|
|
8
9
|
|
|
9
10
|
const VALID_MODES = new Set(['local-only', 'local-primary', 'hosted-only']);
|
|
10
11
|
|
|
@@ -21,10 +22,10 @@ export async function initializeProjectMemory(options) {
|
|
|
21
22
|
|
|
22
23
|
await writeProjectFile(projectFilePath, normalized.projectConfig, normalized.force);
|
|
23
24
|
const gitignoreUpdated = await ensureStateIgnored(gitignorePath);
|
|
25
|
+
const scaffoldResult = await scaffoldInitFiles(normalized.bootstrap);
|
|
24
26
|
const manifestResult = await writeStateManifest(normalized.rootDir, {
|
|
25
27
|
generatedAt: normalized.generatedAt,
|
|
26
28
|
});
|
|
27
|
-
const scaffoldResult = await scaffoldInitFiles(normalized.bootstrap);
|
|
28
29
|
|
|
29
30
|
return {
|
|
30
31
|
rootDir: normalized.rootDir,
|
|
@@ -41,6 +42,39 @@ export async function initializeProjectMemory(options) {
|
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
export async function connectHostedProjectMemory(options) {
|
|
46
|
+
const cwd = options?.cwd ? path.resolve(options.cwd) : process.cwd();
|
|
47
|
+
const rootDir = path.resolve(cwd, options?.rootDir ?? '.compass');
|
|
48
|
+
const projectFilePath = path.join(rootDir, 'project.yaml');
|
|
49
|
+
let projectConfig;
|
|
50
|
+
try {
|
|
51
|
+
projectConfig = parseSimpleYaml(await readFile(projectFilePath, 'utf8'), {
|
|
52
|
+
sourceName: projectFilePath,
|
|
53
|
+
});
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
56
|
+
throw new Error(`No project.yaml found in ${rootDir}. Run "vibecompass init" first.`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!['local-primary', 'hosted-only'].includes(projectConfig.mode)) {
|
|
63
|
+
throw new Error('connect-hosted requires project.yaml mode to be local-primary or hosted-only.');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const sync = normalizeSyncOptions(options?.sync, projectConfig.mode);
|
|
67
|
+
projectConfig.sync = sync;
|
|
68
|
+
await writeFile(projectFilePath, serializeProjectConfig(projectConfig), 'utf8');
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
rootDir,
|
|
72
|
+
projectFilePath,
|
|
73
|
+
mode: projectConfig.mode,
|
|
74
|
+
syncEnvVar: sync.credential_env_var,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
44
78
|
function normalizeInitOptions(options) {
|
|
45
79
|
if (!options || typeof options !== 'object') {
|
|
46
80
|
throw new Error('initializeProjectMemory requires an options object.');
|
|
@@ -91,30 +125,9 @@ function normalizeInitOptions(options) {
|
|
|
91
125
|
};
|
|
92
126
|
});
|
|
93
127
|
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (preparedOptions.mode !== 'local-primary') {
|
|
98
|
-
throw new Error('sync configuration is only supported when mode is local-primary.');
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const requiredFields = ['apiUrl', 'projectId', 'credentialEnvVar'];
|
|
102
|
-
const missing = requiredFields.filter((field) => typeof syncOptions[field] !== 'string' || syncOptions[field].trim() === '');
|
|
103
|
-
|
|
104
|
-
if (missing.length > 0) {
|
|
105
|
-
throw new Error(
|
|
106
|
-
`sync configuration is incomplete. Missing: ${missing.join(', ')}.`,
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
sync = {
|
|
111
|
-
provider: 'vibecompass',
|
|
112
|
-
api_url: syncOptions.apiUrl,
|
|
113
|
-
project_id: syncOptions.projectId,
|
|
114
|
-
credential_source: 'env',
|
|
115
|
-
credential_env_var: syncOptions.credentialEnvVar,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
128
|
+
const sync = preparedOptions.sync
|
|
129
|
+
? normalizeSyncOptions(preparedOptions.sync, preparedOptions.mode)
|
|
130
|
+
: undefined;
|
|
118
131
|
|
|
119
132
|
const projectConfig = {
|
|
120
133
|
format_version: 1,
|
|
@@ -137,6 +150,7 @@ function normalizeInitOptions(options) {
|
|
|
137
150
|
rootDir,
|
|
138
151
|
toolingRootDir,
|
|
139
152
|
projectConfig,
|
|
153
|
+
generatedAt: preparedOptions.generatedAt,
|
|
140
154
|
});
|
|
141
155
|
|
|
142
156
|
return {
|
|
@@ -148,6 +162,33 @@ function normalizeInitOptions(options) {
|
|
|
148
162
|
};
|
|
149
163
|
}
|
|
150
164
|
|
|
165
|
+
function normalizeSyncOptions(syncOptions, mode) {
|
|
166
|
+
if (!syncOptions || typeof syncOptions !== 'object') {
|
|
167
|
+
throw new Error('sync configuration must be an object.');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!['local-primary', 'hosted-only'].includes(mode)) {
|
|
171
|
+
throw new Error('sync configuration is only supported when mode is local-primary or hosted-only.');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const requiredFields = ['apiUrl', 'projectId', 'credentialEnvVar'];
|
|
175
|
+
const missing = requiredFields.filter((field) => typeof syncOptions[field] !== 'string' || syncOptions[field].trim() === '');
|
|
176
|
+
|
|
177
|
+
if (missing.length > 0) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`sync configuration is incomplete. Missing: ${missing.join(', ')}.`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
provider: 'vibecompass',
|
|
185
|
+
api_url: syncOptions.apiUrl,
|
|
186
|
+
project_id: syncOptions.projectId,
|
|
187
|
+
credential_source: 'env',
|
|
188
|
+
credential_env_var: syncOptions.credentialEnvVar,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
151
192
|
function buildAgentFilesMetadata(metadata) {
|
|
152
193
|
const existingAgentFiles = metadata?.agent_files && typeof metadata.agent_files === 'object'
|
|
153
194
|
? metadata.agent_files
|
|
@@ -182,6 +223,7 @@ function normalizeBootstrapOptions(bootstrap, context) {
|
|
|
182
223
|
contextRelativeToToolingRoot: toPosix(
|
|
183
224
|
path.relative(context.toolingRootDir, contextFilePath) || 'context.md',
|
|
184
225
|
),
|
|
226
|
+
generatedAt: context.generatedAt,
|
|
185
227
|
};
|
|
186
228
|
}
|
|
187
229
|
|
package/src/manifest.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { localRevisionFromManifestHash, stableHash } from './hash.js';
|
|
4
4
|
import { scanProjectMemory } from './project-memory.js';
|
|
5
|
+
import { listProjectSessions } from './session.js';
|
|
5
6
|
|
|
6
7
|
export const STATE_VERSION = 1;
|
|
7
8
|
|
|
@@ -50,13 +51,22 @@ export function generateStateManifest(scanResult, options = {}) {
|
|
|
50
51
|
warning_count: warningCount,
|
|
51
52
|
},
|
|
52
53
|
documents: manifestDocuments,
|
|
54
|
+
...(options.activeSessions ? { active_sessions: options.activeSessions } : {}),
|
|
53
55
|
...(options.sync ? { sync: options.sync } : {}),
|
|
54
56
|
};
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
export async function writeStateManifest(rootDir, options = {}) {
|
|
58
60
|
const scanResult = await scanProjectMemory(rootDir);
|
|
59
|
-
const
|
|
61
|
+
const activeSessions = await readActiveSessionsForManifest(rootDir);
|
|
62
|
+
const existingSync = Object.hasOwn(options, 'sync')
|
|
63
|
+
? options.sync
|
|
64
|
+
: await readExistingSyncState(rootDir);
|
|
65
|
+
const manifest = generateStateManifest(scanResult, {
|
|
66
|
+
...options,
|
|
67
|
+
...(existingSync ? { sync: existingSync } : {}),
|
|
68
|
+
...(activeSessions ? { activeSessions } : {}),
|
|
69
|
+
});
|
|
60
70
|
const stateDir = path.join(rootDir, 'state');
|
|
61
71
|
const manifestPath = path.join(stateDir, 'manifest.json');
|
|
62
72
|
|
|
@@ -70,6 +80,46 @@ export async function writeStateManifest(rootDir, options = {}) {
|
|
|
70
80
|
};
|
|
71
81
|
}
|
|
72
82
|
|
|
83
|
+
async function readExistingSyncState(rootDir) {
|
|
84
|
+
try {
|
|
85
|
+
const existing = JSON.parse(
|
|
86
|
+
await readFile(path.join(rootDir, 'state', 'manifest.json'), 'utf8'),
|
|
87
|
+
);
|
|
88
|
+
return existing?.sync && typeof existing.sync === 'object'
|
|
89
|
+
? existing.sync
|
|
90
|
+
: null;
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function readActiveSessionsForManifest(rootDir) {
|
|
97
|
+
const sessions = await listProjectSessions({ rootDir });
|
|
98
|
+
if (sessions.lanes.length === 0 && !sessions.current) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
current: sessions.current,
|
|
104
|
+
lanes: sessions.lanes.map((lane) => ({
|
|
105
|
+
id: lane.id,
|
|
106
|
+
status: lane.status ?? 'active',
|
|
107
|
+
working_on: lane.workingOn,
|
|
108
|
+
feature_slugs: lane.features ?? [],
|
|
109
|
+
repos: lane.repos ?? [],
|
|
110
|
+
claimed_paths: lane.claims ?? [],
|
|
111
|
+
started_at: lane.startedAt ?? null,
|
|
112
|
+
...(lane.decisionSnapshot
|
|
113
|
+
? {
|
|
114
|
+
decision_snapshot: {
|
|
115
|
+
highest_decision_id: lane.decisionSnapshot.highestDecisionId ?? null,
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
: {}),
|
|
119
|
+
})),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
73
123
|
function toIsoString(value) {
|
|
74
124
|
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
|
75
125
|
}
|
package/src/project-memory.js
CHANGED
|
@@ -5,7 +5,7 @@ import { sha256Text } from './hash.js';
|
|
|
5
5
|
import { parseSimpleYaml } from './simple-yaml.js';
|
|
6
6
|
|
|
7
7
|
const PROJECT_FILE = 'project.yaml';
|
|
8
|
-
const NON_CANONICAL_DECISION_FILES = new Set(['INDEX.md', 'README.md']);
|
|
8
|
+
const NON_CANONICAL_DECISION_FILES = new Set(['EXAMPLE.md', 'INDEX.md', 'README.md']);
|
|
9
9
|
const NON_CANONICAL_SESSION_FILES = new Set(['wip.md', 'handoff.md', 'README.md']);
|
|
10
10
|
const NON_CANONICAL_ARCHITECTURE_FILES = new Set(['README.md']);
|
|
11
11
|
const VALID_MODES = new Set(['local-only', 'local-primary', 'hosted-only']);
|
|
@@ -468,8 +468,23 @@ function parseDecisionDocument(options) {
|
|
|
468
468
|
const warnings = [];
|
|
469
469
|
const errors = [];
|
|
470
470
|
const entries = extractDecisionEntries(options.content);
|
|
471
|
+
const malformedHeadings = extractMalformedDecisionHeadings(options.content);
|
|
472
|
+
|
|
473
|
+
for (const heading of malformedHeadings) {
|
|
474
|
+
errors.push(
|
|
475
|
+
createError(
|
|
476
|
+
options.relativePath,
|
|
477
|
+
'decision-invalid-id',
|
|
478
|
+
`Decision heading "${heading}" must use zero-padded D-NNN or higher.`,
|
|
479
|
+
),
|
|
480
|
+
);
|
|
481
|
+
}
|
|
471
482
|
|
|
472
483
|
if (entries.length === 0) {
|
|
484
|
+
if (malformedHeadings.length > 0) {
|
|
485
|
+
return { kind: 'decision', extracted: null, warnings, errors };
|
|
486
|
+
}
|
|
487
|
+
|
|
473
488
|
errors.push(
|
|
474
489
|
createError(options.relativePath, 'decision-missing-entries', 'Decision files must contain at least one resolvable decision entry.'),
|
|
475
490
|
);
|
|
@@ -479,7 +494,7 @@ function parseDecisionDocument(options) {
|
|
|
479
494
|
const decisionIds = [];
|
|
480
495
|
for (const entry of entries) {
|
|
481
496
|
if (!entry.decisionId || entry.decisionId <= 0) {
|
|
482
|
-
errors.push(createError(options.relativePath, 'decision-invalid-id', 'Decision heading must include D-<
|
|
497
|
+
errors.push(createError(options.relativePath, 'decision-invalid-id', 'Decision heading must include zero-padded D-<NNN>.'));
|
|
483
498
|
continue;
|
|
484
499
|
}
|
|
485
500
|
|
|
@@ -576,7 +591,7 @@ function parseSessionDocument(options) {
|
|
|
576
591
|
}
|
|
577
592
|
|
|
578
593
|
function extractDecisionEntries(content) {
|
|
579
|
-
const matches = [...content.matchAll(/^###\s+D-(\d
|
|
594
|
+
const matches = [...content.matchAll(/^###\s+D-(\d{3,})\b.*$/gm)];
|
|
580
595
|
|
|
581
596
|
return matches.map((match, index) => {
|
|
582
597
|
const bodyStart = match.index ?? 0;
|
|
@@ -590,6 +605,10 @@ function extractDecisionEntries(content) {
|
|
|
590
605
|
});
|
|
591
606
|
}
|
|
592
607
|
|
|
608
|
+
function extractMalformedDecisionHeadings(content) {
|
|
609
|
+
return [...content.matchAll(/^###\s+(D-\d{1,2})\b.*$/gm)].map((match) => match[1]);
|
|
610
|
+
}
|
|
611
|
+
|
|
593
612
|
function findDuplicateDecisionErrors(documents) {
|
|
594
613
|
const seen = new Map();
|
|
595
614
|
const errors = [];
|
package/src/read-model.js
CHANGED
|
@@ -5,7 +5,7 @@ import { generateStateManifest } from './manifest.js';
|
|
|
5
5
|
import { scanProjectMemory } from './project-memory.js';
|
|
6
6
|
|
|
7
7
|
const SECTION_PATTERN = /^##\s+(.+)$/gm;
|
|
8
|
-
const DECISION_HEADING_PATTERN = /^###\s+D-(\d
|
|
8
|
+
const DECISION_HEADING_PATTERN = /^###\s+D-(\d{3,})\s+—\s+(.+)$/gm;
|
|
9
9
|
|
|
10
10
|
export async function loadProjectReadModel(rootDir) {
|
|
11
11
|
const scanResult = await scanProjectMemory(rootDir);
|
package/src/scaffold.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { access, mkdir, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { access, mkdir, readdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { resolveWorkflowSettings } from './workflow.js';
|
|
4
4
|
|
|
@@ -6,6 +6,45 @@ export async function scaffoldInitFiles(options) {
|
|
|
6
6
|
const createdFiles = [];
|
|
7
7
|
const skippedFiles = [];
|
|
8
8
|
|
|
9
|
+
await mkdir(path.join(options.rootDir, 'architecture'), { recursive: true });
|
|
10
|
+
await mkdir(path.join(options.rootDir, 'decisions'), { recursive: true });
|
|
11
|
+
await mkdir(path.join(options.rootDir, 'sessions'), { recursive: true });
|
|
12
|
+
|
|
13
|
+
const starterFiles = [
|
|
14
|
+
{
|
|
15
|
+
path: path.join(options.rootDir, 'decisions', 'EXAMPLE.md'),
|
|
16
|
+
content: generateDecisionExample(options.projectConfig),
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
if (!(await hasCanonicalMarkdown(path.join(options.rootDir, 'architecture'), {
|
|
21
|
+
ignoredNames: new Set(['README.md']),
|
|
22
|
+
}))) {
|
|
23
|
+
starterFiles.push({
|
|
24
|
+
path: path.join(options.rootDir, 'architecture', 'overview', 'project-shape.md'),
|
|
25
|
+
content: generateStarterArchitectureDoc(options.projectConfig),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!(await hasCanonicalMarkdown(path.join(options.rootDir, 'sessions'), {
|
|
30
|
+
ignoredNames: new Set(['README.md', 'wip.md', 'handoff.md']),
|
|
31
|
+
ignoredDirectories: new Set(['active']),
|
|
32
|
+
}))) {
|
|
33
|
+
starterFiles.push({
|
|
34
|
+
path: path.join(options.rootDir, 'sessions', `${sessionDateFromGeneratedAt(options.generatedAt)}-1-project-memory-initialized.md`),
|
|
35
|
+
content: generateStarterSessionNote(options),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const file of starterFiles) {
|
|
40
|
+
const outcome = await writeIfMissing(file.path, file.content);
|
|
41
|
+
if (outcome.created) {
|
|
42
|
+
createdFiles.push(file.path);
|
|
43
|
+
} else {
|
|
44
|
+
skippedFiles.push(file.path);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
9
48
|
if (!options.workflow) {
|
|
10
49
|
return {
|
|
11
50
|
contextFilePath: null,
|
|
@@ -14,10 +53,6 @@ export async function scaffoldInitFiles(options) {
|
|
|
14
53
|
};
|
|
15
54
|
}
|
|
16
55
|
|
|
17
|
-
await mkdir(path.join(options.rootDir, 'architecture'), { recursive: true });
|
|
18
|
-
await mkdir(path.join(options.rootDir, 'decisions'), { recursive: true });
|
|
19
|
-
await mkdir(path.join(options.rootDir, 'sessions'), { recursive: true });
|
|
20
|
-
|
|
21
56
|
const contextFilePath = path.join(options.rootDir, 'context.md');
|
|
22
57
|
// context.md is a derived artifact owned by the package; regenerate it on rerun.
|
|
23
58
|
await writeFile(contextFilePath, generateContextMarkdown(options), 'utf8');
|
|
@@ -97,6 +132,43 @@ async function fileExists(filePath) {
|
|
|
97
132
|
}
|
|
98
133
|
}
|
|
99
134
|
|
|
135
|
+
async function hasCanonicalMarkdown(directoryPath, options = {}) {
|
|
136
|
+
const ignoredNames = options.ignoredNames ?? new Set();
|
|
137
|
+
const ignoredDirectories = options.ignoredDirectories ?? new Set();
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const entries = await readdir(directoryPath, { withFileTypes: true });
|
|
141
|
+
for (const entry of entries) {
|
|
142
|
+
if (entry.name.startsWith('.')) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (entry.isDirectory()) {
|
|
147
|
+
if (ignoredDirectories.has(entry.name)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (await hasCanonicalMarkdown(path.join(directoryPath, entry.name), options)) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (entry.isFile() && entry.name.endsWith('.md') && !ignoredNames.has(entry.name)) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
100
172
|
function generateContextMarkdown(options) {
|
|
101
173
|
const projectName = options.projectConfig.name;
|
|
102
174
|
const rootRelativePath = options.rootRelativePath;
|
|
@@ -135,7 +207,7 @@ This workspace uses VibeCompass project memory rooted at \`${rootRelativePath}\`
|
|
|
135
207
|
${repos}
|
|
136
208
|
|
|
137
209
|
## Session roles
|
|
138
|
-
|
|
|
210
|
+
| Entry trigger | Role | Owns session lifecycle? |
|
|
139
211
|
|---|---|---|
|
|
140
212
|
| "start session" | Builder | Yes — opens the session, keeps scratch files current, and writes the final session note |
|
|
141
213
|
| "join as reviewer" | Reviewer | No — reviews the builder's work, appends findings, and updates handoff guidance |
|
|
@@ -146,14 +218,20 @@ ${repos}
|
|
|
146
218
|
- \`planning mode\` — optional prompt-level mode for scoping work before implementation
|
|
147
219
|
- \`review handoff\` — reviewer reads the selected lane's \`wip.md\`, \`handoff.md\`, latest finalized note, and relevant diffs before appending findings
|
|
148
220
|
- \`address review\` — builder reads reviewer feedback in the selected lane's \`wip.md\` / \`handoff.md\`, responds inline, applies accepted changes, and refreshes the builder handoff
|
|
221
|
+
- \`close session\` — builder runs the close-out checklist and ends with \`vibecompass close-session --session <lane-id>\`; \`end session\` is accepted as a synonym
|
|
149
222
|
|
|
150
223
|
These are prompt commands for agent behavior, not \`vibecompass\` CLI subcommands.
|
|
151
|
-
Reviewer handback is explicit: the reviewer ends the pass by updating the selected lane's \`wip.md\` + \`handoff.md\` and then stopping.
|
|
224
|
+
Reviewer handback is explicit: the reviewer ends the pass by updating the selected lane's \`wip.md\` + \`handoff.md\` and then stopping. The \`close session\` prompt runs builder close-out and ends with \`vibecompass close-session --session <lane-id>\`, which follows the workflow defaults recorded in \`${rootRelativePath}/project.yaml\`.
|
|
152
225
|
\`vibecompass end-session\` is also accepted as an alias for \`vibecompass close-session\`; the canonical command name remains \`close-session\`.
|
|
153
226
|
|
|
154
227
|
## Workflow defaults
|
|
155
228
|
${renderWorkflowDefaults(workflow)}
|
|
156
229
|
|
|
230
|
+
## Documentation coverage
|
|
231
|
+
- The architecture, decision, and session files created by init are an initial scaffold, not a comprehensive codebase review.
|
|
232
|
+
- Before risky implementation work, inspect the relevant code and update the affected architecture docs.
|
|
233
|
+
- Run \`vibecompass docs-review --guided\` when you want a comprehensive documentation review. That command is explicit because it may inspect source files and use AI or hosted scan infrastructure.
|
|
234
|
+
|
|
157
235
|
## Session startup
|
|
158
236
|
1. Read \`${rootRelativePath}/project.yaml\`.
|
|
159
237
|
2. Read the latest finalized session note in \`${rootRelativePath}/sessions/\`.
|
|
@@ -240,6 +318,119 @@ automatically by package commands.
|
|
|
240
318
|
`;
|
|
241
319
|
}
|
|
242
320
|
|
|
321
|
+
function generateStarterArchitectureDoc(projectConfig) {
|
|
322
|
+
const repos = projectConfig.repos.map((repo) => repo.id);
|
|
323
|
+
const repoLines = projectConfig.repos
|
|
324
|
+
.map((repo) => `- \`${repo.id}\` — ${repo.remote}${repo.default_branch ? ` (${repo.default_branch})` : ''}`)
|
|
325
|
+
.join('\n');
|
|
326
|
+
const repoEvidence = projectConfig.repos
|
|
327
|
+
.map((repo) => `- \`${repo.id}:/\` — declared repository root`)
|
|
328
|
+
.join('\n');
|
|
329
|
+
|
|
330
|
+
return `---
|
|
331
|
+
domain: Project
|
|
332
|
+
feature: Overview
|
|
333
|
+
component: Project Shape
|
|
334
|
+
status: In progress
|
|
335
|
+
repos:
|
|
336
|
+
${repos.map((repoId) => ` - ${quoteYamlString(repoId)}`).join('\n')}
|
|
337
|
+
confidence: Initial scaffold
|
|
338
|
+
coverage: Initial scaffold
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Description
|
|
342
|
+
Initial project-memory scaffold for ${projectConfig.name}.
|
|
343
|
+
|
|
344
|
+
${projectConfig.description ? `${projectConfig.description}\n` : ''}This document is a starting map, not a comprehensive architecture review.
|
|
345
|
+
|
|
346
|
+
## Details
|
|
347
|
+
- Mode: \`${projectConfig.mode}\`
|
|
348
|
+
- Documentation coverage: initial scaffold
|
|
349
|
+
- Repo inventory:
|
|
350
|
+
${repoLines}
|
|
351
|
+
|
|
352
|
+
## Coverage
|
|
353
|
+
Confirmed:
|
|
354
|
+
- Project identity and mode from \`project.yaml\`
|
|
355
|
+
- Declared repository IDs and remotes from \`project.yaml\`
|
|
356
|
+
- Project-memory root layout created by \`vibecompass init\`
|
|
357
|
+
|
|
358
|
+
Not yet documented:
|
|
359
|
+
- Runtime architecture and data flow
|
|
360
|
+
- Auth/session model
|
|
361
|
+
- Database or persistence ownership
|
|
362
|
+
- External integrations
|
|
363
|
+
- Deployment, jobs, and observability
|
|
364
|
+
- Test strategy
|
|
365
|
+
- Feature/domain/component map beyond this placeholder
|
|
366
|
+
|
|
367
|
+
Before changing any undocumented area, inspect the relevant code and update or add architecture docs.
|
|
368
|
+
|
|
369
|
+
## Next steps
|
|
370
|
+
- Run \`vibecompass docs-review --guided\` for a comprehensive documentation review.
|
|
371
|
+
- Replace this placeholder with domain/feature/component docs as the project map becomes evidence-backed.
|
|
372
|
+
- Keep architecture docs aligned with implementation changes during each builder session.
|
|
373
|
+
|
|
374
|
+
## Involved files
|
|
375
|
+
- \`project.yaml\`
|
|
376
|
+
${repoEvidence}
|
|
377
|
+
`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function generateDecisionExample(projectConfig) {
|
|
381
|
+
return `# Decision Examples
|
|
382
|
+
|
|
383
|
+
This file is example-only guidance for ${projectConfig.name}. It is intentionally ignored by VibeCompass canonical decision parsing and must not be treated as a real decision log.
|
|
384
|
+
|
|
385
|
+
Real decisions belong in domain-grouped files such as \`cross-cutting.md\`, with append-only entries:
|
|
386
|
+
|
|
387
|
+
\`\`\`md
|
|
388
|
+
### D-001 — Example decision title
|
|
389
|
+
**Timestamp:** YYYY-MM-DD HH:MM TZ
|
|
390
|
+
**Decision:** What was decided.
|
|
391
|
+
**Rationale:** Why this was the correct tradeoff.
|
|
392
|
+
\`\`\`
|
|
393
|
+
|
|
394
|
+
Do not copy this example as-is. Allocate real decision IDs from the current canonical decision log when a decision is accepted.
|
|
395
|
+
`;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function generateStarterSessionNote(options) {
|
|
399
|
+
const sessionDate = sessionDateFromGeneratedAt(options.generatedAt);
|
|
400
|
+
const projectConfig = options.projectConfig;
|
|
401
|
+
const repoLines = projectConfig.repos.map((repo) => `- \`${repo.id}\` — ${repo.remote}`).join('\n');
|
|
402
|
+
|
|
403
|
+
return `# Session — ${sessionDate}-1 — Project Memory Initialized
|
|
404
|
+
|
|
405
|
+
Generated by \`vibecompass init\`; this is initialization history, not a builder/reviewer work session.
|
|
406
|
+
|
|
407
|
+
## What we worked on
|
|
408
|
+
Initialized VibeCompass project memory for ${projectConfig.name}.
|
|
409
|
+
|
|
410
|
+
## Completed
|
|
411
|
+
- Created the project-memory root and \`project.yaml\`.
|
|
412
|
+
- Created lightweight starter architecture memory.
|
|
413
|
+
- Created example-only decision guidance without seeding real decisions.
|
|
414
|
+
- Generated local state manifest data from canonical files.
|
|
415
|
+
|
|
416
|
+
## Decisions made
|
|
417
|
+
- No real project decisions were seeded by init. Use \`decisions/EXAMPLE.md\` only as format guidance.
|
|
418
|
+
|
|
419
|
+
## Models used
|
|
420
|
+
- VibeCompass init scaffold.
|
|
421
|
+
|
|
422
|
+
## Blockers / open questions
|
|
423
|
+
- Comprehensive architecture documentation has not been reviewed yet.
|
|
424
|
+
- Declared repos:
|
|
425
|
+
${repoLines}
|
|
426
|
+
|
|
427
|
+
## Next session should start with
|
|
428
|
+
1. Run \`vibecompass docs-review --guided\` if you want a comprehensive documentation review.
|
|
429
|
+
2. Inspect relevant code before making implementation changes in areas not covered by architecture docs.
|
|
430
|
+
3. Replace starter placeholders with evidence-backed domain/feature/component docs.
|
|
431
|
+
`;
|
|
432
|
+
}
|
|
433
|
+
|
|
243
434
|
function generateArchitectureGuide(projectConfig) {
|
|
244
435
|
return `# Architecture
|
|
245
436
|
|
|
@@ -256,7 +447,7 @@ architecture/<domain-slug>/<feature-slug>/<component>.md
|
|
|
256
447
|
- \`feature\`
|
|
257
448
|
- \`component\`
|
|
258
449
|
- \`status\`
|
|
259
|
-
-
|
|
450
|
+
- \`repos\`: array of repo IDs, even for single-repo projects
|
|
260
451
|
|
|
261
452
|
## Recommended sections
|
|
262
453
|
- \`## Description\`
|
|
@@ -328,7 +519,7 @@ Read \`${options.contextRelativeToToolingRoot}\` before doing substantive work.
|
|
|
328
519
|
|
|
329
520
|
## Session roles
|
|
330
521
|
|
|
331
|
-
|
|
|
522
|
+
| Entry trigger | Role | Owns session lifecycle? |
|
|
332
523
|
|---|---|---|
|
|
333
524
|
| "start session" | Builder | Yes |
|
|
334
525
|
| "join as reviewer" | Reviewer | No |
|
|
@@ -341,9 +532,10 @@ Read \`${options.contextRelativeToToolingRoot}\` before doing substantive work.
|
|
|
341
532
|
- \`planning mode\` — optional prompt-level mode for scoping risky or ambiguous work before implementation
|
|
342
533
|
- \`review handoff\` — reviewer reads the selected lane's \`wip.md\`, \`handoff.md\`, latest finalized note, and relevant diffs before appending findings
|
|
343
534
|
- \`address review\` — builder reads reviewer feedback in the selected lane's \`wip.md\` / \`handoff.md\`, responds inline, applies accepted changes, and refreshes the builder handoff
|
|
535
|
+
- \`close session\` — builder runs the close-out checklist and ends with \`vibecompass close-session --session <lane-id>\`; \`end session\` is accepted as a synonym
|
|
344
536
|
|
|
345
537
|
These are prompt commands for agent behavior, not \`vibecompass\` CLI subcommands.
|
|
346
|
-
Reviewer handback is explicit: the reviewer ends the pass by updating the selected lane's \`wip.md\` + \`handoff.md\` and then stopping.
|
|
538
|
+
Reviewer handback is explicit: the reviewer ends the pass by updating the selected lane's \`wip.md\` + \`handoff.md\` and then stopping. The \`close session\` prompt runs builder close-out and ends with \`vibecompass close-session --session <lane-id>\`, which follows the workflow defaults recorded in \`${options.rootRelativePath}/project.yaml\`.
|
|
347
539
|
\`vibecompass end-session\` is accepted as an alias for \`vibecompass close-session\`; \`close-session\` remains the canonical command name.
|
|
348
540
|
|
|
349
541
|
## Current session
|
|
@@ -380,7 +572,7 @@ This workspace uses VibeCompass project memory rooted at \`${options.rootRelativ
|
|
|
380
572
|
|
|
381
573
|
## Session roles
|
|
382
574
|
|
|
383
|
-
|
|
|
575
|
+
| Entry trigger | Role | Owns session lifecycle? |
|
|
384
576
|
|---|---|---|
|
|
385
577
|
| "start session" | Builder | Yes — opens and closes the session lifecycle |
|
|
386
578
|
| "join as reviewer" | Reviewer | No — reviews the builder's work and updates the handoff |
|
|
@@ -393,9 +585,10 @@ This workspace uses VibeCompass project memory rooted at \`${options.rootRelativ
|
|
|
393
585
|
- \`planning mode\` — optional prompt-level mode for scoping risky or ambiguous work before implementation
|
|
394
586
|
- \`review handoff\` — reviewer reads the selected lane's \`wip.md\`, \`handoff.md\`, latest finalized note, and relevant diffs before appending findings
|
|
395
587
|
- \`address review\` — builder reads reviewer feedback in the selected lane's \`wip.md\` / \`handoff.md\`, responds inline, applies accepted changes, and refreshes the builder handoff
|
|
588
|
+
- \`close session\` — builder runs the close-out checklist and ends with \`vibecompass close-session --session <lane-id>\`; \`end session\` is accepted as a synonym
|
|
396
589
|
|
|
397
590
|
These are prompt commands for agent behavior, not \`vibecompass\` CLI subcommands.
|
|
398
|
-
Reviewer handback is explicit: the reviewer ends the pass by updating the selected lane's \`wip.md\` + \`handoff.md\` and then stopping.
|
|
591
|
+
Reviewer handback is explicit: the reviewer ends the pass by updating the selected lane's \`wip.md\` + \`handoff.md\` and then stopping. The \`close session\` prompt runs builder close-out and ends with \`vibecompass close-session --session <lane-id>\`, which follows the workflow defaults recorded in \`${options.rootRelativePath}/project.yaml\`.
|
|
399
592
|
\`vibecompass end-session\` is accepted as an alias for \`vibecompass close-session\`; \`close-session\` remains the canonical command name.
|
|
400
593
|
|
|
401
594
|
## Startup
|
|
@@ -442,3 +635,16 @@ function describeReviewerHandback(workflow) {
|
|
|
442
635
|
|
|
443
636
|
return workflow.reviewerHandback;
|
|
444
637
|
}
|
|
638
|
+
|
|
639
|
+
function sessionDateFromGeneratedAt(value) {
|
|
640
|
+
const date = value ? new Date(value) : new Date();
|
|
641
|
+
if (Number.isNaN(date.getTime())) {
|
|
642
|
+
return new Date().toISOString().slice(0, 10);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return date.toISOString().slice(0, 10);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function quoteYamlString(value) {
|
|
649
|
+
return JSON.stringify(value);
|
|
650
|
+
}
|