@vibecompass/vibecompass 0.10.1 → 0.10.3

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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.3 - 2026-06-19
4
+
5
+ - Let `start-session` create a missing `## Current session` summary block in adopted existing `CLAUDE.md` files instead of failing after guided init.
6
+ - Use the local calendar date, not UTC, when naming the starter `project-memory-initialized` session note during init.
7
+
8
+ ## 0.10.2 - 2026-06-19
9
+
10
+ - Add a package-owned docs-review source inventory scanner that writes `state/docs-review-source-inventory.json`, includes scanned subsystem inventory in the review prompt, and records `scanned_unaccounted` / `source_unavailable` reconciliation warnings without changing the primary coverage score basis.
11
+ - Add an accepted docs-review documentation-plan projection at `state/docs-review-documentation-plan.json`, preserving plan titles, purposes, parent groups, baseline/deepening scope, linked inventory ids, evidence, anchors, and producer stamps as derived state.
12
+ - Add `docs-review --source-root <id=path>` so Git-backed repos can be scanned from local checkouts for a run without committing machine-local paths to `project.yaml`.
13
+ - Add a source-inventory concurrency stress test script and run it during `prepublishOnly` to guard symlink root-containment regressions.
14
+ - Ignore common OS metadata files such as `.DS_Store` during source inventory scans so package evidence does not create false route/API items.
15
+ - Collapse nested `config/<group>/**` source-inventory evidence into one platform subsystem per config group instead of fragmenting every leaf config file into its own item.
16
+ - Report all dangling `completeness_inventory[].coverage_area_ids[]` references in one docs-review apply error and prompt agents to fix them at the plan gate.
17
+ - Clarify docs-review coverage reporting so scores are described as evidence/completeness-inventory accounting when present, not doc-count accounting.
18
+
3
19
  ## 0.10.1 - 2026-06-14
4
20
 
5
21
  - Add completeness-inventory guidance to docs-review so coverage plans account for accepted, deferred, and missing subsystems instead of silently omitting large areas.
package/README.md CHANGED
@@ -134,6 +134,7 @@ npx -y @vibecompass/vibecompass@latest sync-agents --root .compass
134
134
 
135
135
  # Architecture docs review
136
136
  npx -y @vibecompass/vibecompass@latest docs-review --root .compass --guided
137
+ npx -y @vibecompass/vibecompass@latest docs-review --root .compass --guided --source-root app=../app
137
138
  npx -y @vibecompass/vibecompass@latest docs-review --root .compass --apply-output
138
139
 
139
140
  # Rerun docs review from an archived architecture-doc slate
@@ -153,6 +154,9 @@ npx -y @vibecompass/vibecompass@latest docs-review --root .compass --rebuild --a
153
154
  state/
154
155
  ```
155
156
 
157
+ Docs review writes derived state under `state/`, including scanned source
158
+ inventory, accepted coverage, and the accepted documentation-plan projection.
159
+
156
160
  Generated agent files can also include managed VibeCompass blocks:
157
161
 
158
162
  ```text
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecompass/vibecompass",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "description": "Local-first project-memory core for VibeCompass",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,8 @@
18
18
  ],
19
19
  "scripts": {
20
20
  "test": "node --test src/tests/*.test.js",
21
- "prepublishOnly": "npm test"
21
+ "test:source-inventory-stress": "node --test --test-concurrency=8 src/tests/source-inventory.test.js",
22
+ "prepublishOnly": "npm test && npm run test:source-inventory-stress"
22
23
  },
23
24
  "keywords": [
24
25
  "vibecompass",
package/src/cli.js CHANGED
@@ -286,6 +286,13 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
286
286
  if (result.localReview) {
287
287
  io.stdout.write(`Local review output: ${result.localReview.output_path}\n`);
288
288
  }
289
+ if (result.sourceInventory) {
290
+ io.stdout.write(`Source inventory: ${result.sourceInventory.summary.item_count} items`);
291
+ if (result.sourceInventory.summary.warning_count > 0) {
292
+ io.stdout.write(` (${result.sourceInventory.summary.warning_count} warnings)`);
293
+ }
294
+ io.stdout.write('\n');
295
+ }
289
296
  if (result.rebuild) {
290
297
  io.stdout.write(`Rebuild scope: ${result.rebuild.scope_path}\n`);
291
298
  io.stdout.write(`Stale policy: ${result.rebuild.stale_policy}\n`);
@@ -1170,6 +1177,14 @@ function parseDocsReviewArgs(argv) {
1170
1177
  case '--sync-target':
1171
1178
  parsed.syncTarget = value;
1172
1179
  break;
1180
+ case '--source-root': {
1181
+ const [repoId, sourceRootPath] = splitAssignment(value, '--source-root');
1182
+ parsed.sourceRootOverrides = [
1183
+ ...(parsed.sourceRootOverrides ?? []),
1184
+ { repoId, path: sourceRootPath },
1185
+ ];
1186
+ break;
1187
+ }
1173
1188
  case '--max-tokens':
1174
1189
  parsed.maxTokens = value;
1175
1190
  break;
@@ -1605,6 +1620,7 @@ function usageText() {
1605
1620
  ' --poll-hosted Poll hosted docs-review run status and update state/docs-review.json',
1606
1621
  ' --complete Mark accepted docs-review changes as completed locally',
1607
1622
  ' --run-local Run the generated review request with a local provider',
1623
+ ' --source-root <id=path> Repeatable run-local source root override for Git-backed repos',
1608
1624
  ' --provider <name> Local provider for --run-local. Currently: anthropic',
1609
1625
  ' --run-local-anthropic Compatibility alias for --run-local --provider anthropic',
1610
1626
  ' --apply-output Apply accepted architecture doc blocks from review output',
@@ -6,15 +6,27 @@ import { sha256Text } from './hash.js';
6
6
  import { writeStateManifest } from './manifest.js';
7
7
  import { scanProjectMemory } from './project-memory.js';
8
8
  import { parseSimpleYaml } from './simple-yaml.js';
9
+ import {
10
+ buildDocsReviewSourceInventory,
11
+ docsReviewSourceInventoryPath,
12
+ readDocsReviewSourceInventory,
13
+ reconcileCoverageWithSourceInventory,
14
+ summarizeSourceInventory,
15
+ writeDocsReviewSourceInventory,
16
+ } from './source-inventory.js';
9
17
  import { readSyncCursor, resolveSyncBinding } from './sync-binding.js';
10
18
  import { PACKAGE_VERSION } from './version.js';
11
19
 
12
20
  const DOCS_REVIEW_PROMPT_VERSION = 'VibeCompass Docs Review Prompt v6';
21
+ const DOCS_REVIEW_PARSER_VERSION = 'docs-review-parser-v1';
22
+ const DOCS_REVIEW_DOCUMENTATION_PLAN_PROJECTION_VERSION = 'docs-review-documentation-plan-v1';
23
+ const DOCUMENTATION_PLAN_STATE_VERSION = 1;
13
24
  const ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES = 12000;
14
25
  const COVERAGE_PLAN_OUTPUT_VERSION = 1;
15
26
  const COVERAGE_PLAN_STATUSES = new Set(['accepted', 'deferred', 'missing']);
16
27
  const COVERAGE_LEVELS = new Set(['comprehensive', 'partial', 'initial', 'missing']);
17
28
  const ANCHOR_ACTIONS = new Set(['reuse', 'update', 'split', 'merge', 'defer', 'replace', 'new']);
29
+ const DOCUMENTATION_PLAN_RUN_SCOPES = new Set(['baseline', 'deepening']);
18
30
  const REBUILD_STALE_POLICIES = new Set(['keep', 'archive']);
19
31
  const PROJECT_MAP_PATH = 'architecture/overview/project-shape.md';
20
32
 
@@ -93,6 +105,12 @@ export async function preflightDocsReview(options = {}, environment = {}) {
93
105
  anthropicEnvVar: options.anthropicEnvVar ?? 'ANTHROPIC_API_KEY',
94
106
  syncTarget: options.syncTarget ?? null,
95
107
  });
108
+ const sourceInventory = await buildDocsReviewSourceInventory(project, {
109
+ rootDir,
110
+ cwd,
111
+ sourceRootOverrides: options.sourceRootOverrides,
112
+ });
113
+ await writeDocsReviewSourceInventory(rootDir, sourceInventory);
96
114
  const reviewPrompt = renderReviewPrompt({
97
115
  project,
98
116
  rootDir,
@@ -100,6 +118,7 @@ export async function preflightDocsReview(options = {}, environment = {}) {
100
118
  model: reviewConfig.model,
101
119
  executionMode: promptExecutionMode,
102
120
  anchors: await buildDocsReviewAnchorContext(rootDir),
121
+ sourceInventory,
103
122
  });
104
123
 
105
124
  const status = options.submitHosted
@@ -138,6 +157,7 @@ export async function preflightDocsReview(options = {}, environment = {}) {
138
157
  llm: reviewConfig.llm,
139
158
  model: reviewConfig.model,
140
159
  runtime,
160
+ source_inventory: summarizeSourceInventory(sourceInventory),
141
161
  ...(hosted ? { hosted } : {}),
142
162
  ...(localReview ? { local_review: localReview } : {}),
143
163
  completed_at: null,
@@ -154,8 +174,10 @@ export async function preflightDocsReview(options = {}, environment = {}) {
154
174
  status,
155
175
  hosted,
156
176
  localReview,
177
+ sourceInventory,
157
178
  reviewPrompt,
158
179
  warnings: [
180
+ ...(sourceInventory.warnings ?? []).map((warning) => warning.message ?? `${warning.code}: ${warning.repo_id ?? warning.path ?? ''}`.trim()),
159
181
  ...(localReview?.truncated ? [`Local ${localReview.provider} docs-review stopped at max_tokens; saved output may be partial.`] : []),
160
182
  ],
161
183
  message: renderDocsReviewMessage(options),
@@ -242,14 +264,40 @@ async function applyDocsReviewOutput(options) {
242
264
  }
243
265
 
244
266
  validateArchitectureDocBlocks(blocks);
267
+ const sourceInventory = await readDocsReviewSourceInventory(options.rootDir);
245
268
  const coverageProjection = buildCoverageProjection(coverageBlocks, {
246
269
  outputPath,
247
270
  outputHash: sha256Text(source),
271
+ sourceInventory,
248
272
  });
249
273
  const anchorContext = coverageProjection ? await buildDocsReviewAnchorContext(options.rootDir) : null;
274
+ const sourceInventoryReconciliation = reconcileCoverageWithSourceInventory(coverageProjection, sourceInventory);
275
+ if (coverageProjection && sourceInventory) {
276
+ coverageProjection.source_inventory_summary = summarizeSourceInventory(sourceInventory);
277
+ coverageProjection.reconciliation_summary = sourceInventoryReconciliation
278
+ ? {
279
+ scanned_count: sourceInventoryReconciliation.scanned_count,
280
+ declared_count: sourceInventoryReconciliation.declared_count,
281
+ accounted_count: sourceInventoryReconciliation.accounted_count,
282
+ unaccounted_ids: sourceInventoryReconciliation.unaccounted_ids,
283
+ unknown_declared_ids: sourceInventoryReconciliation.unknown_declared_ids,
284
+ source_unavailable_repo_ids: sourceInventoryReconciliation.source_unavailable_repo_ids,
285
+ }
286
+ : null;
287
+ coverageProjection.warnings = sourceInventoryReconciliation?.warnings ?? [];
288
+ }
289
+ const documentationPlanProjection = coverageProjection
290
+ ? buildDocumentationPlanProjection(coverageProjection, sourceInventory, {
291
+ generatedAt: options.generatedAt,
292
+ })
293
+ : null;
294
+ if (coverageProjection && documentationPlanProjection) {
295
+ coverageProjection.documentation_plan_summary = summarizeDocumentationPlan(documentationPlanProjection);
296
+ }
250
297
  const decisionRecommendations = validateDecisionRecommendationBlocks(decisionRecommendationBlocks);
251
298
  const warnings = [
252
299
  ...createCoveragePlanQualityWarnings(coverageProjection, anchorContext),
300
+ ...(sourceInventoryReconciliation?.warnings ?? []).map((warning) => `${warning.code}: ${warning.message}`),
253
301
  ...createArchitectureDocSizeWarnings(blocks),
254
302
  ...createArchitectureDocQualityWarnings(blocks),
255
303
  ];
@@ -264,6 +312,7 @@ async function applyDocsReviewOutput(options) {
264
312
  }
265
313
 
266
314
  let appliedCoverage = null;
315
+ let appliedDocumentationPlan = null;
267
316
  if (coverageProjection) {
268
317
  const coveragePath = path.join(path.dirname(options.statePath), 'docs-review-coverage.json');
269
318
  await writeFile(coveragePath, `${JSON.stringify(coverageProjection, null, 2)}\n`, 'utf8');
@@ -274,6 +323,16 @@ async function applyDocsReviewOutput(options) {
274
323
  statuses: coverageProjection.statuses,
275
324
  };
276
325
  }
326
+ if (documentationPlanProjection) {
327
+ const documentationPlanPath = path.join(path.dirname(options.statePath), 'docs-review-documentation-plan.json');
328
+ await writeFile(documentationPlanPath, `${JSON.stringify(documentationPlanProjection, null, 2)}\n`, 'utf8');
329
+ appliedDocumentationPlan = {
330
+ path: documentationPlanPath,
331
+ item_count: documentationPlanProjection.summary.item_count,
332
+ by_status: documentationPlanProjection.summary.by_status,
333
+ by_run_scope: documentationPlanProjection.summary.by_run_scope,
334
+ };
335
+ }
277
336
 
278
337
  const appliedDecisionCandidates = decisionRecommendations.length > 0
279
338
  ? await applyDecisionRecommendations({
@@ -300,6 +359,7 @@ async function applyDocsReviewOutput(options) {
300
359
  manifest_hash: manifestResult.manifest.canonical.manifest_hash,
301
360
  architecture_docs: applied,
302
361
  ...(appliedCoverage ? { coverage: appliedCoverage } : {}),
362
+ ...(appliedDocumentationPlan ? { documentation_plan: appliedDocumentationPlan } : {}),
303
363
  ...(appliedDecisionCandidates.applied.length > 0 ? { decision_candidates: appliedDecisionCandidates.applied } : {}),
304
364
  ...(warnings.length > 0 ? { warnings } : {}),
305
365
  applied_at: appliedAt,
@@ -930,6 +990,15 @@ function buildCoverageProjection(blocks, options) {
930
990
 
931
991
  return {
932
992
  version: COVERAGE_PLAN_OUTPUT_VERSION,
993
+ producer: {
994
+ package_version: PACKAGE_VERSION,
995
+ prompt_version: DOCS_REVIEW_PROMPT_VERSION,
996
+ parser_version: DOCS_REVIEW_PARSER_VERSION,
997
+ coverage_projection_version: COVERAGE_PLAN_OUTPUT_VERSION,
998
+ ...(options.sourceInventory?.producer?.scanner_version
999
+ ? { scanner_version: options.sourceInventory.producer.scanner_version }
1000
+ : {}),
1001
+ },
933
1002
  source: {
934
1003
  output_path: options.outputPath,
935
1004
  output_hash: options.outputHash,
@@ -944,6 +1013,7 @@ function buildCoverageProjection(blocks, options) {
944
1013
  inventory_count: completenessInventory.length,
945
1014
  inventory_statuses,
946
1015
  } : {}),
1016
+ score_basis: hasCompletenessInventory ? 'model_declared_inventory' : 'area_statuses',
947
1017
  coverage_score: calculateCoverageScore({ areas, statuses, completenessInventory, hasCompletenessInventory }),
948
1018
  statuses,
949
1019
  coverage_levels,
@@ -951,6 +1021,82 @@ function buildCoverageProjection(blocks, options) {
951
1021
  };
952
1022
  }
953
1023
 
1024
+ function buildDocumentationPlanProjection(coverageProjection, sourceInventory, options = {}) {
1025
+ const generatedAt = options.generatedAt instanceof Date ? options.generatedAt : new Date();
1026
+ const inventoryIdsByArea = new Map();
1027
+ for (const item of coverageProjection.completeness_inventory ?? []) {
1028
+ for (const areaId of item.coverage_area_ids ?? []) {
1029
+ const ids = inventoryIdsByArea.get(areaId) ?? [];
1030
+ ids.push(item.id);
1031
+ inventoryIdsByArea.set(areaId, ids);
1032
+ }
1033
+ }
1034
+
1035
+ const items = coverageProjection.areas.map((area) => {
1036
+ const linkedInventoryIds = uniqueStrings([
1037
+ ...(area.linked_inventory_ids ?? []),
1038
+ ...(inventoryIdsByArea.get(area.id) ?? []),
1039
+ ]);
1040
+ return {
1041
+ id: area.id,
1042
+ title: area.title ?? buildCoverageAreaTitle(area),
1043
+ status: area.status,
1044
+ run_scope: area.run_scope ?? 'baseline',
1045
+ expected_coverage: area.coverage,
1046
+ ...(area.proposed_path ? { target_path: area.proposed_path } : {}),
1047
+ ...(area.purpose ? { purpose: area.purpose } : {}),
1048
+ ...(area.parent ? { parent: area.parent } : {}),
1049
+ ...(linkedInventoryIds.length > 0 ? { linked_inventory_ids: linkedInventoryIds } : {}),
1050
+ evidence: area.evidence ?? [],
1051
+ blindspots: area.blindspots ?? [],
1052
+ ...(area.anchor_action ? { anchor_action: area.anchor_action } : {}),
1053
+ ...(area.anchor_paths?.length > 0 ? { anchor_paths: area.anchor_paths } : {}),
1054
+ ...(area.anchor_reason ? { anchor_reason: area.anchor_reason } : {}),
1055
+ };
1056
+ });
1057
+
1058
+ return {
1059
+ version: DOCUMENTATION_PLAN_STATE_VERSION,
1060
+ producer: {
1061
+ package_version: PACKAGE_VERSION,
1062
+ prompt_version: DOCS_REVIEW_PROMPT_VERSION,
1063
+ parser_version: DOCS_REVIEW_PARSER_VERSION,
1064
+ documentation_plan_projection_version: DOCS_REVIEW_DOCUMENTATION_PLAN_PROJECTION_VERSION,
1065
+ coverage_projection_version: coverageProjection.version,
1066
+ ...(sourceInventory?.producer?.scanner_version
1067
+ ? { scanner_version: sourceInventory.producer.scanner_version }
1068
+ : {}),
1069
+ generated_at: generatedAt.toISOString(),
1070
+ },
1071
+ source: coverageProjection.source,
1072
+ summary: {
1073
+ item_count: items.length,
1074
+ by_status: countBy(items, 'status'),
1075
+ by_run_scope: countBy(items, 'run_scope'),
1076
+ linked_inventory_item_count: new Set(items.flatMap((item) => item.linked_inventory_ids ?? [])).size,
1077
+ unlinked_item_count: items.filter((item) => !item.linked_inventory_ids || item.linked_inventory_ids.length === 0).length,
1078
+ },
1079
+ ...(sourceInventory ? { source_inventory_summary: summarizeSourceInventory(sourceInventory) } : {}),
1080
+ items,
1081
+ };
1082
+ }
1083
+
1084
+ function summarizeDocumentationPlan(documentationPlan) {
1085
+ return {
1086
+ path: 'state/docs-review-documentation-plan.json',
1087
+ projection_version: documentationPlan.producer.documentation_plan_projection_version,
1088
+ item_count: documentationPlan.summary.item_count,
1089
+ by_status: documentationPlan.summary.by_status,
1090
+ by_run_scope: documentationPlan.summary.by_run_scope,
1091
+ linked_inventory_item_count: documentationPlan.summary.linked_inventory_item_count,
1092
+ unlinked_item_count: documentationPlan.summary.unlinked_item_count,
1093
+ };
1094
+ }
1095
+
1096
+ function buildCoverageAreaTitle(area) {
1097
+ return [area.domain, area.feature, area.component].filter(Boolean).join(' / ') || area.id;
1098
+ }
1099
+
954
1100
  function calculateCoverageScore({ areas, statuses, completenessInventory, hasCompletenessInventory }) {
955
1101
  if (hasCompletenessInventory) {
956
1102
  const denominator = completenessInventory.length;
@@ -989,10 +1135,21 @@ function normalizeCompletenessInventory(value, areaById) {
989
1135
  }
990
1136
 
991
1137
  const ids = new Set();
992
- return value.map((item, index) => normalizeCompletenessInventoryItem(item, index, ids, areaById));
1138
+ const unknownCoverageAreaReferences = [];
1139
+ const normalized = value.map((item, index) => normalizeCompletenessInventoryItem(
1140
+ item,
1141
+ index,
1142
+ ids,
1143
+ areaById,
1144
+ unknownCoverageAreaReferences,
1145
+ ));
1146
+ if (unknownCoverageAreaReferences.length > 0) {
1147
+ throw new Error(formatUnknownCoverageAreaReferences(unknownCoverageAreaReferences));
1148
+ }
1149
+ return normalized;
993
1150
  }
994
1151
 
995
- function normalizeCompletenessInventoryItem(item, index, ids, areaById) {
1152
+ function normalizeCompletenessInventoryItem(item, index, ids, areaById, unknownCoverageAreaReferences) {
996
1153
  if (!item || typeof item !== 'object' || Array.isArray(item)) {
997
1154
  throw new Error(`Coverage plan completeness_inventory item at index ${index} must be an object.`);
998
1155
  }
@@ -1014,7 +1171,7 @@ function normalizeCompletenessInventoryItem(item, index, ids, areaById) {
1014
1171
  const coverageAreaIds = normalizeStringArray(item.coverage_area_ids);
1015
1172
  for (const areaId of coverageAreaIds) {
1016
1173
  if (!areaById.has(areaId)) {
1017
- throw new Error(`Coverage plan completeness_inventory item "${id}" references unknown coverage area id: ${areaId}`);
1174
+ unknownCoverageAreaReferences.push({ inventoryId: id, areaId });
1018
1175
  }
1019
1176
  }
1020
1177
 
@@ -1029,6 +1186,13 @@ function normalizeCompletenessInventoryItem(item, index, ids, areaById) {
1029
1186
  };
1030
1187
  }
1031
1188
 
1189
+ function formatUnknownCoverageAreaReferences(references) {
1190
+ const formatted = references
1191
+ .map((reference) => `"${reference.inventoryId}" -> "${reference.areaId}"`)
1192
+ .join(', ');
1193
+ return `Coverage plan completeness_inventory references unknown coverage area ids: ${formatted}`;
1194
+ }
1195
+
1032
1196
  function normalizeCoverageArea(area, index, ids) {
1033
1197
  if (!area || typeof area !== 'object' || Array.isArray(area)) {
1034
1198
  throw new Error(`Coverage plan area at index ${index} must be an object.`);
@@ -1070,6 +1234,10 @@ function normalizeCoverageArea(area, index, ids) {
1070
1234
  if (anchorAction && !ANCHOR_ACTIONS.has(anchorAction)) {
1071
1235
  throw new Error(`Coverage plan area "${id}" has invalid anchor_action: ${anchorAction}`);
1072
1236
  }
1237
+ const runScope = normalizeOptionalString(area.run_scope);
1238
+ if (runScope && !DOCUMENTATION_PLAN_RUN_SCOPES.has(runScope)) {
1239
+ throw new Error(`Coverage plan area "${id}" has invalid run_scope: ${runScope}`);
1240
+ }
1073
1241
  const anchorPaths = normalizeStringArray(area.anchor_paths).map(normalizeBlockPath);
1074
1242
  for (const anchorPath of anchorPaths) {
1075
1243
  if (
@@ -1089,7 +1257,14 @@ function normalizeCoverageArea(area, index, ids) {
1089
1257
  component: normalizeOptionalString(area.component),
1090
1258
  status,
1091
1259
  coverage,
1260
+ ...(normalizeOptionalString(area.title) ? { title: normalizeOptionalString(area.title) } : {}),
1261
+ ...(normalizeOptionalString(area.purpose) ? { purpose: normalizeOptionalString(area.purpose) } : {}),
1262
+ ...(normalizeOptionalString(area.parent) ? { parent: normalizeOptionalString(area.parent) } : {}),
1263
+ ...(runScope ? { run_scope: runScope } : {}),
1092
1264
  ...(proposedPath ? { proposed_path: normalizeBlockPath(proposedPath) } : {}),
1265
+ ...(normalizeStringArray(area.linked_inventory_ids).length > 0
1266
+ ? { linked_inventory_ids: normalizeStringArray(area.linked_inventory_ids) }
1267
+ : {}),
1093
1268
  evidence: normalizeStringArray(area.evidence),
1094
1269
  blindspots: normalizeStringArray(area.blindspots),
1095
1270
  ...(normalizeOptionalString(area.reason) ? { reason: normalizeOptionalString(area.reason) } : {}),
@@ -1112,6 +1287,10 @@ function normalizeStringArray(value) {
1112
1287
  : [];
1113
1288
  }
1114
1289
 
1290
+ function uniqueStrings(values) {
1291
+ return [...new Set(values.filter((value) => typeof value === 'string' && value.trim() !== '').map((value) => value.trim()))];
1292
+ }
1293
+
1115
1294
  function normalizeBlockPath(value) {
1116
1295
  return String(value ?? '').trim().replaceAll('\\', '/');
1117
1296
  }
@@ -2033,6 +2212,55 @@ function renderAnchorContext(anchors) {
2033
2212
  return lines.join('\n');
2034
2213
  }
2035
2214
 
2215
+ function renderSourceInventoryContext(inventory) {
2216
+ const lines = [
2217
+ '## Package Source Inventory',
2218
+ 'Use this package-scanned inventory as the source-evidence denominator for coverage planning. Account for each scanned item as accepted, deferred, or missing in `completeness_inventory` when evidence is sufficient.',
2219
+ '',
2220
+ ];
2221
+
2222
+ if (!inventory) {
2223
+ lines.push('No package source inventory was available.');
2224
+ return lines.join('\n');
2225
+ }
2226
+
2227
+ const itemCount = Array.isArray(inventory.items) ? inventory.items.length : 0;
2228
+ lines.push(`Scanner: ${inventory.producer?.scanner_version ?? 'unknown'}; items=${itemCount}; warnings=${inventory.warnings?.length ?? 0}.`);
2229
+ if (Array.isArray(inventory.source_roots) && inventory.source_roots.length > 0) {
2230
+ lines.push('Source roots:');
2231
+ for (const sourceRoot of inventory.source_roots) {
2232
+ const details = [
2233
+ `kind=${sourceRoot.kind}`,
2234
+ `status=${sourceRoot.status}`,
2235
+ sourceRoot.repo_root_path ? `repo_root_path=${sourceRoot.repo_root_path}` : null,
2236
+ ].filter(Boolean).join('; ');
2237
+ lines.push(`- ${sourceRoot.repo_id}: ${details}`);
2238
+ }
2239
+ }
2240
+
2241
+ if (Array.isArray(inventory.warnings) && inventory.warnings.length > 0) {
2242
+ lines.push('Source inventory warnings:');
2243
+ for (const warning of inventory.warnings.slice(0, 20)) {
2244
+ lines.push(`- ${warning.code}: ${warning.message}`);
2245
+ }
2246
+ }
2247
+
2248
+ if (itemCount > 0) {
2249
+ lines.push('Scanned inventory items:');
2250
+ for (const item of inventory.items.slice(0, 80)) {
2251
+ const evidence = item.evidence?.slice(0, 3).map((entry) => entry.path).join(', ');
2252
+ lines.push(`- ${item.id} (${item.kind}, ${item.confidence}): ${item.label}${evidence ? `; evidence=${evidence}` : ''}`);
2253
+ }
2254
+ if (itemCount > 80) {
2255
+ lines.push(`- ... ${itemCount - 80} additional scanned inventory items omitted from prompt summary.`);
2256
+ }
2257
+ } else {
2258
+ lines.push('Scanned inventory items: none. If source roots are unavailable, say coverage is not source-backed for those repos.');
2259
+ }
2260
+
2261
+ return lines.join('\n');
2262
+ }
2263
+
2036
2264
  function renderReviewPrompt(options) {
2037
2265
  const outputPath = path.join(options.rootDir, 'state', 'docs-review-output.md');
2038
2266
  const applyCommand = `vibecompass docs-review --root ${shellQuote(options.rootDir)} --apply-output --output ${shellQuote(outputPath)}`;
@@ -2104,6 +2332,8 @@ function renderReviewPrompt(options) {
2104
2332
  '',
2105
2333
  renderAnchorContext(options.anchors),
2106
2334
  '',
2335
+ renderSourceInventoryContext(options.sourceInventory),
2336
+ '',
2107
2337
  '## Staged Review Protocol',
2108
2338
  'Stage 1 — Evidence inventory:',
2109
2339
  '- Build a compact repo inventory from file paths, manifests/config files, route/job/test directories, and existing architecture frontmatter before reading large file bodies.',
@@ -2117,12 +2347,16 @@ function renderReviewPrompt(options) {
2117
2347
  'Stage 2 — Coverage plan:',
2118
2348
  '- Propose the smallest useful architecture-doc set that will let future agents understand the project and retrieve targeted context.',
2119
2349
  '- The minimum useful set must cover: user journey, project/system map (frontend/backend/API/data/integration structure and connections), and summaries of the core journey-facing features.',
2350
+ '- Treat Stage 2 as a documentation-plan gate: each proposed doc needs a title, target path, purpose, parent/backbone grouping, linked inventory ids, evidence set, expected coverage level, prior-anchor action, and `baseline` or `deepening` run scope.',
2351
+ '- Keep baseline docs breadth-first and compact. Use `deepening` only for scoped follow-up docs that should not block the first project-understanding baseline.',
2120
2352
  '- Prioritize entry points, user-flow/gating relationships, data ownership, external integrations, jobs/async boundaries, auth/security boundaries, and test surfaces.',
2121
2353
  '- Account for each completeness-inventory item in the coverage plan as accepted, deferred, or missing. Do not inflate coverage by omitting large discovered subsystems; honest deferred/missing entries are expected when the first pass cannot cover everything.',
2354
+ '- Before generating prose, self-check that every `completeness_inventory[].coverage_area_ids[]` value points to an `areas[].id` in the same accepted plan. Fix all dangling ids together at the plan gate.',
2122
2355
  '- Choose one primary taxonomy axis from topology evidence and keep it stable: single-repo/monolith may default domain-first; multi-repo or multi-surface projects may use platform/repo-first or domain-first with per-surface components. Do not mix taxonomy axes at the same tier unless you explicitly propose and justify a restructuring.',
2123
2356
  '- For every prior architecture doc or project-map feature identity in Re-review Anchors, classify the plan action as `reuse`, `update`, `split`, `merge`, `defer`, or `replace`. Use `new` only for areas not represented by the prior tree.',
2124
2357
  '- Every `split`, `merge`, or `replace` action requires an evidence-backed anchor reason before prose generation. No unexplained slug rename is allowed.',
2125
2358
  '- Treat the accepted Stage-2 plan as the taxonomy and feature-list freeze for this docs-review run.',
2359
+ '- The user can revise the Stage-2 plan by merging, splitting, demoting, deferring, or requesting deepening before prose generation.',
2126
2360
  '- Mark each proposed doc as `comprehensive`, `partial`, or `initial`, with the reason and blindspots.',
2127
2361
  ...coveragePlanStageLines,
2128
2362
  '',
@@ -2133,6 +2367,7 @@ function renderReviewPrompt(options) {
2133
2367
  '- Keep docs concise and retrieval-oriented: explain contracts, flows, invariants, and ownership; do not rewrite source code or produce file-by-file walkthroughs.',
2134
2368
  `- Soft size budget: keep each generated architecture doc under ${ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES} bytes unless the extra detail is necessary and called out in Blindspots or Retrieval guidance.`,
2135
2369
  '- Include only the most important involved files; prefer representative entry points and owned contracts over exhaustive file lists.',
2370
+ '- Do not put transient apply-state instructions inside architecture docs, such as "this artifact has not been applied". Apply state belongs in the chat response and package marker, not canonical docs.',
2136
2371
  '',
2137
2372
  'Stage 4 — Apply and verify:',
2138
2373
  stage4WriteLine,
@@ -2222,7 +2457,7 @@ function renderReviewPrompt(options) {
2222
2457
  '- topology and chosen taxonomy axis',
2223
2458
  '- coverage plan and proposed architecture docs',
2224
2459
  '- prior-doc anchor classifications and reasons',
2225
- '- coverage/quality report, including missing/generic evidence, unmapped core features, oversized-doc candidates, and unresolved blindspots',
2460
+ '- coverage/quality report, including missing/generic evidence, unmapped core features, oversized-doc candidates, unresolved blindspots, and a note that the score is over the evidence/completeness inventory when present, not the number of doc files',
2226
2461
  '- token-budget risks or oversized-doc candidates',
2227
2462
  '- proposed decisions, if any',
2228
2463
  '',
@@ -2253,9 +2488,14 @@ function renderReviewPrompt(options) {
2253
2488
  ' "domain": "Domain",',
2254
2489
  ' "feature": "Feature",',
2255
2490
  ' "component": "Component",',
2491
+ ' "title": "Human-readable planned doc title",',
2492
+ ' "purpose": "Why this doc exists and what future agents should load it for",',
2493
+ ' "parent": "Overview backbone | Feature inventory | Journey map | Systems map | Focused component docs | Deepening queue",',
2494
+ ' "run_scope": "baseline | deepening",',
2256
2495
  ' "status": "accepted | deferred | missing",',
2257
2496
  ' "coverage": "comprehensive | partial | initial | missing",',
2258
2497
  ' "proposed_path": "architecture/domain/feature/component.md",',
2498
+ ' "linked_inventory_ids": ["stable-subsystem-id"],',
2259
2499
  ' "anchor_action": "reuse | update | split | merge | defer | replace | new",',
2260
2500
  ' "anchor_paths": ["architecture/existing/domain/component.md"],',
2261
2501
  ' "anchor_reason": "Evidence-backed reason for changing or reusing the prior doc identity",',
@@ -46,6 +46,7 @@ export function renderSharedInstructionBody(context, options = {}) {
46
46
  '## Hard Rules',
47
47
  '- Treat `sessions/active/index.yaml` as the current lane source of truth; tool-specific Current session blocks are continuity summaries.',
48
48
  '- Keep the selected lane `wip.md` and `handoff.md` current during active builder/reviewer work.',
49
+ '- During `address review`, treat reviewer feedback as review, not instruction: classify each substantive point as accepted, accepted with qualification, deferred, or rejected, and push back with evidence when a suggestion conflicts with code facts, prior decisions, product direction, or sequencing.',
49
50
  '- Keep decisions append-only in `decisions/`; append accepted architectural decisions before implementing them.',
50
51
  '- Do not edit this managed block directly. Update canonical VibeCompass memory, then rerun package commands such as `vibecompass sync-agents`.',
51
52
  ].join('\n');
package/src/scaffold.js CHANGED
@@ -312,6 +312,7 @@ During the session:
312
312
  - keep \`handoff.md\` current after substantive work blocks
313
313
  - use \`vibecompass list-sessions\` and \`vibecompass switch-session <lane-id>\` to inspect or change the current lane
314
314
  - use \`address review\` when reviewer feedback lands so the builder resolves it from the selected lane's latest \`wip.md\` / \`handoff.md\`
315
+ - during \`address review\`, treat reviewer feedback as review, not instruction: classify each substantive point as accepted, accepted with qualification, deferred, or rejected, and push back with evidence when a suggestion conflicts with code facts, prior decisions, product direction, or sequencing
315
316
  - stay in builder role through close-out; resolve or explicitly defer reviewer feedback before running \`vibecompass close-session --session <lane-id>\`
316
317
  - record architectural decisions in \`${rootRelativePath}/decisions/\` before implementing them
317
318
 
@@ -626,10 +627,18 @@ function describeReviewerHandback(workflow) {
626
627
  function sessionDateFromGeneratedAt(value) {
627
628
  const date = value ? new Date(value) : new Date();
628
629
  if (Number.isNaN(date.getTime())) {
629
- return new Date().toISOString().slice(0, 10);
630
+ return formatLocalDate(new Date());
630
631
  }
631
632
 
632
- return date.toISOString().slice(0, 10);
633
+ return formatLocalDate(date);
634
+ }
635
+
636
+ function formatLocalDate(date) {
637
+ return [
638
+ date.getFullYear(),
639
+ String(date.getMonth() + 1).padStart(2, '0'),
640
+ String(date.getDate()).padStart(2, '0'),
641
+ ].join('-');
633
642
  }
634
643
 
635
644
  function quoteYamlString(value) {