@vibecompass/vibecompass 0.15.0 → 0.16.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/CHANGELOG.md +118 -0
- package/README.md +71 -0
- package/package.json +1 -1
- package/src/brief-keywords.js +203 -0
- package/src/brief-lifecycle.js +638 -0
- package/src/brief-render.js +341 -0
- package/src/brief-settings.js +160 -0
- package/src/brief.js +1557 -0
- package/src/cli.js +269 -3
- package/src/decision-lineage.js +1717 -0
- package/src/generators/agent-files/template.js +2 -1
- package/src/index.js +34 -0
- package/src/project-memory.js +30 -4
- package/src/read-model.js +92 -2
- package/src/scaffold.js +10 -2
- package/src/session.js +30 -0
|
@@ -37,7 +37,8 @@ export function renderSharedInstructionBody(context, options = {}) {
|
|
|
37
37
|
`1. Read \`${contextPath}\`.`,
|
|
38
38
|
`2. Read the latest finalized session note under \`${context.rootDir}/sessions/\`.`,
|
|
39
39
|
`3. If present, read \`${context.rootDir}/sessions/active/index.yaml\` (the lane inventory), then the selected lane's \`wip.md\` and \`handoff.md\`. The selected lane comes from an explicit \`--session\`, the nearest \`.vibecompass-lane.yaml\` worktree marker (walking up from cwd), or the single active lane.`,
|
|
40
|
-
|
|
40
|
+
'4. If present, read the selected lane\'s `brief.md` before planning: the generated session brief (D-359, D-364) selects the docs and decisions for the lane\'s task and says whether it is complete, partial, incomplete, or no-match.',
|
|
41
|
+
`5. Read relevant \`${context.rootDir}/architecture/\` and \`${context.rootDir}/decisions/\` docs before editing implementation, starting from the brief's units and follow-up reads.`,
|
|
41
42
|
'',
|
|
42
43
|
'## Prompt Commands',
|
|
43
44
|
...renderPromptCommandLines({ rootRelativePath: context.rootDir }),
|
package/src/index.js
CHANGED
|
@@ -47,5 +47,39 @@ export {
|
|
|
47
47
|
getProjectContext,
|
|
48
48
|
getFeatureContext,
|
|
49
49
|
getDecisionLog,
|
|
50
|
+
getDecisionLineage,
|
|
50
51
|
getFileContext,
|
|
51
52
|
} from './read-model.js';
|
|
53
|
+
export {
|
|
54
|
+
DECISION_LINEAGE_CONTRACT_VERSION,
|
|
55
|
+
DECISION_RELATION_TYPES,
|
|
56
|
+
DECLARED_LINEAGE_RELATIONS,
|
|
57
|
+
buildDecisionLineageModel,
|
|
58
|
+
collectDeclaredSuccessors,
|
|
59
|
+
extractArchitectureDocCitations,
|
|
60
|
+
extractDecisionFileRelations,
|
|
61
|
+
extractSessionNoteRelations,
|
|
62
|
+
parseDecisionEntries,
|
|
63
|
+
scanDecisionReferences,
|
|
64
|
+
} from './decision-lineage.js';
|
|
65
|
+
export {
|
|
66
|
+
BRIEF_CONTRACT_VERSION,
|
|
67
|
+
BRIEF_DEFAULT_BUDGET,
|
|
68
|
+
BRIEF_FOLLOW_UP_CAP,
|
|
69
|
+
BRIEF_MIN_BUDGET,
|
|
70
|
+
BRIEF_OVERVIEW_PATH,
|
|
71
|
+
BRIEF_RESERVE_TOKENS,
|
|
72
|
+
BRIEF_STATUSES,
|
|
73
|
+
buildSessionBrief,
|
|
74
|
+
estimateBriefTokens,
|
|
75
|
+
resolveBriefRequest,
|
|
76
|
+
} from './brief.js';
|
|
77
|
+
export { renderBrief } from './brief-render.js';
|
|
78
|
+
export {
|
|
79
|
+
LANE_BRIEF_FILENAME,
|
|
80
|
+
LANE_BRIEF_FORMAT,
|
|
81
|
+
generateLaneBrief,
|
|
82
|
+
inspectLaneBrief,
|
|
83
|
+
laneBriefPath,
|
|
84
|
+
} from './brief-lifecycle.js';
|
|
85
|
+
export { validateBriefSettings } from './brief-settings.js';
|
package/src/project-memory.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile, readdir } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { validateBriefSettings } from './brief-settings.js';
|
|
3
4
|
import { parseFrontmatter } from './frontmatter.js';
|
|
4
5
|
import { sha256Text } from './hash.js';
|
|
5
6
|
import { parseSimpleYaml } from './simple-yaml.js';
|
|
@@ -44,6 +45,7 @@ const KNOWN_PROJECT_FIELDS = new Set([
|
|
|
44
45
|
'default_branch',
|
|
45
46
|
'sync',
|
|
46
47
|
'runtime',
|
|
48
|
+
'brief',
|
|
47
49
|
'metadata',
|
|
48
50
|
]);
|
|
49
51
|
// D-282 runtime-isolation settings; deep value validation (ranges, absolute
|
|
@@ -73,9 +75,16 @@ const SECRET_BEARING_SYNC_FIELDS = [
|
|
|
73
75
|
'access_token',
|
|
74
76
|
];
|
|
75
77
|
|
|
76
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Scans a memory root's canonical documents. `options.exclude` is an optional
|
|
80
|
+
* predicate over root-relative paths (D-364, session brief exclusions): an
|
|
81
|
+
* excluded markdown file is dropped from the directory walk before it is
|
|
82
|
+
* opened, so it is never read. `project.yaml` is always read.
|
|
83
|
+
*/
|
|
84
|
+
export async function scanProjectMemory(rootDir, options = {}) {
|
|
77
85
|
const errors = [];
|
|
78
86
|
const documents = [];
|
|
87
|
+
const isExcluded = typeof options.exclude === 'function' ? options.exclude : () => false;
|
|
79
88
|
|
|
80
89
|
const projectPath = path.join(rootDir, PROJECT_FILE);
|
|
81
90
|
const projectDocument = await parseProjectDocument(rootDir, projectPath);
|
|
@@ -87,7 +96,8 @@ export async function scanProjectMemory(rootDir) {
|
|
|
87
96
|
const architectureDocuments = await parseMarkdownTree({
|
|
88
97
|
rootDir,
|
|
89
98
|
directoryName: 'architecture',
|
|
90
|
-
shouldInclude: (filename) =>
|
|
99
|
+
shouldInclude: (filename, relativePath) =>
|
|
100
|
+
!NON_CANONICAL_ARCHITECTURE_FILES.has(filename) && !isExcluded(relativePath),
|
|
91
101
|
parseDocument: (documentPath, relativePath, content) =>
|
|
92
102
|
parseArchitectureDocument({
|
|
93
103
|
content,
|
|
@@ -99,7 +109,8 @@ export async function scanProjectMemory(rootDir) {
|
|
|
99
109
|
const decisionDocuments = await parseMarkdownTree({
|
|
100
110
|
rootDir,
|
|
101
111
|
directoryName: 'decisions',
|
|
102
|
-
shouldInclude: (filename) =>
|
|
112
|
+
shouldInclude: (filename, relativePath) =>
|
|
113
|
+
!NON_CANONICAL_DECISION_FILES.has(filename) && !isExcluded(relativePath),
|
|
103
114
|
parseDocument: (documentPath, relativePath, content) =>
|
|
104
115
|
parseDecisionDocument({
|
|
105
116
|
content,
|
|
@@ -115,7 +126,9 @@ export async function scanProjectMemory(rootDir) {
|
|
|
115
126
|
// fail-close the whole scan as a malformed session note. The filename set
|
|
116
127
|
// still covers the legacy root-level wip.md/handoff.md.
|
|
117
128
|
shouldInclude: (filename, relativePath) =>
|
|
118
|
-
!NON_CANONICAL_SESSION_FILES.has(filename) &&
|
|
129
|
+
!NON_CANONICAL_SESSION_FILES.has(filename) &&
|
|
130
|
+
!relativePath.startsWith('sessions/active/') &&
|
|
131
|
+
!isExcluded(relativePath),
|
|
119
132
|
parseDocument: (documentPath, relativePath, content) =>
|
|
120
133
|
parseSessionDocument({
|
|
121
134
|
content,
|
|
@@ -371,6 +384,19 @@ function validateProjectData(data, relativePath) {
|
|
|
371
384
|
}
|
|
372
385
|
}
|
|
373
386
|
|
|
387
|
+
// D-364 session brief settings; the brief fails closed on any problem, so
|
|
388
|
+
// each one is surfaced here rather than silently ignored.
|
|
389
|
+
if (data.brief !== undefined) {
|
|
390
|
+
for (const problem of validateBriefSettings(data.brief).problems) {
|
|
391
|
+
warnings.push(
|
|
392
|
+
createWarning(
|
|
393
|
+
'project-brief-invalid',
|
|
394
|
+
`project.yaml brief settings: ${problem}. Session briefs read no canonical document until this is fixed (D-364).`,
|
|
395
|
+
),
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
374
400
|
if (data.metadata !== undefined && JSON.stringify(data.metadata).length > 500) {
|
|
375
401
|
warnings.push(
|
|
376
402
|
createWarning(
|
package/src/read-model.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { buildDecisionLineageModel, collectDeclaredSuccessors } from './decision-lineage.js';
|
|
3
4
|
import { parseFrontmatter } from './frontmatter.js';
|
|
4
5
|
import { generateStateManifest } from './manifest.js';
|
|
5
6
|
import { scanProjectMemory } from './project-memory.js';
|
|
@@ -8,7 +9,19 @@ const SECTION_PATTERN = /^##\s+(.+)$/gm;
|
|
|
8
9
|
const DECISION_HEADING_PATTERN = /^###\s+D-(\d{3,})\s+—\s+(.+)$/gm;
|
|
9
10
|
|
|
10
11
|
export async function loadProjectReadModel(rootDir) {
|
|
11
|
-
|
|
12
|
+
return (await loadProjectReadModelWithDocuments(rootDir)).readModel;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The read model plus the scanned canonical documents (`{ path, kind,
|
|
17
|
+
* content, … }`) it was built from, for read-only consumers such as the
|
|
18
|
+
* session brief that need section text the read model does not keep.
|
|
19
|
+
* `options.exclude` (a root-relative path predicate) keeps excluded documents
|
|
20
|
+
* out of the scan before they are read (D-364); `loadProjectReadModel` never
|
|
21
|
+
* excludes.
|
|
22
|
+
*/
|
|
23
|
+
export async function loadProjectReadModelWithDocuments(rootDir, options = {}) {
|
|
24
|
+
const scanResult = await scanProjectMemory(rootDir, { exclude: options.exclude });
|
|
12
25
|
if (scanResult.errors.length > 0) {
|
|
13
26
|
const details = scanResult.errors.map((error) => `${error.path}: ${error.message}`).join('\n');
|
|
14
27
|
throw new Error(`Cannot build read model with canonical parse errors.\n${details}`);
|
|
@@ -35,8 +48,9 @@ export async function loadProjectReadModel(rootDir) {
|
|
|
35
48
|
const decisions = buildDecisionList(scanResult);
|
|
36
49
|
const sessions = buildSessionList(scanResult);
|
|
37
50
|
const fileOwners = buildFileOwnershipIndex(features);
|
|
51
|
+
const decisionLineage = buildDecisionLineageModel(scanResult.documents);
|
|
38
52
|
|
|
39
|
-
|
|
53
|
+
const readModel = {
|
|
40
54
|
freshness,
|
|
41
55
|
repo_aliases: repoAliases,
|
|
42
56
|
manifest_state: {
|
|
@@ -51,7 +65,10 @@ export async function loadProjectReadModel(rootDir) {
|
|
|
51
65
|
decisions,
|
|
52
66
|
sessions,
|
|
53
67
|
file_owners: fileOwners,
|
|
68
|
+
decision_lineage: decisionLineage,
|
|
54
69
|
};
|
|
70
|
+
|
|
71
|
+
return { readModel, documents: scanResult.documents };
|
|
55
72
|
}
|
|
56
73
|
|
|
57
74
|
export function getProjectContext(readModel, options = {}) {
|
|
@@ -90,6 +107,50 @@ export function getDecisionLog(readModel, options = {}) {
|
|
|
90
107
|
};
|
|
91
108
|
}
|
|
92
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Typed lineage for one decision (plan task A2; D-359). `outgoing` is what
|
|
112
|
+
* this decision's own text declares about earlier decisions, `incoming` is
|
|
113
|
+
* what later decisions declare about it, and `declared_successors` follows
|
|
114
|
+
* declared `supersedes`/`amends` transitively. Mentions stay mentions: no
|
|
115
|
+
* relation here certifies that a decision governs anything or is currently
|
|
116
|
+
* valid.
|
|
117
|
+
*/
|
|
118
|
+
export function getDecisionLineage(readModel, decisionId, options = {}) {
|
|
119
|
+
const id = typeof decisionId === 'number' ? decisionId : Number(String(decisionId ?? '').replace(/^D-/i, ''));
|
|
120
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const citationLimit = Math.max(1, Math.min(options.citationLimit ?? 50, 500));
|
|
125
|
+
const relations = readModel.decision_lineage.relations;
|
|
126
|
+
const decision = readModel.decisions.find((entry) => entry.decision_id === id) ?? null;
|
|
127
|
+
const lineageRelations = new Set(['supersedes', 'amends', 'preserves', 'unknown']);
|
|
128
|
+
const outgoing = relations.filter(
|
|
129
|
+
(relation) => relation.source_decision_id === id && lineageRelations.has(relation.relation),
|
|
130
|
+
);
|
|
131
|
+
const incoming = relations.filter(
|
|
132
|
+
(relation) =>
|
|
133
|
+
relation.target_decision_id === id && relation.source_kind === 'decision' && lineageRelations.has(relation.relation),
|
|
134
|
+
);
|
|
135
|
+
const mentions = relations.filter(
|
|
136
|
+
(relation) => relation.target_decision_id === id && ['cites', 'references', 'made'].includes(relation.relation),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
freshness: readModel.freshness,
|
|
141
|
+
decision_id: id,
|
|
142
|
+
exists: Boolean(decision),
|
|
143
|
+
title: decision?.title ?? null,
|
|
144
|
+
path: decision?.path ?? null,
|
|
145
|
+
outgoing,
|
|
146
|
+
incoming,
|
|
147
|
+
declared_successors: collectDeclaredSuccessors(relations, id),
|
|
148
|
+
made_in: mentions.filter((relation) => relation.relation === 'made').map((relation) => relation.source_path),
|
|
149
|
+
cited_by: mentions.filter((relation) => relation.relation !== 'made').slice(0, citationLimit),
|
|
150
|
+
cited_by_total: mentions.filter((relation) => relation.relation !== 'made').length,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
93
154
|
export function getFileContext(readModel, filepath) {
|
|
94
155
|
const normalizedPath = normalizeLookupPath(filepath, readModel.repo_aliases, readModel.project.repos);
|
|
95
156
|
const owners = readModel.file_owners.get(normalizedPath) ?? [];
|
|
@@ -194,6 +255,8 @@ function buildComponentRecord(document, repoAliases, projectRepos) {
|
|
|
194
255
|
description: normalizeSectionText(sections.get('Description')),
|
|
195
256
|
details: normalizeSectionText(sections.get('Details')),
|
|
196
257
|
next_steps: normalizeSectionText(sections.get('Next steps')),
|
|
258
|
+
retrieval_guidance: normalizeSectionText(sections.get('Retrieval guidance')),
|
|
259
|
+
retrieval_scope: extractReviewMetadataField(sections.get('Review metadata'), 'Retrieval scope'),
|
|
197
260
|
involved_files: involvedFiles,
|
|
198
261
|
warnings: document.warnings,
|
|
199
262
|
warning_count: document.warnings.length,
|
|
@@ -529,6 +592,33 @@ function normalizeSectionText(value) {
|
|
|
529
592
|
return value ? value.trim() : null;
|
|
530
593
|
}
|
|
531
594
|
|
|
595
|
+
// One bullet from the `## Review metadata` list ("- Retrieval scope: …"),
|
|
596
|
+
// including indented continuation lines; null when the section or bullet is
|
|
597
|
+
// missing or empty.
|
|
598
|
+
function extractReviewMetadataField(section, label) {
|
|
599
|
+
if (!section) {
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const lines = section.split(/\r?\n/);
|
|
604
|
+
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
605
|
+
const start = lines.findIndex((line) => new RegExp(`^\\s*[-*]\\s+${escaped}:`, 'i').test(line));
|
|
606
|
+
if (start === -1) {
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const parts = [lines[start].replace(new RegExp(`^\\s*[-*]\\s+${escaped}:\\s*`, 'i'), '')];
|
|
611
|
+
for (const line of lines.slice(start + 1)) {
|
|
612
|
+
if (!/^\s+\S/.test(line) || /^\s*[-*]\s+/.test(line)) {
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
parts.push(line.trim());
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const value = parts.join(' ').trim();
|
|
619
|
+
return value || null;
|
|
620
|
+
}
|
|
621
|
+
|
|
532
622
|
function extractLabeledValue(body, label) {
|
|
533
623
|
const pattern = new RegExp(`\\*\\*${label}:\\*\\*\\s+(.+)`);
|
|
534
624
|
return body.match(pattern)?.[1]?.trim() ?? null;
|
package/src/scaffold.js
CHANGED
|
@@ -226,6 +226,7 @@ This workspace uses VibeCompass project memory rooted at \`${rootRelativePath}\`
|
|
|
226
226
|
- \`${rootRelativePath}/sessions/active/<lane-id>/session.yaml\` — lane metadata (including the recorded \`lane_marker\` when one exists)
|
|
227
227
|
- \`${rootRelativePath}/sessions/active/<lane-id>/wip.md\` — lane-local builder scratchpad
|
|
228
228
|
- \`${rootRelativePath}/sessions/active/<lane-id>/handoff.md\` — lane-local builder/reviewer relay
|
|
229
|
+
- \`${rootRelativePath}/sessions/active/<lane-id>/brief.md\` — generated session brief (D-359, D-364): the memory this lane should read first, written by \`start-session\` and \`continue-session\` when \`brief.enabled: true\` (opt-in, D-368) or on request with \`vibecompass brief --write\`; never hand-edit
|
|
229
230
|
- \`.vibecompass-lane.yaml\` — worktree-local lane marker (D-280); lives outside the memory root, written only by \`vibecompass write-lane-marker\` or worktree provisioning, never synced
|
|
230
231
|
- \`${rootRelativePath}/decisions/INDEX.md\` — derived grouped decision index; refresh with \`vibecompass refresh-decision-index\` (D-283, structure-preserving) instead of hand-editing rows
|
|
231
232
|
|
|
@@ -233,6 +234,7 @@ This workspace uses VibeCompass project memory rooted at \`${rootRelativePath}\`
|
|
|
233
234
|
- VibeCompass active builder sessions are named lanes. Use one lane per active feature or workstream.
|
|
234
235
|
- \`vibecompass start-session\` requires \`--id <lane-id>\` so each active lane has a meaningful feature or workstream name.
|
|
235
236
|
- The active lane scratch files live under \`${rootRelativePath}/sessions/active/<lane-id>/\`.
|
|
237
|
+
- When \`${rootRelativePath}/project.yaml\` sets \`brief.enabled: true\` (opt-in, D-368), \`start-session\` and \`continue-session\` write the lane's generated \`brief.md\` (D-359, D-364): the task is the lane's working-on, the files its claims, and the features its feature slugs. A brief failure is reported as a warning or an incomplete brief and never fails the lifecycle command. \`--no-brief\` skips it for one command, \`vibecompass brief --session <lane-id> --write\` writes one on request whatever the setting, and \`brief.exclude\` lists root-relative globs the brief must never read.
|
|
236
238
|
- Lane selection follows D-277: an explicit \`--session\` wins, then the nearest worktree lane marker (\`.vibecompass-lane.yaml\`, walking up from cwd), then the single active lane. With two or more active lanes there is no implicit current-lane fallback.
|
|
237
239
|
- \`${rootRelativePath}/sessions/active/index.yaml\` is the lane inventory; its \`current\` pointer and the tool-specific Current session block are human-readable continuity summaries, not the lane-selection source of truth.
|
|
238
240
|
- An active lane directory is an open session, not an unclosed crash (D-353). Resume it with \`vibecompass continue-session [<lane-id>]\`: the lane keeps its opening date and session number; the command records \`resumed_at\`/\`resume_count\` in \`session.yaml\`, appends a dated \`Resumed\` line to \`wip.md\`, moves the index \`current\` hint and the Current session block to the lane, and re-emits runtime, git-binding, and staleness warnings. \`start-session --id <existing-lane>\` fails closed and names \`continue-session\`.
|
|
@@ -276,7 +278,8 @@ ${renderWorkflowDefaults(workflow)}
|
|
|
276
278
|
3. Inventory active lanes with \`vibecompass list-sessions\` (or read \`${rootRelativePath}/sessions/active/index.yaml\`), then follow the "Open, resume, or choose" protocol below to decide between opening a new lane and resuming one; select a lane from an explicit \`--session\`, the nearest worktree lane marker, or the single active lane (D-277).
|
|
277
279
|
4. If present, read \`${rootRelativePath}/sessions/active/<lane-id>/wip.md\`.
|
|
278
280
|
5. If present, read \`${rootRelativePath}/sessions/active/<lane-id>/handoff.md\`.
|
|
279
|
-
6.
|
|
281
|
+
6. If present, read \`${rootRelativePath}/sessions/active/<lane-id>/brief.md\` before planning: the generated session brief selects the architecture docs, decisions (with their declared successors), and notes for the lane's task. Its status line says whether it is complete, partial, incomplete (read the listed required reads first), or no-match; \`vibecompass brief --check\` reports whether it is stale.
|
|
282
|
+
7. Read the relevant docs under \`${rootRelativePath}/architecture/\` and \`${rootRelativePath}/decisions/\`, starting from the brief's units and follow-up reads.
|
|
280
283
|
|
|
281
284
|
## Open, resume, or choose (D-353)
|
|
282
285
|
Both \`start session\` and \`continue session\` begin with the active-lane inventory. Then:
|
|
@@ -284,7 +287,7 @@ Both \`start session\` and \`continue session\` begin with the active-lane inven
|
|
|
284
287
|
- **Exactly one active lane, or a cwd bound to a lane by a worktree marker:** resume it with \`vibecompass continue-session\` without asking. Open a second lane only when the user is clearly asking for separate new work; never re-open a lane that already exists.
|
|
285
288
|
- **Two or more active lanes, no marker, no lane named by the user:** do not guess. Present the inventory (lane id, working-on, opened date, resume count, branch/worktree, last log line) and ask which lane to continue or whether to open a new one. The index \`current\` pointer and the Current session block are continuity hints to mention, never defaults to adopt silently. Resume with \`vibecompass continue-session <lane-id>\` once the user answers (\`continue session <lane-id>\`).
|
|
286
289
|
- **Lane named by the user:** \`continue session <lane-id>\` resumes that lane; an unknown id fails closed with the inventory.
|
|
287
|
-
- After resuming, read the lane's \`wip.md\` (latest \`## Log\` entries and \`## Reviewer input needed\`), \`handoff.md\`, and the latest finalized note; treat resume-staleness warnings (new decisions past the lane snapshot, stale base revisions, newer finalized notes touching the lane scope, claim overlap) as required reading before editing.
|
|
290
|
+
- After resuming, read the lane's \`wip.md\` (latest \`## Log\` entries and \`## Reviewer input needed\`), \`handoff.md\`, \`brief.md\` if present (with \`brief.enabled: true\`, continue-session keeps a current brief and regenerates a missing or stale one), and the latest finalized note; treat resume-staleness warnings (new decisions past the lane snapshot, stale base revisions, newer finalized notes touching the lane scope, claim overlap) as required reading before editing.
|
|
288
291
|
- Run recovery (reconstruct a missing finalized note from git history) only when the Current session block date is in the past *and* no active lane exists *and* no finalized note covers that date. An active lane with a past date is simply resumable.
|
|
289
292
|
|
|
290
293
|
## Builder workflow
|
|
@@ -302,6 +305,8 @@ Session lane: <lane-id>
|
|
|
302
305
|
|
|
303
306
|
## Reviewer input needed
|
|
304
307
|
|
|
308
|
+
## Context used
|
|
309
|
+
|
|
305
310
|
## Review log
|
|
306
311
|
\`\`\`
|
|
307
312
|
|
|
@@ -330,6 +335,8 @@ Session lane: <lane-id>
|
|
|
330
335
|
During the session:
|
|
331
336
|
- append short summaries to \`wip.md\` after meaningful exchanges
|
|
332
337
|
- keep \`handoff.md\` current after substantive work blocks
|
|
338
|
+
- optionally list under \`wip.md\` \`## Context used\` the docs and decisions the lane actually relied on (path or D-number, and what for), so the brief's usefulness can be audited
|
|
339
|
+
- refresh the lane brief with \`vibecompass brief --write\` after the lane's scope changes; \`vibecompass brief --check\` reports whether it is stale
|
|
333
340
|
- run \`eval "$(vibecompass lane-env)"\` in a lane shell before starting dev servers or build tools so the lane's assigned port and temp dir are used (D-282); do not hardcode ports in lane work
|
|
334
341
|
- run \`vibecompass docs-update --session <lane-id>\` whenever you need an ad hoc targeted documentation-maintenance plan for the current session delta
|
|
335
342
|
- after substantive feature work, confirm affected architecture docs and decisions still match the implementation; if not, update them while the context is fresh — fold the changes into the doc's current-state sections (rewrite in place; no dated "update" sections, lane names in headings, or completed-task chronology; D-292)
|
|
@@ -588,6 +595,7 @@ Finalized session notes for ${projectConfig.name} live here.
|
|
|
588
595
|
- \`active/<lane-id>/session.yaml\` — lane metadata, including the recorded \`lane_marker\` when one exists
|
|
589
596
|
- \`active/<lane-id>/wip.md\` — builder scratchpad during an active lane
|
|
590
597
|
- \`active/<lane-id>/handoff.md\` — reviewer/builder baton-pass during an active lane
|
|
598
|
+
- \`active/<lane-id>/brief.md\` — generated session brief for the lane (D-359, D-364); regenerated, never hand-edited
|
|
591
599
|
|
|
592
600
|
Those scratch files are session-scoped working artifacts, not finalized history.
|
|
593
601
|
`;
|
package/src/session.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { access, mkdir, readFile, readdir, realpath, rm, rmdir, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { refreshLaneBriefForLifecycle } from './brief-lifecycle.js';
|
|
4
5
|
import { scanProjectMemory } from './project-memory.js';
|
|
5
6
|
import { parseSimpleYaml } from './simple-yaml.js';
|
|
6
7
|
import { buildCloseSessionGuidance, resolveWorkflowSettings } from './workflow.js';
|
|
@@ -398,6 +399,16 @@ async function startProjectSessionLocked(normalized, options, markerContext) {
|
|
|
398
399
|
rootDir: normalized.rootDir,
|
|
399
400
|
toolingRootDir: normalized.toolingRootDir,
|
|
400
401
|
});
|
|
402
|
+
// D-359/D-364/D-368: the opt-in lane brief is written only after every
|
|
403
|
+
// lifecycle write succeeded, still inside the memory-root lock; it never
|
|
404
|
+
// throws, so a brief failure cannot overturn the start.
|
|
405
|
+
const brief = await refreshLaneBriefForLifecycle({
|
|
406
|
+
trigger: 'start',
|
|
407
|
+
rootDir: normalized.rootDir,
|
|
408
|
+
laneId: sessionId,
|
|
409
|
+
disabled: options?.brief === false,
|
|
410
|
+
buildBrief: options?.briefBuilder,
|
|
411
|
+
});
|
|
401
412
|
|
|
402
413
|
return {
|
|
403
414
|
rootDir: normalized.rootDir,
|
|
@@ -420,7 +431,9 @@ async function startProjectSessionLocked(normalized, options, markerContext) {
|
|
|
420
431
|
...(gitBinding?.warnings ?? []),
|
|
421
432
|
...manifestRefresh.warnings,
|
|
422
433
|
...auditWarnings,
|
|
434
|
+
...brief.warnings,
|
|
423
435
|
],
|
|
436
|
+
brief,
|
|
424
437
|
...(gitBinding
|
|
425
438
|
? {
|
|
426
439
|
gitBinding: {
|
|
@@ -1145,6 +1158,18 @@ async function continueProjectSessionLocked(normalized, options, markerContext)
|
|
|
1145
1158
|
rootDir: normalized.rootDir,
|
|
1146
1159
|
toolingRootDir: normalized.toolingRootDir,
|
|
1147
1160
|
});
|
|
1161
|
+
// D-359/D-364: keep a current lane brief, regenerate a missing or stale
|
|
1162
|
+
// one. `--source-root` (D-255 dogfood) is read only when named here; it is
|
|
1163
|
+
// never inferred from the existing brief's header.
|
|
1164
|
+
const brief = await refreshLaneBriefForLifecycle({
|
|
1165
|
+
trigger: 'continue',
|
|
1166
|
+
rootDir: normalized.rootDir,
|
|
1167
|
+
laneId: lane.id,
|
|
1168
|
+
disabled: options?.brief === false,
|
|
1169
|
+
sourceRootDir: options?.sourceRootDir ? path.resolve(normalized.cwd, options.sourceRootDir) : undefined,
|
|
1170
|
+
sourceSessionId: normalizeOptionalString(options?.sourceSessionId) ?? undefined,
|
|
1171
|
+
buildBrief: options?.briefBuilder,
|
|
1172
|
+
});
|
|
1148
1173
|
const finalizedSessions = await listFinalizedSessions(normalized.sessionsDir);
|
|
1149
1174
|
const latestFinalized = finalizedSessions.length > 0 ? finalizedSessions[finalizedSessions.length - 1] : null;
|
|
1150
1175
|
// The returned inventory is the post-resume state (what list-sessions shows
|
|
@@ -1181,6 +1206,7 @@ async function continueProjectSessionLocked(normalized, options, markerContext)
|
|
|
1181
1206
|
otherLanes: inventoryAfter.lanes.filter((item) => item.id !== lane.id),
|
|
1182
1207
|
inventory: inventoryAfter,
|
|
1183
1208
|
docsUpdatePlan: docsUpdate.plan,
|
|
1209
|
+
brief,
|
|
1184
1210
|
warnings: [
|
|
1185
1211
|
...markerContext.warnings,
|
|
1186
1212
|
...selection.warnings,
|
|
@@ -1196,6 +1222,7 @@ async function continueProjectSessionLocked(normalized, options, markerContext)
|
|
|
1196
1222
|
}),
|
|
1197
1223
|
...docsUpdate.warnings,
|
|
1198
1224
|
...manifestRefresh.warnings,
|
|
1225
|
+
...brief.warnings,
|
|
1199
1226
|
],
|
|
1200
1227
|
manifest: manifestRefresh.manifest,
|
|
1201
1228
|
agentFileSync,
|
|
@@ -2279,6 +2306,9 @@ ${options.workingOn}
|
|
|
2279
2306
|
## Reviewer input needed
|
|
2280
2307
|
- None yet.
|
|
2281
2308
|
|
|
2309
|
+
## Context used
|
|
2310
|
+
- Optional: the docs and decisions this lane actually relied on (path or D-number, and what for) — an audit trail for the session brief; list what you used, not everything the brief showed.
|
|
2311
|
+
|
|
2282
2312
|
## Review log
|
|
2283
2313
|
`;
|
|
2284
2314
|
}
|