@vibecompass/vibecompass 0.17.1 → 0.17.2
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 +38 -0
- package/README.md +3 -0
- package/package.json +1 -1
- package/src/cli.js +2 -2
- package/src/decision-lineage.js +9 -2
- package/src/generators/agent-files/template.js +4 -2
- package/src/init.js +9 -6
- package/src/markdown-sections.js +1 -1
- package/src/project-memory.js +5 -5
- package/src/scaffold.js +8 -2
- package/src/serialization.js +88 -9
- package/src/session.js +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.17.2 - 2026-10-12
|
|
4
|
+
|
|
5
|
+
Lockstep release with the app's lineage port: section headings open a fence
|
|
6
|
+
only where masking does, commands leave nothing behind on an uninitialized root,
|
|
7
|
+
agent files show a repo's remote beside its local path, `init` ignores lane
|
|
8
|
+
scratch, and the scanner's section warnings read headings fence-aware. The
|
|
9
|
+
lane brief stays opt-in (D-368).
|
|
10
|
+
|
|
11
|
+
- Lineage: section headings in architecture docs and session notes now open a
|
|
12
|
+
fence only where masking does. A backtick fence's info string holds no
|
|
13
|
+
backtick, so a prose line that starts with backticks, such as
|
|
14
|
+
```` ```a`b ````, no longer hides every later heading of the file. Before,
|
|
15
|
+
the decisions listed under a later `## Decisions made` heading were missed as
|
|
16
|
+
`made` relations, and later citations were filed under the wrong section.
|
|
17
|
+
Tilde fences may still hold backticks in their info string. A shared golden
|
|
18
|
+
fixture pair (`note-fence-opener`, `doc-fence-opener`) pins the rule for the
|
|
19
|
+
app port.
|
|
20
|
+
- Commands no longer leave directories behind on an uninitialized root. A
|
|
21
|
+
write command run against a missing root (or a folder holding no project
|
|
22
|
+
memory) left an empty `state/` directory, and the root folder itself, behind
|
|
23
|
+
before it failed. The cross-process lock still serializes every command
|
|
24
|
+
there, since `bootstrap` and `apply-export` from an external bundle can
|
|
25
|
+
create a root, but at release it removes the directories it made while they
|
|
26
|
+
are still empty. Session commands report the missing root instead of a
|
|
27
|
+
missing `project.yaml`.
|
|
28
|
+
- Agent files and `context.md` show a Git repo's remote when `project.yaml` also
|
|
29
|
+
records its local `path:`, as `<remote> (local path <path>)`. Before, any repo
|
|
30
|
+
with a path read as a local folder. Local-folder sources and repos without a
|
|
31
|
+
path read as before; run `vibecompass sync-agents` to refresh managed blocks.
|
|
32
|
+
- `init` adds `sessions/active/` to the root's `.gitignore` beside `state/`, so
|
|
33
|
+
a versioned memory root never commits local-only lane scratch (D-278). It
|
|
34
|
+
appends only the entries a `.gitignore` lacks. An existing root's
|
|
35
|
+
`.gitignore` is not changed; add the line yourself.
|
|
36
|
+
- Scanner: the recommended-section warnings for architecture docs and session
|
|
37
|
+
notes ignore headings inside fenced code, with the read model's fence-aware
|
|
38
|
+
reader. A fenced example `## Description` no longer hides a missing real
|
|
39
|
+
section.
|
|
40
|
+
|
|
3
41
|
## 0.17.1 - 2026-10-07
|
|
4
42
|
|
|
5
43
|
Hardening release: decision lineage fails soft, the read model reads sections
|
package/README.md
CHANGED
|
@@ -230,6 +230,9 @@ npx -y @vibecompass/vibecompass@latest docs-review --root .compass --rebuild --a
|
|
|
230
230
|
|
|
231
231
|
Docs review writes derived state under `state/`, including scanned source
|
|
232
232
|
inventory, accepted coverage, and the accepted documentation-plan projection.
|
|
233
|
+
`init` adds `state/` and `sessions/active/` (local-only lane scratch) to the
|
|
234
|
+
root's `.gitignore`. A command that fails in a folder that is not a project
|
|
235
|
+
memory root yet leaves nothing behind there.
|
|
233
236
|
|
|
234
237
|
Generated agent files can also include managed VibeCompass blocks:
|
|
235
238
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -74,9 +74,9 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
|
|
|
74
74
|
}
|
|
75
75
|
io.stdout.write(`Wrote ${result.projectFilePath}\n`);
|
|
76
76
|
if (result.gitignoreUpdated) {
|
|
77
|
-
io.stdout.write(`Updated ${result.gitignorePath} to ignore state/\n`);
|
|
77
|
+
io.stdout.write(`Updated ${result.gitignorePath} to ignore state/ and sessions/active/\n`);
|
|
78
78
|
} else {
|
|
79
|
-
io.stdout.write(`${result.gitignorePath} already ignored state/\n`);
|
|
79
|
+
io.stdout.write(`${result.gitignorePath} already ignored state/ and sessions/active/\n`);
|
|
80
80
|
}
|
|
81
81
|
io.stdout.write(
|
|
82
82
|
`Generated ${result.manifestPath} (${result.manifest.canonical.document_count} canonical docs, ${result.manifest.canonical.warning_count} warnings)\n`,
|
package/src/decision-lineage.js
CHANGED
|
@@ -1577,7 +1577,7 @@ export function findFencedRanges(text) {
|
|
|
1577
1577
|
}
|
|
1578
1578
|
continue;
|
|
1579
1579
|
}
|
|
1580
|
-
if (
|
|
1580
|
+
if (opensFence(marker, line)) {
|
|
1581
1581
|
fence = { char: marker[1][0], length: marker[1].length, start: lineStart };
|
|
1582
1582
|
}
|
|
1583
1583
|
}
|
|
@@ -1585,6 +1585,13 @@ export function findFencedRanges(text) {
|
|
|
1585
1585
|
return ranges;
|
|
1586
1586
|
}
|
|
1587
1587
|
|
|
1588
|
+
// A fence opens on three or more backticks or tildes indented at most three
|
|
1589
|
+
// spaces; a backtick opener's info string holds no backtick, so ```a`b is
|
|
1590
|
+
// prose. Shared by masking and the section index.
|
|
1591
|
+
function opensFence(marker, line) {
|
|
1592
|
+
return Boolean(marker) && !(marker[1][0] === '`' && line.slice(line.indexOf(marker[1]) + marker[1].length).includes('`'));
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1588
1595
|
// A closing fence holds only the opener's character, at least as many times,
|
|
1589
1596
|
// so ```~~~ never closes. Shared by masking and the section index.
|
|
1590
1597
|
function closesFence(fence, marker, line) {
|
|
@@ -1686,7 +1693,7 @@ function buildSectionIndex(content, bodyStart) {
|
|
|
1686
1693
|
}
|
|
1687
1694
|
continue;
|
|
1688
1695
|
}
|
|
1689
|
-
if (fenceMatch) {
|
|
1696
|
+
if (opensFence(fenceMatch, line)) {
|
|
1690
1697
|
fence = { char: fenceMatch[1][0], length: fenceMatch[1].length };
|
|
1691
1698
|
continue;
|
|
1692
1699
|
}
|
|
@@ -88,13 +88,15 @@ function renderProjectSnapshot(context) {
|
|
|
88
88
|
return lines.length > 0 ? lines : ['- No project details recorded yet.'];
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
// A local-folder source shows its folder; a Git repo shows its remote, plus
|
|
92
|
+
// the local path where one is recorded beside it (PC2).
|
|
91
93
|
function formatRepoDescriptor(repo) {
|
|
92
|
-
if (repo?.source === 'local' || repo?.path) {
|
|
94
|
+
if (repo?.source === 'local' || (repo?.path && !repo?.remote)) {
|
|
93
95
|
return `local folder ${repo.path ?? '.'}`;
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
if (repo?.remote) {
|
|
97
|
-
return repo.remote;
|
|
99
|
+
return repo.path ? `${repo.remote} (local path ${repo.path})` : repo.remote;
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
return 'source not recorded';
|
package/src/init.js
CHANGED
|
@@ -34,7 +34,7 @@ export async function initializeProjectMemory(options) {
|
|
|
34
34
|
const gitignorePath = path.join(normalized.rootDir, '.gitignore');
|
|
35
35
|
|
|
36
36
|
await writeProjectFile(projectFilePath, normalized.projectConfig, normalized.force);
|
|
37
|
-
const gitignoreUpdated = await
|
|
37
|
+
const gitignoreUpdated = await ensureLocalOnlyPathsIgnored(gitignorePath);
|
|
38
38
|
const scaffoldResult = await scaffoldInitFiles(normalized.bootstrap);
|
|
39
39
|
const manifestResult = await writeStateManifest(normalized.rootDir, {
|
|
40
40
|
generatedAt: normalized.generatedAt,
|
|
@@ -461,22 +461,25 @@ async function writeProjectFile(projectFilePath, projectConfig, force) {
|
|
|
461
461
|
await writeFile(projectFilePath, source, 'utf8');
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
-
|
|
465
|
-
|
|
464
|
+
// Local-only paths a versioned memory root never commits: derived state, and
|
|
465
|
+
// lane scratch under sessions/active/ (D-278; PC5).
|
|
466
|
+
const LOCAL_ONLY_IGNORE_ENTRIES = ['state/', 'sessions/active/'];
|
|
466
467
|
|
|
468
|
+
async function ensureLocalOnlyPathsIgnored(gitignorePath) {
|
|
467
469
|
if (!(await fileExists(gitignorePath))) {
|
|
468
|
-
await writeFile(gitignorePath, `${
|
|
470
|
+
await writeFile(gitignorePath, `${LOCAL_ONLY_IGNORE_ENTRIES.join('\n')}\n`, 'utf8');
|
|
469
471
|
return true;
|
|
470
472
|
}
|
|
471
473
|
|
|
472
474
|
const current = await readFile(gitignorePath, 'utf8');
|
|
473
475
|
const entries = current.split(/\r?\n/).map((line) => line.trim());
|
|
474
|
-
|
|
476
|
+
const missing = LOCAL_ONLY_IGNORE_ENTRIES.filter((entry) => !entries.includes(entry));
|
|
477
|
+
if (missing.length === 0) {
|
|
475
478
|
return false;
|
|
476
479
|
}
|
|
477
480
|
|
|
478
481
|
const separator = current.length > 0 && !current.endsWith('\n') ? '\n' : '';
|
|
479
|
-
await writeFile(gitignorePath, `${current}${separator}${
|
|
482
|
+
await writeFile(gitignorePath, `${current}${separator}${missing.join('\n')}\n`, 'utf8');
|
|
480
483
|
return true;
|
|
481
484
|
}
|
|
482
485
|
|
package/src/markdown-sections.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The one markdown section reader shared by the read model's component fields
|
|
3
3
|
* (D-372) and the session brief (D-359): fence-aware, first section of a title
|
|
4
4
|
* wins. The lineage extractor keeps its own section index
|
|
5
|
-
* (`decision-lineage.js`),
|
|
5
|
+
* (`decision-lineage.js`), which follows the same opening and closing rules.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
/**
|
package/src/project-memory.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { validateBriefSettings } from './brief-settings.js';
|
|
4
4
|
import { parseFrontmatter } from './frontmatter.js';
|
|
5
5
|
import { sha256Text } from './hash.js';
|
|
6
|
+
import { splitSections } from './markdown-sections.js';
|
|
6
7
|
import { parseSimpleYaml } from './simple-yaml.js';
|
|
7
8
|
|
|
8
9
|
const PROJECT_FILE = 'project.yaml';
|
|
@@ -886,12 +887,11 @@ function findDuplicateDecisionErrors(documents) {
|
|
|
886
887
|
return errors;
|
|
887
888
|
}
|
|
888
889
|
|
|
890
|
+
// Level-2 section titles, lowercased, read fence-aware with the section reader
|
|
891
|
+
// the read model and the brief share (D-372; H10): a heading inside fenced
|
|
892
|
+
// code is an example, so it never satisfies a recommended section.
|
|
889
893
|
function extractLevelTwoSections(content) {
|
|
890
|
-
|
|
891
|
-
for (const match of content.matchAll(/^##\s+(.+)$/gm)) {
|
|
892
|
-
sections.add(match[1].trim().toLowerCase());
|
|
893
|
-
}
|
|
894
|
-
return sections;
|
|
894
|
+
return new Set([...splitSections(content, 2).keys()].map((title) => title.trim().toLowerCase()));
|
|
895
895
|
}
|
|
896
896
|
|
|
897
897
|
// D-292 changelog-shape detector, shared by the scanner and the docs-review
|
package/src/scaffold.js
CHANGED
|
@@ -503,12 +503,18 @@ ${repoLines}
|
|
|
503
503
|
`;
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
+
// A local-folder source shows its folder; a Git repo shows its remote, plus
|
|
507
|
+
// the local path where one is recorded beside it (PC2).
|
|
506
508
|
function formatRepoDescriptor(repo) {
|
|
507
|
-
if (repo.source === 'local' || repo.path) {
|
|
509
|
+
if (repo.source === 'local' || (repo.path && !repo.remote)) {
|
|
508
510
|
return `local folder ${repo.path}`;
|
|
509
511
|
}
|
|
510
512
|
|
|
511
|
-
|
|
513
|
+
if (repo.remote) {
|
|
514
|
+
return repo.path ? `${repo.remote} (local path ${repo.path})` : repo.remote;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return 'source not recorded';
|
|
512
518
|
}
|
|
513
519
|
|
|
514
520
|
function generateArchitectureGuide(projectConfig) {
|
package/src/serialization.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { mkdir, readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { mkdir, readdir, readFile, realpath, rename, rm, rmdir, stat, writeFile } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -16,6 +16,15 @@ import path from 'node:path';
|
|
|
16
16
|
* re-enter instead of deadlocking (e.g. close-session refreshing the
|
|
17
17
|
* manifest, which is itself lock-wrapped)
|
|
18
18
|
*
|
|
19
|
+
* Every command takes the disk lock, on an uninitialized root too: a locked
|
|
20
|
+
* command can create a root there (bootstrap, or apply-export from an external
|
|
21
|
+
* bundle), so skipping the lock would let two of them validate the same
|
|
22
|
+
* missing files and both write. The lock leaves nothing behind (PC1): at
|
|
23
|
+
* release it removes the directories it created to hold itself (state/, and
|
|
24
|
+
* the root when it was missing) while they are still empty, and a root the
|
|
25
|
+
* lock created still counts as missing to the command
|
|
26
|
+
* (`memoryRootCreatedByLock`).
|
|
27
|
+
*
|
|
19
28
|
* This tier is intentionally local-only. Cross-machine serialization belongs
|
|
20
29
|
* to git merge (local-primary teams) or the hosted database (hosted-only);
|
|
21
30
|
* no distributed locking is ever built on top of this file.
|
|
@@ -28,6 +37,7 @@ const DEFAULT_ATTEMPTS = 120;
|
|
|
28
37
|
const RETRY_BASE_DELAY_MS = 25;
|
|
29
38
|
const RETRY_MAX_DELAY_MS = 100;
|
|
30
39
|
|
|
40
|
+
// Held root key -> { createdRoot }: whether this lock made the root directory.
|
|
31
41
|
const heldRoots = new AsyncLocalStorage();
|
|
32
42
|
const processQueues = new Map();
|
|
33
43
|
|
|
@@ -88,13 +98,14 @@ export async function withMemoryRootLock(rootDir, label, fn, options = {}) {
|
|
|
88
98
|
await previous;
|
|
89
99
|
|
|
90
100
|
try {
|
|
91
|
-
const
|
|
101
|
+
const lock = await acquireDiskLock(key, label, options);
|
|
92
102
|
try {
|
|
93
|
-
const nextHeld = new
|
|
94
|
-
nextHeld.
|
|
103
|
+
const nextHeld = new Map(held ?? []);
|
|
104
|
+
nextHeld.set(key, { createdRoot: lock.createdRoot });
|
|
95
105
|
return await heldRoots.run(nextHeld, fn);
|
|
96
106
|
} finally {
|
|
97
|
-
await releaseDiskLock(key, ownerToken);
|
|
107
|
+
await releaseDiskLock(key, lock.ownerToken);
|
|
108
|
+
await removeLockCreatedDirectories(key, lock.created);
|
|
98
109
|
}
|
|
99
110
|
} finally {
|
|
100
111
|
releaseQueue();
|
|
@@ -104,6 +115,57 @@ export async function withMemoryRootLock(rootDir, label, fn, options = {}) {
|
|
|
104
115
|
}
|
|
105
116
|
}
|
|
106
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Whether `rootDir` exists only because the lock this caller holds made it
|
|
120
|
+
* (PC1): the root was missing when the command started and still holds
|
|
121
|
+
* nothing but the lock, so a command that requires an existing root reports
|
|
122
|
+
* it missing instead of reading the lock's own directory as one. Once the
|
|
123
|
+
* command writes into it (bootstrap), it is a root in its own right. False
|
|
124
|
+
* outside a lock holder.
|
|
125
|
+
*/
|
|
126
|
+
export async function memoryRootCreatedByLock(rootDir) {
|
|
127
|
+
const held = heldRoots.getStore();
|
|
128
|
+
if (!held) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
const heldKey = await findHeldMemoryRootKey(held, await canonicalizeMemoryRootKey(rootDir));
|
|
132
|
+
if (!heldKey || !held.get(heldKey).createdRoot) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const rootEntries = await readdir(heldKey);
|
|
137
|
+
const stateEntries = await readdir(path.dirname(memoryRootLockPath(heldKey)));
|
|
138
|
+
return rootEntries.every((entry) => entry === 'state') && stateEntries.every((entry) => entry.startsWith(LOCK_DIR_NAME));
|
|
139
|
+
} catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Removes the directories acquisition created (state/, then the root and any
|
|
146
|
+
* missing parents up to `created`) while each is still empty (PC1). A command
|
|
147
|
+
* that wrote memory there, or another holder's lock, keeps a directory
|
|
148
|
+
* non-empty, so it stays. Two commands racing on the same missing root can
|
|
149
|
+
* leave an empty directory when the second found it already made; it never
|
|
150
|
+
* holds memory.
|
|
151
|
+
*/
|
|
152
|
+
async function removeLockCreatedDirectories(key, created) {
|
|
153
|
+
if (!created) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const top = path.resolve(created);
|
|
157
|
+
for (let dir = path.dirname(memoryRootLockPath(key)); ; dir = path.dirname(dir)) {
|
|
158
|
+
try {
|
|
159
|
+
await rmdir(dir);
|
|
160
|
+
} catch {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (dir === top || path.dirname(dir) === dir) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
107
169
|
async function canonicalizeMemoryRootKey(rootDir) {
|
|
108
170
|
const resolved = path.resolve(rootDir);
|
|
109
171
|
try {
|
|
@@ -126,7 +188,7 @@ async function findHeldMemoryRootKey(held, key) {
|
|
|
126
188
|
return key;
|
|
127
189
|
}
|
|
128
190
|
|
|
129
|
-
for (const heldKey of held) {
|
|
191
|
+
for (const heldKey of held.keys()) {
|
|
130
192
|
if (await memoryRootKeysReferToSamePath(heldKey, key)) {
|
|
131
193
|
return heldKey;
|
|
132
194
|
}
|
|
@@ -145,9 +207,11 @@ async function memoryRootKeysReferToSamePath(left, right) {
|
|
|
145
207
|
|
|
146
208
|
async function acquireDiskLock(key, label, options) {
|
|
147
209
|
const lockDir = memoryRootLockPath(key);
|
|
210
|
+
const stateDir = path.dirname(lockDir);
|
|
148
211
|
const attempts = options.attempts ?? DEFAULT_ATTEMPTS;
|
|
149
212
|
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
150
|
-
|
|
213
|
+
// The topmost directory this acquisition created, if any (mkdir reports it).
|
|
214
|
+
let created = await mkdir(stateDir, { recursive: true });
|
|
151
215
|
|
|
152
216
|
for (let attempt = 1; ; attempt += 1) {
|
|
153
217
|
const ownerToken = randomUUID();
|
|
@@ -158,9 +222,17 @@ async function acquireDiskLock(key, label, options) {
|
|
|
158
222
|
`${JSON.stringify({ pid: process.pid, token: ownerToken, label, acquired_at: new Date().toISOString() })}\n`,
|
|
159
223
|
'utf8',
|
|
160
224
|
);
|
|
161
|
-
return
|
|
225
|
+
return {
|
|
226
|
+
ownerToken,
|
|
227
|
+
created,
|
|
228
|
+
createdRoot: Boolean(created) && path.resolve(created) !== path.resolve(stateDir),
|
|
229
|
+
};
|
|
162
230
|
} catch (error) {
|
|
163
|
-
if (error?.code
|
|
231
|
+
if (error?.code === 'ENOENT') {
|
|
232
|
+
// A releasing holder removed the state/ (or root) it had created; make
|
|
233
|
+
// it again, and remove it at release in turn.
|
|
234
|
+
created = topmostDirectory(created, await mkdir(stateDir, { recursive: true }));
|
|
235
|
+
} else if (error?.code !== 'EEXIST') {
|
|
164
236
|
throw error;
|
|
165
237
|
}
|
|
166
238
|
}
|
|
@@ -186,6 +258,13 @@ async function acquireDiskLock(key, label, options) {
|
|
|
186
258
|
}
|
|
187
259
|
}
|
|
188
260
|
|
|
261
|
+
function topmostDirectory(left, right) {
|
|
262
|
+
if (!left || !right) {
|
|
263
|
+
return left ?? right;
|
|
264
|
+
}
|
|
265
|
+
return path.resolve(left).length <= path.resolve(right).length ? left : right;
|
|
266
|
+
}
|
|
267
|
+
|
|
189
268
|
/**
|
|
190
269
|
* A lock is abandoned only when its owner process is provably gone (dead PID),
|
|
191
270
|
* or the owner file never became readable within the grace window (a crash
|
package/src/session.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
removeLaneWorktreesAtClose,
|
|
17
17
|
rollbackGitBinding,
|
|
18
18
|
} from './git-binding.js';
|
|
19
|
-
import { withMemoryRootLock } from './serialization.js';
|
|
19
|
+
import { memoryRootCreatedByLock, withMemoryRootLock } from './serialization.js';
|
|
20
20
|
import { findDuplicateDecisionIds, formatDecisionId } from './decisions.js';
|
|
21
21
|
import {
|
|
22
22
|
assignLanePort,
|
|
@@ -1782,7 +1782,9 @@ function getLanePaths(normalized, sessionId) {
|
|
|
1782
1782
|
}
|
|
1783
1783
|
|
|
1784
1784
|
async function ensureInitializedProjectMemory(rootDir, options = {}) {
|
|
1785
|
-
|
|
1785
|
+
// The lock makes a missing root to hold its state/ and removes it again at
|
|
1786
|
+
// release (PC1), so a root it created is still missing here.
|
|
1787
|
+
if (!(await fileExists(rootDir)) || (await memoryRootCreatedByLock(rootDir))) {
|
|
1786
1788
|
throw new Error(
|
|
1787
1789
|
`No project memory root was found at ${rootDir}. Run "vibecompass init" before using session commands.`,
|
|
1788
1790
|
);
|