@vibecompass/vibecompass 0.11.2 → 0.13.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 CHANGED
@@ -2,6 +2,63 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.13.0 - 2026-07-11
6
+
7
+ - Architecture docs are now explicitly current-state contracts (D-292/D-293):
8
+ generated context/README/workflow guidance, the docs-update recommendations,
9
+ close-session guidance, and the docs-review prompt all state that updates
10
+ fold into existing sections in place — work/ship chronology goes to session
11
+ notes, decisions record accepted choices, and the doc keeps the durable
12
+ plan, unresolved next steps, and rollout state.
13
+ - New advisory changelog-shape detection: the shared scanner emits one
14
+ aggregated `architecture-changelog-smell` warning per doc (conjunctive
15
+ line-local signals; fenced/inline code, comments, and blockquotes masked),
16
+ and `docs-review --apply-output` emits the matching
17
+ `changelog_shaped_architecture_doc` warning. Optional frontmatter
18
+ `content_mode: chronological-ledger` suppresses only this advisory;
19
+ explicit unknown/non-string values warn. Advisory-only — never gates
20
+ close-session or read-model projection.
21
+ - `docs-update` affected docs now carry a self-describing
22
+ `size: { byteLength, softLimitBytes, exceedsSoftLimit }` advisory (the
23
+ 12000-byte soft budget is hoisted from docs-review into the shared
24
+ scanner); a session-scoped size advisory renders in the plan instead of a
25
+ standing status warning.
26
+ - Local docs-review prompt is v8; local parser is `docs-review-parser-v2`
27
+ (new warning codes). `detectChangelogShapedLines` and
28
+ `ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES` are exported as public API for
29
+ hosted-parser parity consumers.
30
+
31
+ ## 0.12.0 - 2026-07-10
32
+
33
+ - New `vibecompass bootstrap --bundle <file>` materializes a complete local
34
+ root from a hosted bootstrap-export bundle (fail-closed hash validation,
35
+ verbatim writes, sync-cursor seeding for local-primary bundles).
36
+ - New `vibecompass sync-adopt [--accept-divergence]` re-baselines a root's
37
+ sync cursor onto the hosted head after a divergence preview (D-215) — the
38
+ recovery path for fresh clones and second devices.
39
+ - New `vibecompass promote-hosted [--resume|--abort]` and
40
+ `vibecompass demote-hosted [--accept-divergence]` run the D-288 two-phase
41
+ hosting-mode cutover: fresh verified baseline, server-side transition
42
+ intent with a completeness report, local flip + promoted-root marker, and
43
+ confirm — resumable after any crash and fully reversible.
44
+ - Promoted roots hard-refuse canonical write commands (override with
45
+ VIBECOMPASS_ALLOW_PROMOTED_ROOT_WRITES=1); `vibecompass status` now checks
46
+ the hosted mode and warns on local/hosted mode mismatches.
47
+ - `apply-export` pre-validates every operation before writing and treats
48
+ already-applied operations as converged, so retries after partial failure
49
+ succeed. `docs-review --apply-decision-artifact` is idempotent and
50
+ crash-safe (provenance Source line, marker repair, post-write verification,
51
+ code-enforced append-only) and warns on hosted-only roots.
52
+
53
+ - The missing-sync-token error now explains how to recover: re-export the bound
54
+ env var, persist it in a shell profile, or rotate the token from the hosted
55
+ dashboard (previously a bare "requires VIBECOMPASS_SYNC_TOKEN" with no
56
+ guidance). README documents the persistence recipe.
57
+ - AGENTS.md gains a release checklist, including verifying that
58
+ `vibecompass-mcp`'s dependency range still spans the new version so core
59
+ releases stop stranding the MCP package (its `^0.1.0` pin had drifted 27
60
+ releases behind).
61
+
5
62
  ## 0.11.2 - 2026-07-06
6
63
 
7
64
  - Complete the remaining Phase 8 start-session overlap warnings: lanes can now
package/README.md CHANGED
@@ -250,6 +250,17 @@ changed canonical project-memory files and the hosted dashboard should catch
250
250
  up. Hosted-only projects do not use a local authoritative push; update or
251
251
  refresh them through the hosted dashboard/proposal flow.
252
252
 
253
+ The sync token is read from the environment variable you bind (for example
254
+ `VIBECOMPASS_SYNC_TOKEN`). One-off `export`s do not survive new terminals —
255
+ persist the token in your shell profile (`~/.zshenv` or `~/.bashrc`):
256
+
257
+ ```bash
258
+ echo 'export VIBECOMPASS_SYNC_TOKEN="<your sync token>"' >> ~/.zshenv
259
+ ```
260
+
261
+ If the token is lost, rotate it on the hosted dashboard under Setup -> Hosted
262
+ sync and export the new value.
263
+
253
264
  ### Multiple environments
254
265
 
255
266
  One project can have named sync targets (for example a local dev server and
@@ -283,12 +294,21 @@ import {
283
294
  scanProjectMemory,
284
295
  loadProjectReadModel,
285
296
  getProjectContext,
297
+ detectChangelogShapedLines,
298
+ ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES,
286
299
  } from "@vibecompass/vibecompass";
287
300
  ```
288
301
 
289
302
  Use the JavaScript API when building tools that need to inspect or manage a
290
303
  local VibeCompass project-memory root.
291
304
 
305
+ `detectChangelogShapedLines(body)` takes architecture-doc body text (frontmatter
306
+ already stripped) and returns the changelog-shaped lines it matched — the same
307
+ D-292 advisory detector the scanner and `docs-review --apply-output` use, shared
308
+ so external validators (for example the hosted proposal parser) stay in parity.
309
+ `ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES` is the byte-valued soft size budget
310
+ (12000) both docs-review apply warnings and the docs-update size advisory read.
311
+
292
312
  ## More Documentation
293
313
 
294
314
  - Developer guide: https://vibecompass.dev/developers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecompass/vibecompass",
3
- "version": "0.11.2",
3
+ "version": "0.13.0",
4
4
  "description": "Local-first project-memory core for VibeCompass",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -17,8 +17,11 @@ import { getProjectStatus, renderStatusText, toStatusJson } from './status.js';
17
17
  import { refreshWorkflow } from './refresh-workflow.js';
18
18
  import { inspectProjectCompatibility, formatCompatibilityWarnings } from './compatibility.js';
19
19
  import { PACKAGE_VERSION } from './version.js';
20
+ import { demoteHosted, promoteHosted } from './mode-transition.js';
20
21
  import {
22
+ adoptRemoteHead,
21
23
  applyPullExport,
24
+ bootstrapFromBundle,
22
25
  pullExportProjectMemory,
23
26
  pullPreviewProjectMemory,
24
27
  pushProjectMemory,
@@ -437,6 +440,71 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
437
440
  return 0;
438
441
  }
439
442
 
443
+ if (parsed.command === 'promote-hosted' || parsed.command === 'demote-hosted') {
444
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
445
+ const run = parsed.command === 'promote-hosted' ? promoteHosted : demoteHosted;
446
+ const result = await run(parsed.options, {
447
+ cwd: runtime.cwd,
448
+ env: runtime.env,
449
+ runtime,
450
+ });
451
+ io.stdout.write(`Mode transition: ${result.status}\n`);
452
+ if (result.completeness) {
453
+ io.stdout.write(`Carries over: ${result.completeness.carries_over}\n`);
454
+ for (const [kind, total] of Object.entries(result.completeness.documents_by_kind ?? {})) {
455
+ io.stdout.write(` ${kind}: ${total}\n`);
456
+ }
457
+ io.stdout.write('Does not carry over:\n');
458
+ for (const item of result.completeness.does_not_carry_over ?? []) {
459
+ io.stdout.write(` - ${item}\n`);
460
+ }
461
+ }
462
+ for (const warning of result.warnings) {
463
+ io.stdout.write(`Note: ${warning}\n`);
464
+ }
465
+ io.stdout.write(`${result.message}\n`);
466
+ return 0;
467
+ }
468
+
469
+ if (parsed.command === 'sync-adopt') {
470
+ await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
471
+ const result = await adoptRemoteHead(parsed.options, {
472
+ cwd: runtime.cwd,
473
+ env: runtime.env,
474
+ runtime,
475
+ });
476
+ io.stdout.write(`Sync adopt: ${result.status}\n`);
477
+ io.stdout.write(`Adopted head: ${result.remoteRevisionId}\n`);
478
+ io.stdout.write(`Recorded ${result.manifestPath}\n`);
479
+ for (const warning of result.warnings) {
480
+ io.stdout.write(`Warning: ${warning}\n`);
481
+ }
482
+ io.stdout.write(`${result.message}\n`);
483
+ return 0;
484
+ }
485
+
486
+ if (parsed.command === 'bootstrap') {
487
+ const result = await bootstrapFromBundle(parsed.options, {
488
+ cwd: runtime.cwd,
489
+ env: runtime.env,
490
+ runtime,
491
+ });
492
+ io.stdout.write(`Bootstrap: ${result.status}\n`);
493
+ io.stdout.write(`Root: ${result.rootDir}\n`);
494
+ io.stdout.write(`Documents: ${result.documentCount}\n`);
495
+ if (result.mode) {
496
+ io.stdout.write(`Mode: ${result.mode}\n`);
497
+ }
498
+ if (result.manifestPath) {
499
+ io.stdout.write(`Recorded ${result.manifestPath}\n`);
500
+ }
501
+ for (const warning of result.warnings) {
502
+ io.stdout.write(`Warning: ${warning}\n`);
503
+ }
504
+ io.stdout.write(`${result.message}\n`);
505
+ return 0;
506
+ }
507
+
440
508
  if (parsed.command === 'pull-preview') {
441
509
  await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
442
510
  const result = await pullPreviewProjectMemory(parsed.options, {
@@ -708,6 +776,18 @@ export function parseCliArgs(argv) {
708
776
  return parsePushArgs(rest);
709
777
  }
710
778
 
779
+ if (command === 'promote-hosted' || command === 'demote-hosted') {
780
+ return parseModeTransitionArgs(command, rest);
781
+ }
782
+
783
+ if (command === 'sync-adopt') {
784
+ return parseSyncAdoptArgs(rest);
785
+ }
786
+
787
+ if (command === 'bootstrap') {
788
+ return parseBootstrapArgs(rest);
789
+ }
790
+
711
791
  if (command === 'pull-preview') {
712
792
  return parsePullPreviewArgs(rest);
713
793
  }
@@ -1776,6 +1856,122 @@ function parseDocsReviewArgs(argv) {
1776
1856
  };
1777
1857
  }
1778
1858
 
1859
+ function parseModeTransitionArgs(command, argv) {
1860
+ const parsed = {};
1861
+
1862
+ for (let index = 0; index < argv.length; index += 1) {
1863
+ const token = argv[index];
1864
+ if (!token.startsWith('--')) {
1865
+ throw new Error(`Unexpected argument "${token}".`);
1866
+ }
1867
+
1868
+ if (token === '--resume') {
1869
+ parsed.resume = true;
1870
+ continue;
1871
+ }
1872
+ if (token === '--abort') {
1873
+ parsed.abort = true;
1874
+ continue;
1875
+ }
1876
+ if (token === '--accept-divergence') {
1877
+ parsed.acceptDivergence = true;
1878
+ continue;
1879
+ }
1880
+
1881
+ const value = argv[index + 1];
1882
+ if (value === undefined) {
1883
+ throw new Error(`Flag "${token}" requires a value.`);
1884
+ }
1885
+ index += 1;
1886
+
1887
+ switch (token) {
1888
+ case '--root':
1889
+ parsed.rootDir = value;
1890
+ break;
1891
+ case '--sync-target':
1892
+ parsed.syncTarget = value;
1893
+ break;
1894
+ default:
1895
+ throw new Error(`Unknown flag "${token}" for ${command}.`);
1896
+ }
1897
+ }
1898
+
1899
+ return { command, options: parsed };
1900
+ }
1901
+
1902
+ function parseSyncAdoptArgs(argv) {
1903
+ const parsed = {};
1904
+
1905
+ for (let index = 0; index < argv.length; index += 1) {
1906
+ const token = argv[index];
1907
+ if (!token.startsWith('--')) {
1908
+ throw new Error(`Unexpected argument "${token}".`);
1909
+ }
1910
+
1911
+ if (token === '--accept-divergence') {
1912
+ parsed.acceptDivergence = true;
1913
+ continue;
1914
+ }
1915
+
1916
+ const value = argv[index + 1];
1917
+ if (value === undefined) {
1918
+ throw new Error(`Flag "${token}" requires a value.`);
1919
+ }
1920
+ index += 1;
1921
+
1922
+ switch (token) {
1923
+ case '--root':
1924
+ parsed.rootDir = value;
1925
+ break;
1926
+ case '--sync-target':
1927
+ parsed.syncTarget = value;
1928
+ break;
1929
+ default:
1930
+ throw new Error(`Unknown flag "${token}" for sync-adopt.`);
1931
+ }
1932
+ }
1933
+
1934
+ return {
1935
+ command: 'sync-adopt',
1936
+ options: parsed,
1937
+ };
1938
+ }
1939
+
1940
+ function parseBootstrapArgs(argv) {
1941
+ const parsed = {};
1942
+
1943
+ for (let index = 0; index < argv.length; index += 1) {
1944
+ const token = argv[index];
1945
+ if (!token.startsWith('--')) {
1946
+ throw new Error(`Unexpected argument "${token}".`);
1947
+ }
1948
+ const value = argv[index + 1];
1949
+ if (value === undefined) {
1950
+ throw new Error(`Flag "${token}" requires a value.`);
1951
+ }
1952
+ index += 1;
1953
+
1954
+ switch (token) {
1955
+ case '--root':
1956
+ parsed.rootDir = value;
1957
+ break;
1958
+ case '--bundle':
1959
+ parsed.bundlePath = value;
1960
+ break;
1961
+ case '--sync-target':
1962
+ parsed.syncTarget = value;
1963
+ break;
1964
+ default:
1965
+ throw new Error(`Unknown flag "${token}" for bootstrap.`);
1966
+ }
1967
+ }
1968
+
1969
+ return {
1970
+ command: 'bootstrap',
1971
+ options: parsed,
1972
+ };
1973
+ }
1974
+
1779
1975
  function parsePushArgs(argv) {
1780
1976
  return {
1781
1977
  command: 'push',
@@ -2183,6 +2379,10 @@ function usageText() {
2183
2379
  ' vibecompass next-decision-id [options]',
2184
2380
  ' vibecompass sync-agents [options]',
2185
2381
  ' vibecompass push [options]',
2382
+ ' vibecompass bootstrap --bundle <file> [--root <dir>]',
2383
+ ' vibecompass sync-adopt [--accept-divergence] [options]',
2384
+ ' vibecompass promote-hosted [--resume|--abort] [options]',
2385
+ ' vibecompass demote-hosted [--accept-divergence] [options]',
2186
2386
  ' vibecompass pull-preview [options]',
2187
2387
  ' vibecompass pull-export [options]',
2188
2388
  ' vibecompass apply-export [options]',
@@ -4,7 +4,11 @@ import { createInterface } from 'node:readline/promises';
4
4
  import { parseFrontmatter } from './frontmatter.js';
5
5
  import { sha256Text } from './hash.js';
6
6
  import { writeStateManifest } from './manifest.js';
7
- import { scanProjectMemory } from './project-memory.js';
7
+ import {
8
+ ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES,
9
+ detectChangelogShapedLines,
10
+ scanProjectMemory,
11
+ } from './project-memory.js';
8
12
  import { parseSimpleYaml } from './simple-yaml.js';
9
13
  import {
10
14
  buildDocsReviewSourceInventory,
@@ -20,12 +24,11 @@ import { withMemoryRootLock } from './serialization.js';
20
24
  import { listDecisionFileNames, readNextDecisionId } from './decisions.js';
21
25
  import { looksLikeGroupedDecisionIndex } from './decision-index.js';
22
26
 
23
- const DOCS_REVIEW_PROMPT_VERSION = 'VibeCompass Docs Review Prompt v7';
24
- const DOCS_REVIEW_PARSER_VERSION = 'docs-review-parser-v1';
27
+ const DOCS_REVIEW_PROMPT_VERSION = 'VibeCompass Docs Review Prompt v8';
28
+ const DOCS_REVIEW_PARSER_VERSION = 'docs-review-parser-v2';
25
29
  const DOCS_REVIEW_ACCEPTED_OUTPUT_CONTRACT_VERSION = 'docs-review-accepted-output-v7';
26
30
  const DOCS_REVIEW_DOCUMENTATION_PLAN_PROJECTION_VERSION = 'docs-review-documentation-plan-v1';
27
31
  const DOCUMENTATION_PLAN_STATE_VERSION = 1;
28
- const ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES = 12000;
29
32
  const COVERAGE_PLAN_OUTPUT_VERSION = 1;
30
33
  const COVERAGE_PLAN_STATUSES = new Set(['accepted', 'deferred', 'missing']);
31
34
  const COVERAGE_LEVELS = new Set(['comprehensive', 'partial', 'initial', 'missing']);
@@ -434,6 +437,15 @@ async function applyDecisionArtifactLocked(options) {
434
437
  throw new Error(`Decision artifact ${artifactId} must include a non-empty rationale before it can be appended locally.`);
435
438
  }
436
439
 
440
+ // Idempotency guard: never append the same artifact twice. The marker is
441
+ // the fast path; the file-content check covers a crash between the file
442
+ // write and the marker update.
443
+ if (artifact.local_status === 'applied') {
444
+ throw new Error(
445
+ `Decision artifact ${artifactId} is already applied locally as ${artifact.local_decision_id ? `decision ${artifact.local_decision_id}` : 'a decision entry'}. Re-applying would append a duplicate entry.`,
446
+ );
447
+ }
448
+
437
449
  const nextDecisionId = await readNextDecisionId(options.rootDir);
438
450
  const targetFilePath = path.join(options.rootDir, targetPath);
439
451
  await mkdir(path.dirname(targetFilePath), { recursive: true });
@@ -446,27 +458,99 @@ async function applyDecisionArtifactLocked(options) {
446
458
  existingContent = `# ${path.basename(targetPath, '.md')} decisions\n`;
447
459
  }
448
460
 
449
- const entry = renderDecisionEntry({
450
- decisionId: nextDecisionId,
451
- title,
452
- decision,
453
- rationale,
454
- context,
455
- artifactId,
456
- });
457
- await writeFile(targetFilePath, `${existingContent.trimEnd()}\n\n${entry}`, 'utf8');
458
- const indexResult = options.refreshIndex
459
- ? await refreshDecisionIndex(options.rootDir)
460
- : {
461
+ const applyWarnings = [];
462
+ const alreadyInFile = existingContent.includes(
463
+ `hosted docs-review artifact ${artifactId}`,
464
+ );
465
+ let appliedDecisionIdForMarker = nextDecisionId;
466
+
467
+ if (alreadyInFile) {
468
+ // A previous apply wrote the file but crashed before recording the
469
+ // marker. Repair the marker instead of appending a duplicate.
470
+ applyWarnings.push(
471
+ `Artifact ${artifactId} was already present in ${targetPath}; repaired the marker without appending a duplicate entry.`,
472
+ );
473
+ const existingIdMatch = existingContent.match(
474
+ new RegExp(
475
+ `### (D-\\d+)[^\\n]*(?:\\n(?!### ).*)*hosted docs-review artifact ${artifactId.replaceAll(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}`,
476
+ ),
477
+ );
478
+ appliedDecisionIdForMarker = existingIdMatch
479
+ ? Number.parseInt(existingIdMatch[1].slice(2), 10)
480
+ : nextDecisionId;
481
+ } else {
482
+ const entry = renderDecisionEntry({
483
+ decisionId: nextDecisionId,
484
+ title,
485
+ decision,
486
+ rationale,
487
+ context,
488
+ artifactId,
489
+ });
490
+ const nextContent = `${existingContent.trimEnd()}\n\n${entry}`;
491
+ // Append-only enforced in code, not just convention: the previous file
492
+ // content must survive verbatim as a prefix of what we write.
493
+ if (!nextContent.startsWith(existingContent.trimEnd())) {
494
+ throw new Error(
495
+ `Refusing to apply artifact ${artifactId}: the write would not preserve the existing content of ${targetPath} (decisions are append-only).`,
496
+ );
497
+ }
498
+ await writeFile(targetFilePath, nextContent, 'utf8');
499
+
500
+ // Post-write verification: the canonical write must be durable and
501
+ // intact before the marker records the artifact as applied.
502
+ const verification = await readFile(targetFilePath, 'utf8');
503
+ if (verification !== nextContent) {
504
+ throw new Error(
505
+ `Post-write verification failed for ${targetPath}: the file on disk does not match the appended content. The marker was NOT updated; inspect the file and retry.`,
506
+ );
507
+ }
508
+ }
509
+
510
+ // Hosted-only roots can apply locally but can never push the result back
511
+ // until the project is demoted to local-primary — say so instead of letting
512
+ // the user discover it at push time.
513
+ try {
514
+ const projectConfig = parseSimpleYaml(
515
+ await readFile(path.join(options.rootDir, 'project.yaml'), 'utf8'),
516
+ { sourceName: 'project.yaml' },
517
+ );
518
+ if (projectConfig?.mode === 'hosted-only') {
519
+ applyWarnings.push(
520
+ 'This is a hosted-only project: the locally applied decision cannot be pushed back to the hosted canonical store. The hosted copy was already recorded when the artifact was accepted; treat this local root as a working copy.',
521
+ );
522
+ }
523
+ } catch {
524
+ // Unreadable project.yaml is surfaced by other commands; not fatal here.
525
+ }
526
+
527
+ let indexResult;
528
+ if (options.refreshIndex) {
529
+ try {
530
+ indexResult = await refreshDecisionIndex(options.rootDir);
531
+ } catch (error) {
532
+ // An index-refresh failure after a verified canonical write must not
533
+ // strand the marker (a retry would duplicate the entry): degrade to a
534
+ // warning and keep going.
535
+ indexResult = {
536
+ refreshed: false,
537
+ warnings: [
538
+ `decisions/INDEX.md refresh failed after the entry was appended: ${error.message}. Run vibecompass refresh-decision-index manually.`,
539
+ ],
540
+ };
541
+ }
542
+ } else {
543
+ indexResult = {
461
544
  refreshed: false,
462
545
  warnings: [
463
546
  'decisions/INDEX.md was not refreshed. Run again with --refresh-index if this root uses the package-generated flat index, or update the project-specific index manually.',
464
547
  ],
465
548
  };
549
+ }
466
550
 
467
551
  const appliedDecisionArtifact = {
468
552
  artifact_id: artifactId,
469
- decision_id: nextDecisionId,
553
+ decision_id: appliedDecisionIdForMarker,
470
554
  target_path: targetPath,
471
555
  applied_at: new Date().toISOString(),
472
556
  refreshed_index: indexResult.refreshed,
@@ -478,7 +562,7 @@ async function applyDecisionArtifactLocked(options) {
478
562
  ...current.hosted,
479
563
  artifacts: (current.hosted?.artifacts ?? []).map((item) =>
480
564
  item?.artifact_id === artifactId
481
- ? { ...item, local_status: 'applied', local_decision_id: nextDecisionId }
565
+ ? { ...item, local_status: 'applied', local_decision_id: appliedDecisionIdForMarker }
482
566
  : item,
483
567
  ),
484
568
  },
@@ -494,8 +578,10 @@ async function applyDecisionArtifactLocked(options) {
494
578
  hosted: current.hosted ?? null,
495
579
  appliedDecisionArtifact,
496
580
  reviewPrompt: null,
497
- warnings: indexResult.warnings,
498
- message: `Applied decision artifact ${artifactId} as D-${String(nextDecisionId).padStart(3, '0')} in ${targetPath}.`,
581
+ warnings: [...applyWarnings, ...indexResult.warnings],
582
+ message: alreadyInFile
583
+ ? `Repaired the marker for decision artifact ${artifactId} (already present in ${targetPath} as D-${String(appliedDecisionIdForMarker).padStart(3, '0')}).`
584
+ : `Applied decision artifact ${artifactId} as D-${String(appliedDecisionIdForMarker).padStart(3, '0')} in ${targetPath}.`,
499
585
  };
500
586
  }
501
587
 
@@ -1413,6 +1499,24 @@ function createArchitectureDocQualityWarnings(blocks) {
1413
1499
  ) {
1414
1500
  warnings.push(`missing_repo_scope: ${block.path} should declare repo or repos in frontmatter when implementation-scoped.`);
1415
1501
  }
1502
+
1503
+ let contentMode = null;
1504
+ if (Object.hasOwn(data, 'content_mode')) {
1505
+ const rawContentMode = data.content_mode;
1506
+ contentMode = typeof rawContentMode === 'string' ? rawContentMode.trim() : null;
1507
+ if (contentMode !== 'chronological-ledger') {
1508
+ const rendered = typeof rawContentMode === 'string' ? `"${rawContentMode}"` : JSON.stringify(rawContentMode) ?? 'undefined';
1509
+ warnings.push(`unknown_content_mode: ${block.path} declares content_mode ${rendered}; the recognized value is "chronological-ledger" (absence means current-state).`);
1510
+ contentMode = null;
1511
+ }
1512
+ }
1513
+ if (contentMode !== 'chronological-ledger') {
1514
+ const changelogLines = detectChangelogShapedLines(body);
1515
+ if (changelogLines.length > 0) {
1516
+ const example = changelogLines[0].length > 80 ? `${changelogLines[0].slice(0, 77)}...` : changelogLines[0];
1517
+ warnings.push(`changelog_shaped_architecture_doc: ${block.path} contains ${changelogLines.length} changelog-shaped line(s) (e.g. "${example}"); architecture docs are current-state contracts (D-292) — fold the content into the existing sections and move work/ship chronology to the session note; decisions record accepted choices (D-293). Intentionally dated ledgers/reports declare content_mode: chronological-ledger.`);
1518
+ }
1519
+ }
1416
1520
  }
1417
1521
 
1418
1522
  return warnings;
@@ -1972,6 +2076,10 @@ function renderDecisionEntry(options) {
1972
2076
  `**Decision:** ${options.decision}`,
1973
2077
  `**Rationale:** ${options.rationale}`,
1974
2078
  ...(options.context ? [`**Context:** ${options.context}`] : []),
2079
+ // Provenance makes re-application detectable from the file itself, so a
2080
+ // crash between the file write and the marker update can never cause a
2081
+ // silent duplicate append on retry.
2082
+ ...(options.artifactId ? [`**Source:** hosted docs-review artifact ${options.artifactId}`] : []),
1975
2083
  '',
1976
2084
  ].join('\n');
1977
2085
  }
@@ -2246,7 +2354,7 @@ function renderAnchorContext(anchors) {
2246
2354
  '',
2247
2355
  'Required prior-doc classifications for Stage 2:',
2248
2356
  '- `reuse`: keep path/frontmatter identity and update only if needed',
2249
- '- `update`: keep path/frontmatter identity but refresh content or coverage',
2357
+ '- `update`: keep path/frontmatter identity but refresh content or coverage by rewriting the affected sections in place — deepening or audit findings fold into the current contract, never an appended dated section',
2250
2358
  '- `split`: replace one broad prior doc with multiple focused docs',
2251
2359
  '- `merge`: combine overlapping prior docs',
2252
2360
  '- `defer`: leave prior area documented but out of the accepted generation scope',
@@ -2445,7 +2553,7 @@ function renderReviewPrompt(options) {
2445
2553
  '- Propose the smallest useful architecture-doc set that will let future agents understand the project and retrieve targeted context.',
2446
2554
  '- 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.',
2447
2555
  '- 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.',
2448
- '- Keep baseline docs breadth-first and compact. Use `deepening` only for scoped follow-up docs that should not block the first project-understanding baseline.',
2556
+ '- Keep baseline docs breadth-first and compact. Use `deepening` only for scoped follow-up docs that should not block the first project-understanding baseline. Deepening output still rewrites existing docs in place or creates new focused docs; it never appends dated audit/session sections to an existing doc.',
2449
2557
  '- Prioritize entry points, user-flow/gating relationships, data ownership, external integrations, jobs/async boundaries, auth/security boundaries, and test surfaces.',
2450
2558
  '- 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.',
2451
2559
  '- 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.',
@@ -2463,6 +2571,7 @@ function renderReviewPrompt(options) {
2463
2571
  '- Ensure `architecture/overview/project-shape.md` includes compact backbone sections for topology, core feature inventory, user journey map, project systems map, and coverage/quality summary.',
2464
2572
  '- Ensure `architecture/overview/project-shape.md` includes exactly one `vibecompass-project-map version=1` JSON fence that lists journey-facing features, supported feature relationships, and minimal optional derived `systems[]` metadata.',
2465
2573
  '- Keep docs concise and retrieval-oriented: explain contracts, flows, invariants, and ownership; do not rewrite source code or produce file-by-file walkthroughs.',
2574
+ '- Write every doc as a current-state contract (D-292): when updating an existing doc, rewrite the affected sections in place or propose a new focused doc — never append dated session/audit sections (for example "Audit deepening — YYYY-MM-DD"). Keep the durable plan, unresolved next steps, and material rollout state in the doc; work/ship chronology belongs in session notes, and decision entries record accepted choices (D-293). Intentionally dated ledgers/reports declare frontmatter `content_mode: chronological-ledger`.',
2466
2575
  `- 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.`,
2467
2576
  '- Include only the most important involved files; prefer representative entry points and owned contracts over exhaustive file lists.',
2468
2577
  '- 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.',
@@ -5,7 +5,7 @@ import { promisify } from 'node:util';
5
5
  import { parseFrontmatter } from './frontmatter.js';
6
6
  import { readCurrentRepoHead, resolveRepoSourceDir } from './git-binding.js';
7
7
  import { resolveLaneMarkerContext, resolveLaneSelection } from './lane-marker.js';
8
- import { scanProjectMemory } from './project-memory.js';
8
+ import { ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES, scanProjectMemory } from './project-memory.js';
9
9
  import { parseSimpleYaml } from './simple-yaml.js';
10
10
 
11
11
  const execFileAsync = promisify(execFile);
@@ -140,6 +140,9 @@ export function renderDocsUpdatePlan(plan) {
140
140
  if (doc.qualityWarnings.length > 0) {
141
141
  lines.push(` - quality warnings: ${doc.qualityWarnings.map((warning) => warning.code).join(', ')}`);
142
142
  }
143
+ if (doc.size?.exceedsSoftLimit) {
144
+ lines.push(` - size advisory: ${doc.size.byteLength} bytes exceeds the ${doc.size.softLimitBytes}-byte soft budget; this session is about to edit the doc — split it into focused component docs or trim low-value detail while folding in changes.`);
145
+ }
143
146
  }
144
147
  }
145
148
 
@@ -817,6 +820,7 @@ function summarizeArchitectureDocument(document) {
817
820
  feature: normalizeOptionalString(data.feature),
818
821
  component: normalizeOptionalString(data.component),
819
822
  involvedFiles: extractInvolvedFiles(frontmatter.body),
823
+ byteLength: document.byteLength,
820
824
  warnings: document.warnings,
821
825
  };
822
826
  }
@@ -853,6 +857,14 @@ function findAffectedArchitectureDocs(docs, delta) {
853
857
  path: doc.path,
854
858
  reasons: Array.from(new Set(reasons)),
855
859
  qualityWarnings: doc.warnings,
860
+ // Session-scoped size advisory (D-292): surfaced only for docs the
861
+ // session is about to touch, deliberately not a standing status
862
+ // check. Self-describing so every renderer reads the same values.
863
+ size: {
864
+ byteLength: typeof doc.byteLength === 'number' ? doc.byteLength : null,
865
+ softLimitBytes: ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES,
866
+ exceedsSoftLimit: typeof doc.byteLength === 'number' && doc.byteLength > ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES,
867
+ },
856
868
  };
857
869
  })
858
870
  .filter((doc) => doc.reasons.length > 0)
@@ -906,9 +918,9 @@ function buildRecommendations(options) {
906
918
  const recommendations = [];
907
919
 
908
920
  if (options.affectedArchitectureDocs.length > 0) {
909
- recommendations.push('Review and update the affected architecture docs listed above, then close with --architecture-docs updated.');
921
+ recommendations.push('Fold the session changes into the affected architecture docs as current-state contracts — rewrite sections in place, keep the durable plan/next steps/rollout state in the doc, and move work/ship chronology to the session note (D-292, D-293) — then close with --architecture-docs updated.');
910
922
  } else if (options.changedFiles.length > 0 && options.affectedArchitectureDocs.length === 0) {
911
- recommendations.push('No matching architecture doc was found for the session delta; create a focused doc, mark docs not-needed with evidence, or defer explicitly.');
923
+ recommendations.push('No matching architecture doc was found for the session delta; create a focused doc written as a current-state contract, mark docs not-needed with evidence, or defer explicitly.');
912
924
  } else {
913
925
  recommendations.push('No architecture doc update is indicated by the current delta; close with --architecture-docs not-needed if that remains true.');
914
926
  }
package/src/index.js CHANGED
@@ -6,13 +6,20 @@ export { getProjectStatus, renderStatusText, toStatusJson } from './status.js';
6
6
  export { initializeProjectMemory } from './init.js';
7
7
  export { preflightDocsReview } from './docs-review.js';
8
8
  export { planDocsUpdate, renderDocsUpdatePlan } from './docs-update.js';
9
+ export { demoteHosted, promoteHosted } from './mode-transition.js';
9
10
  export {
11
+ adoptRemoteHead,
10
12
  applyPullExport,
13
+ bootstrapFromBundle,
11
14
  pullExportProjectMemory,
12
15
  pullPreviewProjectMemory,
13
16
  pushProjectMemory,
14
17
  } from './sync.js';
15
- export { scanProjectMemory } from './project-memory.js';
18
+ export {
19
+ ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES,
20
+ detectChangelogShapedLines,
21
+ scanProjectMemory,
22
+ } from './project-memory.js';
16
23
  export {
17
24
  closeProjectSession,
18
25
  listProjectSessions,