@vibecompass/vibecompass 0.10.8 → 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.
@@ -1,4 +1,15 @@
1
1
  const KEY_VALUE_PATTERN = /^([A-Za-z_][A-Za-z0-9_-]*)\s*:(?:\s+(.*)|\s*)$/;
2
+ const SAFE_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_-]*$/;
3
+
4
+ /**
5
+ * Whether a value can appear as a block-map key in this subset. Writers that
6
+ * derive map keys from user-supplied identifiers (e.g. repo ids in lane
7
+ * metadata) must check this and fail closed instead of emitting a key the
8
+ * parser will reject — a parse throw degrades the whole document for readers.
9
+ */
10
+ export function isSimpleYamlKeySafe(value) {
11
+ return typeof value === 'string' && SAFE_KEY_PATTERN.test(value);
12
+ }
2
13
 
3
14
  export function parseSimpleYaml(source, options = {}) {
4
15
  // This intentionally supports only the YAML subset used by project.yaml and
@@ -208,10 +219,19 @@ function parseScalar(rawValue) {
208
219
  return Number(rawValue);
209
220
  }
210
221
 
211
- if (
212
- (rawValue.startsWith('"') && rawValue.endsWith('"')) ||
213
- (rawValue.startsWith("'") && rawValue.endsWith("'"))
214
- ) {
222
+ if (rawValue.startsWith('"') && rawValue.endsWith('"')) {
223
+ // Writers quote free-text values with JSON.stringify; parse the token
224
+ // back symmetrically so escaped quotes and backslashes (e.g. Windows
225
+ // paths) round-trip. Hand-authored quoting that is not valid JSON keeps
226
+ // the historical naive slice so legacy files stay readable.
227
+ try {
228
+ return JSON.parse(rawValue);
229
+ } catch {
230
+ return rawValue.slice(1, -1);
231
+ }
232
+ }
233
+
234
+ if (rawValue.startsWith("'") && rawValue.endsWith("'")) {
215
235
  return rawValue.slice(1, -1);
216
236
  }
217
237
 
package/src/status.js CHANGED
@@ -2,17 +2,21 @@ import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { inspectProjectCompatibility } from './compatibility.js';
4
4
  import { sha256Text } from './hash.js';
5
+ import { resolveLaneMarkerContext } from './lane-marker.js';
5
6
  import { parseSimpleYaml } from './simple-yaml.js';
6
- import { listProjectSessions } from './session.js';
7
+ import { inferToolingRootForMemoryRoot, listProjectSessions } from './session.js';
7
8
  import { syncAgentInstructionFiles } from './generators/agent-files/index.js';
8
9
  import { scanProjectMemory } from './project-memory.js';
9
10
 
10
11
  export async function getProjectStatus(options = {}) {
11
12
  const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
12
- const rootDir = path.resolve(cwd, options.rootDir ?? '.compass');
13
+ const rootContext = await resolveStatusRootContext(cwd, options.rootDir);
14
+ const rootDir = rootContext.rootDir;
13
15
  const toolingRootDir = options.toolingRootDir
14
16
  ? path.resolve(cwd, options.toolingRootDir)
15
- : cwd;
17
+ : rootContext.rootSource === 'marker'
18
+ ? await inferToolingRootForMemoryRoot(rootDir)
19
+ : cwd;
16
20
 
17
21
  const compatibility = await inspectProjectCompatibility({
18
22
  rootDir,
@@ -36,7 +40,15 @@ export async function getProjectStatus(options = {}) {
36
40
  cliPackageVersion: compatibility.cliPackageVersion,
37
41
  package: compatibility.package,
38
42
  state: compatibility.state,
39
- warnings: compatibility.warnings,
43
+ warnings: [
44
+ ...(rootContext.markerError
45
+ ? [{
46
+ code: 'lane-marker-unreadable',
47
+ message: `A lane marker near ${cwd} could not be resolved (${rootContext.markerError}); reporting on ${rootDir} instead. Lane-scoped commands will fail closed on it.`,
48
+ }]
49
+ : []),
50
+ ...compatibility.warnings,
51
+ ],
40
52
  },
41
53
  sessions,
42
54
  docsReview,
@@ -53,6 +65,30 @@ export async function getProjectStatus(options = {}) {
53
65
  };
54
66
  }
55
67
 
68
+ /**
69
+ * D-280: status adopts the nearest valid marker's memory root when --root is
70
+ * omitted, matching lane-scoped commands, so `status` from a worktree cwd
71
+ * reports the bound root instead of a nonexistent `cwd/.compass`. Status is
72
+ * read-only and must not fail on a corrupt or foreign marker — resolution
73
+ * failures fall back to the cwd default.
74
+ */
75
+ async function resolveStatusRootContext(cwd, explicitRootDir) {
76
+ if (explicitRootDir) {
77
+ return { rootDir: path.resolve(cwd, explicitRootDir), rootSource: 'flag', markerError: null };
78
+ }
79
+
80
+ try {
81
+ const context = await resolveLaneMarkerContext({ cwd });
82
+ return { rootDir: context.rootDir, rootSource: context.rootSource, markerError: null };
83
+ } catch (error) {
84
+ return {
85
+ rootDir: path.resolve(cwd, '.compass'),
86
+ rootSource: 'default',
87
+ markerError: error instanceof Error ? error.message : String(error),
88
+ };
89
+ }
90
+ }
91
+
56
92
  export function renderStatusText(status) {
57
93
  if (status.project.status === 'unreadable') {
58
94
  return [
@@ -375,7 +411,9 @@ function buildRecommendations({ project, compatibility, sessions, docsReview, ag
375
411
  recommendations.push(
376
412
  sessions.status === 'ok' && sessions.current
377
413
  ? `vibecompass docs-update --session ${sessions.current}`
378
- : 'vibecompass docs-review --guided',
414
+ : sessions.status === 'ok' && sessions.lanes.length >= 2
415
+ ? 'vibecompass list-sessions'
416
+ : 'vibecompass docs-review --guided',
379
417
  );
380
418
  }
381
419
 
package/src/sync.js CHANGED
@@ -2,6 +2,7 @@ import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { sha256Text } from './hash.js';
4
4
  import { writeStateManifest } from './manifest.js';
5
+ import { withMemoryRootLock } from './serialization.js';
5
6
  import { parseSimpleYaml } from './simple-yaml.js';
6
7
  import {
7
8
  buildSyncStateWithCursor,
@@ -143,6 +144,10 @@ export async function pullExportProjectMemory(options = {}, environment = {}) {
143
144
  export async function applyPullExport(options = {}, environment = {}) {
144
145
  const cwd = environment.cwd ? path.resolve(environment.cwd) : process.cwd();
145
146
  const rootDir = path.resolve(cwd, options.rootDir ?? '.compass');
147
+ return withMemoryRootLock(rootDir, 'apply-export', () => applyPullExportLocked(options, rootDir));
148
+ }
149
+
150
+ async function applyPullExportLocked(options, rootDir) {
146
151
  const outputPath = path.resolve(rootDir, options.outputPath ?? DEFAULT_EXPORT_PATH);
147
152
  const bundle = await readJsonRequired(outputPath, 'pull-export bundle');
148
153
  const operations = Array.isArray(bundle.operations) ? bundle.operations : [];
@@ -10,7 +10,7 @@ export const WORKFLOW_REGISTRY = [
10
10
  { trigger: 'start session', summary: 'builder role trigger' },
11
11
  { trigger: 'join as reviewer', summary: 'reviewer role trigger' },
12
12
  { trigger: 'planning mode', summary: 'optional prompt-level mode for scoping work before implementation' },
13
- { trigger: 'review handoff', summary: 'reviewer reads the selected lane handoff and appends findings' },
13
+ { trigger: 'review handoff', summary: 'reviewer reads the selected lane handoff and appends findings; with multiple active lanes name the lane explicitly: `review handoff <lane-id>`' },
14
14
  { trigger: 'address review', summary: 'builder critically resolves reviewer feedback from the selected lane scratch files' },
15
15
  { trigger: 'docs update', summary: 'builder runs a targeted session-delta documentation maintenance plan with `vibecompass docs-update --session <lane-id>`' },
16
16
  { trigger: 'close session', summary: 'builder runs the close-out checklist and ends with `vibecompass close-session --session <lane-id>` plus required document-maintenance checkpoint statuses; `end session` and `vibecompass end-session` are accepted aliases' },