@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 +57 -0
- package/README.md +20 -0
- package/package.json +1 -1
- package/src/cli.js +200 -0
- package/src/docs-review.js +131 -22
- package/src/docs-update.js +15 -3
- package/src/index.js +8 -1
- package/src/mode-transition.js +265 -0
- package/src/project-memory.js +422 -0
- package/src/scaffold.js +7 -5
- package/src/serialization.js +26 -0
- package/src/status.js +78 -0
- package/src/sync.js +264 -13
- package/src/workflow.js +3 -0
- package/src/workflows/registry.js +1 -1
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
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]',
|
package/src/docs-review.js
CHANGED
|
@@ -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 {
|
|
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
|
|
24
|
-
const DOCS_REVIEW_PARSER_VERSION = 'docs-review-parser-
|
|
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
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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.',
|
package/src/docs-update.js
CHANGED
|
@@ -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('
|
|
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 {
|
|
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,
|