wendkeep 0.75.3 → 0.76.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.75.3",
3
+ "version": "0.76.1",
4
4
  "description": "Vault-first persistent memory for AI coding agents, with an optional profile-aware governance runtime: OFF, FLOW, GUIDE, GOVERN, or ASSURE. Local-first and agent-agnostic (Claude Code, Codex, Cursor…).",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -41,6 +41,7 @@
41
41
  "node": ">=18"
42
42
  },
43
43
  "scripts": {
44
+ "precheck": "node --check src/worktree.mjs && node --check src/context.mjs && node --check packages/vault/src/worktree-metadata.mjs",
44
45
  "check": "node --check scripts/release.mjs && node --check scripts/release-plan.mjs && node --check scripts/release-provenance.mjs && node --check scripts/run-scope.mjs && node --check src/release-provenance.mjs && node --check bin/wendkeep.mjs && node --check packages/cli/src/index.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs && node --check src/observer-auth.mjs && node --check src/observer-privacy.mjs && node --check src/observer-snapshot.mjs && node --check src/observer-store.mjs && node --check src/observer-memory.mjs && node --check src/observer-memory-publish.mjs && node --check src/observer-sql-store.mjs && node --check src/observer-sql-migrate.mjs && node --check src/observer-sql-publish.mjs && node --check src/observer-transcript-store.mjs && node --check src/observer-server.mjs && node --check src/observer.mjs && node --check src/observer-publish.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check src/work-kind.mjs && node --check src/delivery.mjs && node --check web/observer/app.mjs && node --check hooks/observer-publish.mjs && node --check hooks/evidence-context.mjs && node --check hooks/evidence-recall.mjs && node --check hooks/memory-scope.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/operating-profile-task-store.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/flow-store.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/integrations/src/host-hooks.mjs && node --check packages/integrations/src/hook-envelope.mjs && node --check packages/integrations/src/prompt-content.mjs && node --check packages/integrations/src/transcript-usage.mjs && node --check packages/integrations/src/transcripts.mjs && node --check packages/integrations/src/session-identity.mjs && node --check packages/integrations/src/index.mjs && node --check packages/mcp/src/config.mjs && node --check packages/mcp/src/index.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/locale.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-scope.mjs && node --check packages/vault/src/memory-candidate-policy.mjs && node --check packages/vault/src/evidence-recall.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
45
46
  "test": "node --test --test-concurrency=2",
46
47
  "test:core": "node scripts/run-scope.mjs core",
@@ -1,5 +1,5 @@
1
1
  // wendkeep CLI — canonical private runtime.
2
- // wendkeep init [--vault <path>] [--project <path>] [--no-mcp] [--yes] [--force]
2
+ // wendkeep init [--vault <path>] [--project <path>] [--no-mcp] [--yes] [--force] [--vscode-worktree-tasks]
3
3
  // wendkeep hook <name> (invoked by the agent's settings.json; pipes stdin/stdout)
4
4
  // wendkeep doctor [--vault <path>]
5
5
  // wendkeep --version | --help
@@ -33,6 +33,7 @@ Usage:
33
33
  (default: none — opt in explicitly). dotcontext is legacy — the native a2 loop replaces it.
34
34
  --no-companions Skip companion plugins/MCP entirely.
35
35
  --no-colors Skip the Obsidian color system (.obsidian snippet + graph groups).
36
+ --vscode-worktree-tasks Create local, Git-excluded VS Code tasks for managed worktrees.
36
37
  --dotcontext-mcp <v> dotcontext MCP placement: auto (default; skip project entry
37
38
  if already global), project, or none.
38
39
  --dotcontext-hooks <v> dotcontext hooks: full (default), light (no PostToolUse), none.
@@ -46,10 +47,18 @@ Usage:
46
47
  command — the three steps that repeat identically after every
47
48
  package update. Stops at the first failing step. Install the
48
49
  package first (npm i -D wendkeep@latest); a running process
49
- cannot replace itself. · --vault P · --profile <name> · --yes.
50
+ cannot replace itself. · --vault P · --profile <name> · --yes
51
+ · --vscode-worktree-tasks.
50
52
 
51
53
  wendkeep doctor [--vault P] Health check. --scope core|runtime · --strict for CI/release.
52
54
  wendkeep observer <sub> Local multi-project Observer: serve | register | publish | reconcile | status.
55
+ wendkeep worktree create <slug> [--base ref] [--branch name] [--open vscode|none] [--json]
56
+ wendkeep worktree list [--json]
57
+ wendkeep worktree status [slug] [--json]
58
+ wendkeep worktree open <slug> [--editor vscode] [--json]
59
+ Managed linked worktrees under .worktrees (branch default wk/<slug>).
60
+ wendkeep context switch <branch> [--create] [--session <id>] [--json]
61
+ Switch Git branch and the causal session scope in the same worktree.
53
62
  wendkeep change <sub> Change lifecycle: new [--simple|--guide] | use | bind <slug> --session <id> | continue | list | show |
54
63
  status | done <id> | undone <id> | diff | archive [--force] | abandon | relink | backlink.
55
64
  archive exige verdict (rode verify --deep); abandon descarta sem ADR.
@@ -193,6 +202,9 @@ async function main(argv) {
193
202
  } else if (cmd === 'profile') {
194
203
  const { PROFILE_HELP } = await import('../../../src/profile.mjs');
195
204
  process.stdout.write(PROFILE_HELP);
205
+ } else if (cmd === 'context') {
206
+ const { CONTEXT_HELP } = await import('../../../src/context.mjs');
207
+ process.stdout.write(CONTEXT_HELP);
196
208
  } else {
197
209
  process.stdout.write(HELP);
198
210
  }
@@ -206,7 +218,7 @@ async function main(argv) {
206
218
  // `sync` starts with `init` and resolves the freshly bound Vault itself. Pre-resolving
207
219
  // here would prevent that repair step from reporting a corrupt binding as its own
208
220
  // first-stage failure (and could never make it as far as the guarded init).
209
- && !['init', 'sync', 'hook', 'observer', '--version', '-v', '--help', '-h', 'help'].includes(cmd)) {
221
+ && !['init', 'sync', 'worktree', 'hook', 'observer', '--version', '-v', '--help', '-h', 'help'].includes(cmd)) {
210
222
  await preferProjectVault(rest);
211
223
  }
212
224
  switch (cmd) {
@@ -229,6 +241,16 @@ async function main(argv) {
229
241
  if (rest[0] !== 'serve') process.exit(observerExitCode);
230
242
  break;
231
243
  }
244
+ case 'worktree': {
245
+ const { runWorktree } = await import('../../../src/worktree.mjs');
246
+ process.exit(runWorktree(rest));
247
+ break;
248
+ }
249
+ case 'context': {
250
+ const { runContext } = await import('../../../src/context.mjs');
251
+ process.exit(runContext(rest));
252
+ break;
253
+ }
232
254
  case 'sync': {
233
255
  const { runSync } = await import('../../../src/sync.mjs');
234
256
  process.exit(await runSync(rest));
@@ -1,4 +1,5 @@
1
1
  export * from './project-vault.mjs';
2
+ export * from './worktree-metadata.mjs';
2
3
  export * from './vault-path-safety.mjs';
3
4
  export * from './locale.mjs';
4
5
  export * from './memory-schema.mjs';
@@ -9,6 +9,7 @@ import {
9
9
  writeFileSync,
10
10
  } from 'node:fs';
11
11
  import { basename, dirname, isAbsolute, join, parse, relative, resolve } from 'node:path';
12
+ import { resolveWorktreeVaultBinding } from './worktree-metadata.mjs';
12
13
 
13
14
  export const PROJECT_CONFIG_FILE = '.wendkeep.json';
14
15
  export const PROJECT_MARKER_REL = '.brain/PROJECT.json';
@@ -203,7 +204,7 @@ export function resolveProjectVault({
203
204
  bindingFailure = error;
204
205
  }
205
206
  if (binding) {
206
- const result = {
207
+ let result = {
207
208
  base: binding.base,
208
209
  source: 'project-config',
209
210
  projectRoot: binding.projectRoot,
@@ -211,7 +212,29 @@ export function resolveProjectVault({
211
212
  configPath: binding.configPath,
212
213
  config: binding.config,
213
214
  };
214
- if (validateIdentity) validateMarker(result);
215
+ if (validateIdentity) {
216
+ try {
217
+ validateMarker(result);
218
+ } catch (error) {
219
+ const canUsePrivateBinding = error?.code === 'WENDKEEP_VAULT_MARKER_MISSING'
220
+ && !isAbsolute(binding.config.vault);
221
+ if (!canUsePrivateBinding) throw error;
222
+ const privateBinding = resolveWorktreeVaultBinding({
223
+ startDir: start,
224
+ projectId: binding.config.projectId,
225
+ });
226
+ if (!privateBinding) throw error;
227
+ result = {
228
+ ...result,
229
+ base: privateBinding.base,
230
+ source: 'worktree-registry',
231
+ projectRoot: privateBinding.projectRoot,
232
+ repositoryId: privateBinding.repositoryId,
233
+ registryPath: privateBinding.registryPath,
234
+ };
235
+ validateMarker(result);
236
+ }
237
+ }
215
238
  return result;
216
239
  }
217
240
 
@@ -0,0 +1,274 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { spawnSync } from 'node:child_process';
3
+ import {
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ renameSync,
8
+ rmSync,
9
+ statSync,
10
+ writeFileSync,
11
+ } from 'node:fs';
12
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
13
+
14
+ export const WORKTREE_REGISTRY_SCHEMA = 1;
15
+ export const WORKTREE_REGISTRY_REL = 'wendkeep/worktrees-v1.json';
16
+
17
+ function worktreeError(code, message) {
18
+ const error = new Error(message);
19
+ error.code = code;
20
+ return error;
21
+ }
22
+
23
+ function gitOutput(startDir, args, spawn = spawnSync) {
24
+ const result = spawn('git', args, {
25
+ cwd: startDir,
26
+ encoding: 'utf8',
27
+ windowsHide: true,
28
+ });
29
+ if (result.status !== 0) {
30
+ throw worktreeError(
31
+ 'WENDKEEP_WORKTREE_GIT_FAILED',
32
+ String(result.stderr || result.error?.message || `git ${args[0]} falhou`).trim(),
33
+ );
34
+ }
35
+ return String(result.stdout || '').trim();
36
+ }
37
+
38
+ function absoluteGitPath(repoRoot, value) {
39
+ return resolve(isAbsolute(value) ? value : join(repoRoot, value));
40
+ }
41
+
42
+ function parseWorktreeList(text) {
43
+ const records = [];
44
+ let current = null;
45
+ for (const line of String(text || '').split(/\r?\n/)) {
46
+ if (line.startsWith('worktree ')) {
47
+ current = { path: resolve(line.slice('worktree '.length)) };
48
+ records.push(current);
49
+ } else if (current && line.startsWith('HEAD ')) {
50
+ current.head = line.slice('HEAD '.length);
51
+ } else if (current && line.startsWith('branch ')) {
52
+ current.branch = line.slice('branch '.length).replace(/^refs\/heads\//, '');
53
+ } else if (current && line === 'bare') {
54
+ current.bare = true;
55
+ } else if (current && line === 'detached') {
56
+ current.detached = true;
57
+ }
58
+ }
59
+ return records;
60
+ }
61
+
62
+ export function discoverWorktreeRepository({ startDir = process.cwd(), spawn = spawnSync } = {}) {
63
+ const repoRoot = resolve(gitOutput(startDir, ['rev-parse', '--show-toplevel'], spawn));
64
+ const commonDir = absoluteGitPath(
65
+ repoRoot,
66
+ gitOutput(repoRoot, ['rev-parse', '--git-common-dir'], spawn),
67
+ );
68
+ const gitDir = absoluteGitPath(
69
+ repoRoot,
70
+ gitOutput(repoRoot, ['rev-parse', '--git-dir'], spawn),
71
+ );
72
+ const worktrees = parseWorktreeList(gitOutput(repoRoot, ['worktree', 'list', '--porcelain'], spawn));
73
+ const main = worktrees.find((entry) => !entry.bare) || null;
74
+ if (!main) {
75
+ throw worktreeError(
76
+ 'WENDKEEP_WORKTREE_MAIN_UNRESOLVED',
77
+ 'Não foi possível localizar a worktree principal do repositório.',
78
+ );
79
+ }
80
+ return {
81
+ repoRoot,
82
+ commonDir,
83
+ gitDir,
84
+ mainWorktree: main.path,
85
+ worktrees,
86
+ };
87
+ }
88
+
89
+ function sleep(milliseconds) {
90
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
91
+ }
92
+
93
+ export function withWorktreeRegistryLock(registryPath, operation, {
94
+ timeoutMs = 5_000,
95
+ staleMs = 30_000,
96
+ now = () => Date.now(),
97
+ } = {}) {
98
+ const lockPath = `${registryPath}.lock`;
99
+ const startedAt = now();
100
+ mkdirSync(dirname(registryPath), { recursive: true });
101
+ while (true) {
102
+ try {
103
+ mkdirSync(lockPath);
104
+ break;
105
+ } catch (error) {
106
+ if (error?.code !== 'EEXIST') throw error;
107
+ try {
108
+ if (now() - statSync(lockPath).mtimeMs > staleMs) {
109
+ rmSync(lockPath, { recursive: true, force: true });
110
+ continue;
111
+ }
112
+ } catch (inspectError) {
113
+ if (inspectError?.code !== 'ENOENT') throw inspectError;
114
+ continue;
115
+ }
116
+ if (now() - startedAt >= timeoutMs) {
117
+ throw worktreeError(
118
+ 'WENDKEEP_WORKTREE_REGISTRY_BUSY',
119
+ `Registry de worktrees ocupado: ${registryPath}`,
120
+ );
121
+ }
122
+ sleep(20);
123
+ }
124
+ }
125
+ try {
126
+ return operation();
127
+ } finally {
128
+ rmSync(lockPath, { recursive: true, force: true });
129
+ }
130
+ }
131
+
132
+ function atomicJson(path, value) {
133
+ mkdirSync(dirname(path), { recursive: true });
134
+ const content = `${JSON.stringify(value, null, 2)}\n`;
135
+ if (existsSync(path) && readFileSync(path, 'utf8') === content) return false;
136
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
137
+ writeFileSync(temporary, content, { encoding: 'utf8', flag: 'wx' });
138
+ renameSync(temporary, path);
139
+ return true;
140
+ }
141
+
142
+ function validateRegistry(value, path) {
143
+ if (
144
+ value?.schemaVersion !== WORKTREE_REGISTRY_SCHEMA
145
+ || typeof value.repositoryId !== 'string'
146
+ || !value.repositoryId
147
+ || typeof value.projectId !== 'string'
148
+ || !value.projectId
149
+ || typeof value.vaultPath !== 'string'
150
+ || !value.vaultPath
151
+ || !value.entries
152
+ || typeof value.entries !== 'object'
153
+ || Array.isArray(value.entries)
154
+ ) {
155
+ throw worktreeError(
156
+ 'WENDKEEP_WORKTREE_REGISTRY_INVALID',
157
+ `Registry de worktrees incompleto em "${path}".`,
158
+ );
159
+ }
160
+ return value;
161
+ }
162
+
163
+ function parseRegistry(path) {
164
+ let value;
165
+ try {
166
+ value = JSON.parse(readFileSync(path, 'utf8'));
167
+ } catch (error) {
168
+ throw worktreeError(
169
+ 'WENDKEEP_WORKTREE_REGISTRY_INVALID',
170
+ `Registry de worktrees inválido em "${path}": ${error.message}`,
171
+ );
172
+ }
173
+ return validateRegistry(value, path);
174
+ }
175
+
176
+ export function readWorktreeRegistry(repository) {
177
+ const path = join(repository.commonDir, ...WORKTREE_REGISTRY_REL.split('/'));
178
+ return existsSync(path) ? { path, registry: parseRegistry(path) } : { path, registry: null };
179
+ }
180
+
181
+ export function resolveWorktreeVaultBinding({ startDir = process.cwd(), projectId } = {}) {
182
+ let repository;
183
+ try {
184
+ repository = discoverWorktreeRepository({ startDir });
185
+ } catch (error) {
186
+ if (error?.code === 'WENDKEEP_WORKTREE_GIT_FAILED') return null;
187
+ throw error;
188
+ }
189
+ const { path, registry } = readWorktreeRegistry(repository);
190
+ if (!registry) return null;
191
+ if (registry.projectId !== projectId) {
192
+ throw worktreeError(
193
+ 'WENDKEEP_WORKTREE_PROJECT_MISMATCH',
194
+ `Registry "${path}" pertence ao projeto "${registry.projectId}", não "${projectId}".`,
195
+ );
196
+ }
197
+ return {
198
+ base: resolve(registry.vaultPath),
199
+ projectId: registry.projectId,
200
+ repositoryId: registry.repositoryId,
201
+ registryPath: path,
202
+ projectRoot: repository.mainWorktree,
203
+ repository,
204
+ };
205
+ }
206
+
207
+ export function mutateWorktreeRegistry(repository, mutator) {
208
+ if (!repository?.commonDir || typeof mutator !== 'function') {
209
+ throw worktreeError('WENDKEEP_WORKTREE_REPOSITORY_INVALID', 'Repositório Git inválido.');
210
+ }
211
+ const registryPath = join(repository.commonDir, ...WORKTREE_REGISTRY_REL.split('/'));
212
+ return withWorktreeRegistryLock(registryPath, () => {
213
+ if (!existsSync(registryPath)) {
214
+ throw worktreeError(
215
+ 'WENDKEEP_WORKTREE_REGISTRY_MISSING',
216
+ `Registry de worktrees ausente em "${registryPath}".`,
217
+ );
218
+ }
219
+ const current = parseRegistry(registryPath);
220
+ const next = validateRegistry(mutator(structuredClone(current)), registryPath);
221
+ atomicJson(registryPath, next);
222
+ return next;
223
+ });
224
+ }
225
+
226
+ export function worktreeIdentity(repositoryId, gitDir) {
227
+ return createHash('sha256')
228
+ .update(`${repositoryId}\n${resolve(gitDir).replaceAll('\\', '/').toLowerCase()}\n`)
229
+ .digest('hex');
230
+ }
231
+
232
+ export function ensureWorktreeMetadata({
233
+ repository,
234
+ projectId,
235
+ vaultPath,
236
+ worktreesRoot = '.worktrees',
237
+ } = {}) {
238
+ if (!repository?.commonDir || !repository?.gitDir) {
239
+ throw worktreeError('WENDKEEP_WORKTREE_REPOSITORY_INVALID', 'Repositório Git inválido.');
240
+ }
241
+ const registryPath = join(repository.commonDir, ...WORKTREE_REGISTRY_REL.split('/'));
242
+ const normalizedVault = resolve(String(vaultPath || ''));
243
+ const registry = withWorktreeRegistryLock(registryPath, () => {
244
+ const current = existsSync(registryPath) ? parseRegistry(registryPath) : null;
245
+ if (current && current.projectId !== projectId) {
246
+ throw worktreeError(
247
+ 'WENDKEEP_WORKTREE_PROJECT_MISMATCH',
248
+ `Registry pertence a outro projeto: ${current.projectId}.`,
249
+ );
250
+ }
251
+ if (current && resolve(current.vaultPath) !== normalizedVault) {
252
+ throw worktreeError(
253
+ 'WENDKEEP_WORKTREE_VAULT_MISMATCH',
254
+ 'Registry aponta para outro Vault canônico.',
255
+ );
256
+ }
257
+ const next = current || {
258
+ schemaVersion: WORKTREE_REGISTRY_SCHEMA,
259
+ repositoryId: randomUUID(),
260
+ projectId,
261
+ vaultPath: normalizedVault,
262
+ worktreesRoot,
263
+ entries: {},
264
+ };
265
+ atomicJson(registryPath, next);
266
+ return next;
267
+ });
268
+ return {
269
+ registryPath,
270
+ repositoryId: registry.repositoryId,
271
+ currentWorktreeId: worktreeIdentity(registry.repositoryId, repository.gitDir),
272
+ registry,
273
+ };
274
+ }