@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.
@@ -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
- `4. Read relevant \`${context.rootDir}/architecture/\` and \`${context.rootDir}/decisions/\` docs before editing implementation.`,
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';
@@ -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
- export async function scanProjectMemory(rootDir) {
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) => !NON_CANONICAL_ARCHITECTURE_FILES.has(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) => !NON_CANONICAL_DECISION_FILES.has(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) && !relativePath.startsWith('sessions/active/'),
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
- const scanResult = await scanProjectMemory(rootDir);
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
- return {
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. Read the relevant docs under \`${rootRelativePath}/architecture/\` and \`${rootRelativePath}/decisions/\`.
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
  }