@vibecompass/vibecompass 0.11.0 → 0.11.1
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 +6 -0
- package/package.json +1 -1
- package/src/cli.js +2 -2
- package/src/serialization.js +42 -3
- package/src/session.js +59 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.1 - 2026-07-06
|
|
4
|
+
|
|
5
|
+
- Harden the per-root serialization lock for symlink-aliased memory roots: root keys now canonicalize through existing parent directories when the memory root itself does not yet exist, and nested lock holders compare held/requested roots by `realpath` before reacquiring the disk lock. This preserves reentrancy when an outer lock creates the root and an inner call reaches it through another spelling.
|
|
6
|
+
- Make shared-checkout lane warnings honest about runnable isolation: unbound same-repo lanes now recommend `--branch <name> --worktree` for independent files or dev servers, including at `lane-env` where `PORT` is exported, while keeping `--claim` as the ownership-review fallback.
|
|
7
|
+
- Suppress shared-checkout warnings when either the selected lane or the sibling lane is already worktree-bound, and add regression coverage for the symlink-root reentrancy and worktree-bound warning paths.
|
|
8
|
+
|
|
3
9
|
## 0.11.0 - 2026-07-03
|
|
4
10
|
|
|
5
11
|
Phase 8 concurrent multi-session lanes (stages S1–S4). Highlights: a per-root
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -151,7 +151,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
|
|
|
151
151
|
}
|
|
152
152
|
if (result.runtime) {
|
|
153
153
|
io.stdout.write(`Runtime: port ${result.runtime.port}, temp dir ${result.runtime.tmpDir}\n`);
|
|
154
|
-
io.stdout.write('Export into a shell with: eval "$(vibecompass lane-env)" (D-282; pass --session <lane-id> when multiple lanes are active).\n');
|
|
154
|
+
io.stdout.write('Export into a shell with: eval "$(vibecompass lane-env)" (D-282; pass --session <lane-id> when multiple lanes are active; use --worktree for independently runnable same-repo lanes).\n');
|
|
155
155
|
}
|
|
156
156
|
writeWarnings(io, result.warnings);
|
|
157
157
|
writeAgentFileSyncResult(io, result.agentFileSync);
|
|
@@ -2290,7 +2290,7 @@ function usageText() {
|
|
|
2290
2290
|
' --session <lane-id> Lane to export. Omitted: nearest worktree lane marker, else the single active lane; 2+ active lanes require --session or a marker (D-277)',
|
|
2291
2291
|
' --json Print the runtime assignment as JSON instead of shell export lines',
|
|
2292
2292
|
' --no-conventional Omit the conventional PORT/TMPDIR alias exports',
|
|
2293
|
-
' Consume with: eval "$(vibecompass lane-env)"; defaults come from project.yaml runtime.{port_base,port_step,tmp_base}',
|
|
2293
|
+
' Consume with: eval "$(vibecompass lane-env)"; defaults come from project.yaml runtime.{port_base,port_step,tmp_base}; use --worktree for independent same-repo dev servers',
|
|
2294
2294
|
'',
|
|
2295
2295
|
'Decision-log options (append-decision / next-decision-id):',
|
|
2296
2296
|
' --root <path> Project-memory root. Defaults to .compass',
|
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, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { mkdir, readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -47,9 +47,9 @@ export function memoryRootLockPath(rootDir) {
|
|
|
47
47
|
* a nested call from inside the lock holder runs immediately.
|
|
48
48
|
*/
|
|
49
49
|
export async function withMemoryRootLock(rootDir, label, fn, options = {}) {
|
|
50
|
-
const key =
|
|
50
|
+
const key = await canonicalizeMemoryRootKey(rootDir);
|
|
51
51
|
const held = heldRoots.getStore();
|
|
52
|
-
if (held
|
|
52
|
+
if (await findHeldMemoryRootKey(held, key)) {
|
|
53
53
|
return fn();
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -78,6 +78,45 @@ export async function withMemoryRootLock(rootDir, label, fn, options = {}) {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
async function canonicalizeMemoryRootKey(rootDir) {
|
|
82
|
+
const resolved = path.resolve(rootDir);
|
|
83
|
+
try {
|
|
84
|
+
return await realpath(resolved);
|
|
85
|
+
} catch {
|
|
86
|
+
try {
|
|
87
|
+
return path.join(await realpath(path.dirname(resolved)), path.basename(resolved));
|
|
88
|
+
} catch {
|
|
89
|
+
return resolved;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function findHeldMemoryRootKey(held, key) {
|
|
95
|
+
if (!held) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (held.has(key)) {
|
|
100
|
+
return key;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const heldKey of held) {
|
|
104
|
+
if (await memoryRootKeysReferToSamePath(heldKey, key)) {
|
|
105
|
+
return heldKey;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function memoryRootKeysReferToSamePath(left, right) {
|
|
113
|
+
try {
|
|
114
|
+
return (await realpath(left)) === (await realpath(right));
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
81
120
|
async function acquireDiskLock(key, label, options) {
|
|
82
121
|
const lockDir = memoryRootLockPath(key);
|
|
83
122
|
const attempts = options.attempts ?? DEFAULT_ATTEMPTS;
|
package/src/session.js
CHANGED
|
@@ -158,6 +158,7 @@ async function startProjectSessionLocked(normalized, options, markerContext) {
|
|
|
158
158
|
repos,
|
|
159
159
|
claims,
|
|
160
160
|
existingLanes,
|
|
161
|
+
hasWorktree: options?.worktree === true,
|
|
161
162
|
});
|
|
162
163
|
const highestDecisionId = await readHighestDecisionId(normalized.rootDir);
|
|
163
164
|
const projectConfig = await readProjectConfig(normalized.projectFilePath);
|
|
@@ -850,7 +851,18 @@ export async function readLaneEnvironment(options = {}) {
|
|
|
850
851
|
port: lane.runtime.port,
|
|
851
852
|
tmpDir: lane.runtime.tmpDir,
|
|
852
853
|
env,
|
|
853
|
-
warnings: [
|
|
854
|
+
warnings: [
|
|
855
|
+
...markerContext.warnings,
|
|
856
|
+
...selection.warnings,
|
|
857
|
+
...buildSharedCheckoutWarnings({
|
|
858
|
+
sessionId: selection.sessionId,
|
|
859
|
+
repos: lane.repos ?? [],
|
|
860
|
+
claims: lane.claims ?? [],
|
|
861
|
+
existingLanes: lanes.filter((item) => item.id !== selection.sessionId),
|
|
862
|
+
hasWorktree: Boolean(lane.worktreeContainer),
|
|
863
|
+
context: 'lane-env',
|
|
864
|
+
}),
|
|
865
|
+
],
|
|
854
866
|
};
|
|
855
867
|
}
|
|
856
868
|
|
|
@@ -2017,7 +2029,21 @@ function buildOverlapWarnings(options) {
|
|
|
2017
2029
|
}
|
|
2018
2030
|
|
|
2019
2031
|
const sharedRepos = intersect(options.repos, lane.repos ?? []);
|
|
2020
|
-
|
|
2032
|
+
warnings.push(...buildSharedCheckoutWarnings({
|
|
2033
|
+
sessionId: options.sessionId,
|
|
2034
|
+
repos: options.repos,
|
|
2035
|
+
claims: options.claims,
|
|
2036
|
+
existingLanes: [lane],
|
|
2037
|
+
hasWorktree: options.hasWorktree,
|
|
2038
|
+
context: 'start-session',
|
|
2039
|
+
}));
|
|
2040
|
+
|
|
2041
|
+
if (
|
|
2042
|
+
sharedRepos.length > 0 &&
|
|
2043
|
+
(options.hasWorktree || lane.worktreeContainer) &&
|
|
2044
|
+
options.claims.length === 0 &&
|
|
2045
|
+
(lane.claims ?? []).length === 0
|
|
2046
|
+
) {
|
|
2021
2047
|
warnings.push(
|
|
2022
2048
|
`Active session lane "${options.sessionId}" shares repo(s) ${sharedRepos.join(', ')} with "${lane.id}" without path claims; add --claim values to clarify ownership.`,
|
|
2023
2049
|
);
|
|
@@ -2027,6 +2053,37 @@ function buildOverlapWarnings(options) {
|
|
|
2027
2053
|
return warnings;
|
|
2028
2054
|
}
|
|
2029
2055
|
|
|
2056
|
+
function buildSharedCheckoutWarnings(options) {
|
|
2057
|
+
if (options.hasWorktree) {
|
|
2058
|
+
return [];
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
const warnings = [];
|
|
2062
|
+
for (const lane of options.existingLanes) {
|
|
2063
|
+
if (lane.worktreeContainer) {
|
|
2064
|
+
continue;
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
const sharedRepos = intersect(options.repos, lane.repos ?? []);
|
|
2068
|
+
if (sharedRepos.length === 0) {
|
|
2069
|
+
continue;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
const ownershipHint = options.claims.length === 0 && (lane.claims ?? []).length === 0
|
|
2073
|
+
? ' Add --claim values when shared-checkout work is intentional so ownership is reviewable.'
|
|
2074
|
+
: '';
|
|
2075
|
+
const runtimeHint = options.context === 'lane-env'
|
|
2076
|
+
? ' The exported PORT is a runtime assignment, not an independently runnable dev server for a shared checkout.'
|
|
2077
|
+
: '';
|
|
2078
|
+
warnings.push(
|
|
2079
|
+
`Active session lane "${options.sessionId}" shares repo(s) ${sharedRepos.join(', ')} with unbound lane "${lane.id}" in the same checkout. ` +
|
|
2080
|
+
`Use --branch <name> --worktree when lanes need independent files or dev servers; worktrees are the isolation boundary.${runtimeHint}${ownershipHint}`,
|
|
2081
|
+
);
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
return warnings;
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2030
2087
|
function collectLaneMetadataWarnings(lanes) {
|
|
2031
2088
|
return lanes.flatMap((lane) => lane.warnings ?? []);
|
|
2032
2089
|
}
|