wendkeep 0.75.2 → 0.75.3

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 CHANGED
@@ -4,6 +4,15 @@ All notable changes to **wendkeep** are documented here. Format based on
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project follows
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.75.3] — 2026-08-20
8
+
9
+ ### Fixed
10
+
11
+ - **Sync seguro no self-checkout.** O instalador agora reconhece o repositório do próprio
12
+ WendKeep e migra hooks Claude/Codex para `node ./bin/wendkeep.mjs` sem duplicá-los. Projetos
13
+ consumidores continuam usando `npx --no-install`, e executar `sync` no checkout de
14
+ desenvolvimento não reintroduz a autodependência publicada.
15
+
7
16
  ## [0.75.2] — 2026-08-20
8
17
 
9
18
  ### Fixed
package/README.en.md CHANGED
@@ -180,6 +180,10 @@ later stages. An invalid `.wendkeep.json` stops at `init` without falling back t
180
180
  The install stays outside `sync` on purpose: a running process cannot replace itself and
181
181
  keep going — the code in memory would still be the old one.
182
182
 
183
+ In WendKeep's own development checkout, do not install `wendkeep` in `devDependencies`. Use
184
+ `node ./bin/wendkeep.mjs sync --project . --yes`: the installer recognizes the self-checkout and
185
+ keeps hooks on the working tree without duplicating consumer `npx` commands.
186
+
183
187
  In a **pnpm** monorepo the install command differs (`npm` in a pnpm repo fails with
184
188
  `Cannot read properties of null (reading 'matches')`). Resolve the published version first and
185
189
  reuse exactly the value returned:
package/README.md CHANGED
@@ -180,6 +180,10 @@ later stages. An invalid `.wendkeep.json` stops at `init` without falling back t
180
180
  The install stays outside `sync` on purpose: a running process cannot replace itself and
181
181
  keep going — the code in memory would still be the old one.
182
182
 
183
+ In WendKeep's own development checkout, do not install `wendkeep` in `devDependencies`. Use
184
+ `node ./bin/wendkeep.mjs sync --project . --yes`: the installer recognizes the self-checkout and
185
+ keeps hooks on the working tree without duplicating consumer `npx` commands.
186
+
183
187
  In a **pnpm** monorepo the install command differs (`npm` in a pnpm repo fails with
184
188
  `Cannot read properties of null (reading 'matches')`). Resolve the published version first and
185
189
  reuse exactly the value returned:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.75.2",
3
+ "version": "0.75.3",
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": [
@@ -36,6 +36,12 @@ export function hookCommand(name) {
36
36
  return `npx --no-install wendkeep hook ${name}`;
37
37
  }
38
38
 
39
+ // O checkout do próprio WendKeep não depende do pacote publicado. Seus hooks versionados
40
+ // executam o binário do working tree para que sync/dogfood não recrie a autodependência.
41
+ export function hookCommandWorkingTree(name) {
42
+ return `node ./bin/wendkeep.mjs hook ${name}`;
43
+ }
44
+
39
45
  // Forma node-direta do comando de hook: 1 processo (~100-250ms) em vez dos 3 do npx (cold-start
40
46
  // de segundos no Windows). Usada pelos hooks de ALTA FREQUÊNCIA (por prompt / por tool-call)
41
47
  // quando o projeto tem wendkeep instalado localmente; o init decide (hookCommandFor).
package/src/init.mjs CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  CHANGE_NUDGE_HOOKS,
14
14
  CHANGE_GATE_HOOKS,
15
15
  hookCommand,
16
+ hookCommandWorkingTree,
16
17
  hookCommandLocal,
17
18
  hookCommandLocalLegacy,
18
19
  codexHookSpecs,
@@ -109,7 +110,19 @@ function backup(path) {
109
110
 
110
111
  // Comando preferido para um hook: node-direto quando o projeto tem o pacote local (alta
111
112
  // frequência sem cold-start de npx — ver R3 do design 0.31.0); senão o npx portátil.
113
+ export function isWendkeepSelfCheckout(projectPath) {
114
+ try {
115
+ if (!projectPath || !existsSync(join(projectPath, 'bin', 'wendkeep.mjs'))) return false;
116
+ const pkg = JSON.parse(readFileSync(join(projectPath, 'package.json'), 'utf8'));
117
+ const bin = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.wendkeep;
118
+ return pkg.name === 'wendkeep' && String(bin || '').replace(/\\/g, '/') === 'bin/wendkeep.mjs';
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
123
+
112
124
  export function hookCommandFor(name, projectPath) {
125
+ if (isWendkeepSelfCheckout(projectPath)) return hookCommandWorkingTree(name);
113
126
  try {
114
127
  if (projectPath && existsSync(join(projectPath, 'node_modules', 'wendkeep', 'hooks', `${name}.mjs`))) {
115
128
  return hookCommandLocal(name);
@@ -142,13 +155,22 @@ export function mergeSettings(existing, { vaultPath, withMcp, force, companions
142
155
  ...CHANGE_GATE_HOOKS,
143
156
  ...companionHookSpecs(companions, { dotcontextHookLevel }),
144
157
  ].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
158
+ const selfCheckout = isWendkeepSelfCheckout(projectPath);
145
159
  for (const h of allSpecs) {
160
+ const useWorkingTree = !h.command && selfCheckout;
146
161
  const useLocal = !h.command && h.preferLocal && localHookAvailable(h.name, projectPath);
147
- const command = h.command ?? (useLocal ? 'node' : hookCommand(h.name));
148
- const args = useLocal ? [localHookArg(h.name)] : undefined;
162
+ const command = h.command ?? (useWorkingTree
163
+ ? hookCommandWorkingTree(h.name)
164
+ : (useLocal ? 'node' : hookCommand(h.name)));
165
+ const args = !useWorkingTree && useLocal ? [localHookArg(h.name)] : undefined;
149
166
  // Dual-recognition: um hook nomeado é reconhecido tanto na forma npx quanto na node-direta,
150
167
  // para que trocar a forma preferida (ou re-initar noutra máquina) nunca duplique o grupo.
151
- const candidates = h.command ? [h.command] : [hookCommand(h.name), hookCommandLocal(h.name), hookCommandLocalLegacy(h.name)];
168
+ const candidates = h.command ? [h.command] : [
169
+ hookCommand(h.name),
170
+ hookCommandWorkingTree(h.name),
171
+ hookCommandLocal(h.name),
172
+ hookCommandLocalLegacy(h.name),
173
+ ];
152
174
  const ownsHook = (x) => candidates.includes(x.command)
153
175
  || (x.command === 'node' && Array.isArray(x.args) && x.args[0] === localHookArg(h.name));
154
176
  const groups = Array.isArray(s.hooks[h.event]) ? [...s.hooks[h.event]] : [];
@@ -159,7 +181,8 @@ export function mergeSettings(existing, { vaultPath, withMcp, force, companions
159
181
  // entry's fields in place — without disturbing any sibling hooks the user grouped with it.
160
182
  const hk = owning.hooks.find(ownsHook);
161
183
  const brokenRelative = hk?.command === hookCommandLocalLegacy(h.name);
162
- if (force || brokenRelative) {
184
+ const wrongRuntime = !h.command && hk?.command !== command;
185
+ if (force || brokenRelative || wrongRuntime) {
163
186
  hk.command = command;
164
187
  if (args) hk.args = args;
165
188
  else delete hk.args;
@@ -208,14 +231,19 @@ export function mergeSettings(existing, { vaultPath, withMcp, force, companions
208
231
  // it does handle is the legacy `timeout` key, which Codex silently ignores in favour of a
209
232
  // 600s default; rewriting it to `timeoutSec` invalidates the stored trusted_hash and costs
210
233
  // the user one "Hooks need review" prompt. That is the point.
211
- export function mergeCodexHooks(existing, { force = false } = {}) {
234
+ export function mergeCodexHooks(existing, { force = false, projectPath = '' } = {}) {
212
235
  const file = existing && typeof existing === 'object' ? { ...existing } : {};
213
236
  file.hooks = { ...(file.hooks || {}) };
214
237
  const specs = codexHookSpecs([...SESSION_HOOKS, ...CHANGE_NUDGE_HOOKS, ...CHANGE_GATE_HOOKS])
215
238
  .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
239
+ const selfCheckout = isWendkeepSelfCheckout(projectPath);
216
240
  for (const h of specs) {
217
- const entry = codexHookEntry(h);
218
- const owns = (x) => x.command === entry.command;
241
+ const entry = {
242
+ ...codexHookEntry(h),
243
+ ...(selfCheckout ? { command: hookCommandWorkingTree(h.name) } : {}),
244
+ };
245
+ const candidates = new Set([hookCommand(h.name), hookCommandWorkingTree(h.name)]);
246
+ const owns = (x) => candidates.has(x.command);
219
247
  const groups = Array.isArray(file.hooks[h.event]) ? [...file.hooks[h.event]] : [];
220
248
  const owning = groups.find((g) => (g.hooks || []).some(owns));
221
249
  if (owning) {
@@ -224,7 +252,8 @@ export function mergeCodexHooks(existing, { force = false } = {}) {
224
252
  // `timeout` is the pre-0.46 key: Codex never read it. Migrate it even without --force,
225
253
  // otherwise the hook keeps running at the 600s default forever.
226
254
  const legacyTimeout = 'timeout' in hk;
227
- if (force || legacyTimeout) {
255
+ const wrongRuntime = hk.command !== entry.command;
256
+ if (force || legacyTimeout || wrongRuntime) {
228
257
  if (legacyTimeout) {
229
258
  if (hk.timeoutSec === undefined) hk.timeoutSec = hk.timeout;
230
259
  delete hk.timeout;
@@ -233,6 +262,7 @@ export function mergeCodexHooks(existing, { force = false } = {}) {
233
262
  hk.timeoutSec = entry.timeoutSec;
234
263
  if (entry.statusMessage) hk.statusMessage = entry.statusMessage;
235
264
  }
265
+ if (wrongRuntime) hk.command = entry.command;
236
266
  }
237
267
  if (force && matcher) owning.matcher = matcher;
238
268
  if (force && !matcher) delete owning.matcher;
@@ -593,12 +623,12 @@ export async function runInit(argv) {
593
623
  const codexPath = join(projectPath, '.codex', 'hooks.json');
594
624
  const codexRead = readJsonSafe(codexPath);
595
625
  if (!codexRead.ok) {
596
- writeJson(`${codexPath}.new`, mergeCodexHooks(null, { force: true }));
626
+ writeJson(`${codexPath}.new`, mergeCodexHooks(null, { force: true, projectPath }));
597
627
  log(M.codexBadJson(codexPath));
598
628
  } else {
599
629
  const hadFile = codexRead.data !== null;
600
630
  if (hadFile) backup(codexPath);
601
- writeJson(codexPath, mergeCodexHooks(codexRead.data, { force: args.force }));
631
+ writeJson(codexPath, mergeCodexHooks(codexRead.data, { force: args.force, projectPath }));
602
632
  log(M.codexHooks(hadFile ? M.merged : M.created, hadFile ? M.bakSaved : ''));
603
633
  }
604
634
  log(M.codexTrust);
package/src/taxonomy.mjs CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  codexHookEntry,
12
12
  codexHookSpecs,
13
13
  hookCommand,
14
+ hookCommandWorkingTree,
14
15
  hookCommandLocal,
15
16
  hookCommandLocalLegacy,
16
17
  } from '../packages/integrations/src/host-hooks.mjs';
@@ -24,6 +25,7 @@ export {
24
25
  codexHookEntry,
25
26
  codexHookSpecs,
26
27
  hookCommand,
28
+ hookCommandWorkingTree,
27
29
  hookCommandLocal,
28
30
  hookCommandLocalLegacy,
29
31
  mcpServerEntry,