wendkeep 0.30.0 → 0.32.0

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/src/init.mjs CHANGED
@@ -9,9 +9,13 @@ import { createInterface } from 'node:readline/promises';
9
9
  import {
10
10
  VAULT_FOLDERS,
11
11
  SESSION_HOOKS,
12
+ CHANGE_NUDGE_HOOKS,
13
+ CHANGE_GATE_HOOKS,
12
14
  MCP_SERVER_KEY,
13
15
  mcpServerEntry,
14
16
  hookCommand,
17
+ hookCommandLocal,
18
+ hookCommandLocalLegacy,
15
19
  deriveVaultDirName,
16
20
  selectableCompanions,
17
21
  resolveCompanions,
@@ -91,27 +95,62 @@ function backup(path) {
91
95
 
92
96
  // --- merges -----------------------------------------------------------------
93
97
 
94
- export function mergeSettings(existing, { vaultPath, withMcp, force, companions = [], skipMcp = [], dotcontextHookLevel = 'full' }) {
98
+ // Comando preferido para um hook: node-direto quando o projeto tem o pacote local (alta
99
+ // frequência sem cold-start de npx — ver R3 do design 0.31.0); senão o npx portátil.
100
+ export function hookCommandFor(name, projectPath) {
101
+ try {
102
+ if (projectPath && existsSync(join(projectPath, 'node_modules', 'wendkeep', 'hooks', `${name}.mjs`))) {
103
+ return hookCommandLocal(name);
104
+ }
105
+ } catch { /* fs indisponível — npx */ }
106
+ return hookCommand(name);
107
+ }
108
+
109
+ function localHookAvailable(name, projectPath) {
110
+ try {
111
+ return !!projectPath && existsSync(join(projectPath, 'node_modules', 'wendkeep', 'hooks', `${name}.mjs`));
112
+ } catch { return false; }
113
+ }
114
+
115
+ function localHookArg(name) {
116
+ return `${'${CLAUDE_PROJECT_DIR}'}/node_modules/wendkeep/hooks/${name}.mjs`;
117
+ }
118
+
119
+ export function mergeSettings(existing, { vaultPath, withMcp, force, companions = [], skipMcp = [], dotcontextHookLevel = 'full', projectPath = '' }) {
95
120
  const s = existing && typeof existing === 'object' ? { ...existing } : {};
96
121
  s.hooks = { ...(s.hooks || {}) };
97
122
  let added = 0;
98
- // wendkeep's own session hooks + any companion-authored hooks. Stable-sort by
99
- // `order` (default 0) so higher-order companion hooks (e.g. dotcontext's order 100)
100
- // fold AFTER wendkeep's own within each event. A spec may carry its own `command`
101
- // (dotcontext dispatch) instead of a wendkeep hook `name`.
102
- const allSpecs = [...SESSION_HOOKS, ...companionHookSpecs(companions, { dotcontextHookLevel })].sort(
103
- (a, b) => (a.order ?? 0) - (b.order ?? 0),
104
- );
123
+ // wendkeep's own session hooks + the change-lifecycle hooks (0.31.0, default) + any
124
+ // companion-authored hooks. Stable-sort by `order` (default 0) so higher-order companion
125
+ // hooks (e.g. dotcontext's order 100) fold AFTER wendkeep's own within each event. A spec
126
+ // may carry its own `command` (dotcontext dispatch) instead of a wendkeep hook `name`.
127
+ const allSpecs = [
128
+ ...SESSION_HOOKS,
129
+ ...CHANGE_NUDGE_HOOKS,
130
+ ...CHANGE_GATE_HOOKS,
131
+ ...companionHookSpecs(companions, { dotcontextHookLevel }),
132
+ ].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
105
133
  for (const h of allSpecs) {
106
- const command = h.command ?? hookCommand(h.name);
134
+ const useLocal = !h.command && h.preferLocal && localHookAvailable(h.name, projectPath);
135
+ const command = h.command ?? (useLocal ? 'node' : hookCommand(h.name));
136
+ const args = useLocal ? [localHookArg(h.name)] : undefined;
137
+ // Dual-recognition: um hook nomeado é reconhecido tanto na forma npx quanto na node-direta,
138
+ // para que trocar a forma preferida (ou re-initar noutra máquina) nunca duplique o grupo.
139
+ const candidates = h.command ? [h.command] : [hookCommand(h.name), hookCommandLocal(h.name), hookCommandLocalLegacy(h.name)];
140
+ const ownsHook = (x) => candidates.includes(x.command)
141
+ || (x.command === 'node' && Array.isArray(x.args) && x.args[0] === localHookArg(h.name));
107
142
  const groups = Array.isArray(s.hooks[h.event]) ? [...s.hooks[h.event]] : [];
108
- const owning = groups.find((g) => (g.hooks || []).some((x) => x.command === command));
143
+ const owning = groups.find((g) => (g.hooks || []).some(ownsHook));
109
144
  if (owning) {
110
145
  // Already wired: never add a duplicate group (the old `if (present && !force)` fell through
111
146
  // under --force and appended a second identical group). Under --force, refresh the managed
112
147
  // entry's fields in place — without disturbing any sibling hooks the user grouped with it.
113
- if (force) {
114
- const hk = owning.hooks.find((x) => x.command === command);
148
+ const hk = owning.hooks.find(ownsHook);
149
+ const brokenRelative = hk?.command === hookCommandLocalLegacy(h.name);
150
+ if (force || brokenRelative) {
151
+ hk.command = command;
152
+ if (args) hk.args = args;
153
+ else delete hk.args;
115
154
  hk.timeout = h.timeout;
116
155
  if (h.statusMessage) hk.statusMessage = h.statusMessage;
117
156
  if (h.matcher && (owning.hooks || []).length === 1) owning.matcher = h.matcher;
@@ -120,6 +159,7 @@ export function mergeSettings(existing, { vaultPath, withMcp, force, companions
120
159
  continue;
121
160
  }
122
161
  const entry = { type: 'command', command, timeout: h.timeout, statusMessage: h.statusMessage };
162
+ if (args) entry.args = args;
123
163
  const group = h.matcher ? { matcher: h.matcher, hooks: [entry] } : { hooks: [entry] };
124
164
  groups.push(group);
125
165
  s.hooks[h.event] = groups;
@@ -477,12 +517,12 @@ export async function runInit(argv) {
477
517
  const settingsRead = readJsonSafe(settingsPath);
478
518
  if (!settingsRead.ok) {
479
519
  // Unparseable existing file — never clobber. Drop a .new for manual merge.
480
- const fresh = mergeSettings(null, { vaultPath, withMcp: args.mcp, force: true, companions, skipMcp, dotcontextHookLevel }).settings;
520
+ const fresh = mergeSettings(null, { vaultPath, withMcp: args.mcp, force: true, companions, skipMcp, dotcontextHookLevel, projectPath }).settings;
481
521
  writeJson(`${settingsPath}.new`, fresh);
482
522
  log(M.settingsBadJson(settingsPath));
483
523
  } else {
484
524
  const hadFile = settingsRead.data !== null;
485
- const { settings, added } = mergeSettings(settingsRead.data, { vaultPath, withMcp: args.mcp, force: args.force, companions, skipMcp, dotcontextHookLevel });
525
+ const { settings, added } = mergeSettings(settingsRead.data, { vaultPath, withMcp: args.mcp, force: args.force, companions, skipMcp, dotcontextHookLevel, projectPath });
486
526
  if (hadFile) backup(settingsPath);
487
527
  writeJson(settingsPath, settings);
488
528
  log(M.settings(hadFile ? M.merged : M.created, added, hadFile ? M.bakSaved : ''));
@@ -509,6 +549,8 @@ export async function runInit(argv) {
509
549
  log(M.mcpSkipped);
510
550
  }
511
551
 
552
+ log(' [!] ignore runtime do wendkeep no Git quando o vault for versionado: .brain/.change-*');
553
+
512
554
  // 4. Vault color system (.obsidian) -----------------------------------------
513
555
  if (args.noColors) {
514
556
  log(M.colorsSkipped);
@@ -16,6 +16,12 @@ const WORKFLOW = `# Loop a2 — o ciclo de trabalho do wendkeep
16
16
  Use ao começar qualquer mudança não-trivial. O loop mantém memória (vault) e prova
17
17
  (sensores) juntas, tudo linkado no grafo do Obsidian.
18
18
 
19
+ <HARD-GATE>
20
+ NÃO edite arquivos de código antes do passo 2 (Propose / \`wendkeep change new\`).
21
+ Toda tarefa não-trivial passa pelo loop — planejar no chat e sair editando deixa o
22
+ vault cego. Exceção única: mudança trivial (typo, 1 linha).
23
+ </HARD-GATE>
24
+
19
25
  ## Os passos
20
26
 
21
27
  1. **Explore** — entenda o problema antes de propor. Leia o código/contexto relevante.
@@ -25,6 +31,11 @@ Use ao começar qualquer mudança não-trivial. O loop mantém memória (vault)
25
31
  - \`tarefas.md\` — a lista de tarefas \`- [ ] N.N descrição\`.
26
32
  A mudança vira a *ativa* (ponteiro \`.brain/CURRENT_CHANGE.md\`) e é injetada no
27
33
  próximo SessionStart, então você retoma o trabalho em curso automaticamente.
34
+ Antes de implementar, resolva \`spec_impact\` na proposta:
35
+ - \`required\`: liste a capability em \`specs:\` e preencha
36
+ \`specs/<capability>/spec.md\` com ADDED/MODIFIED/REMOVED; ligue tarefas com \`[req:ID]\`.
37
+ - \`none\`: registre uma justificativa real em \`spec_impact_reason\`.
38
+ \`pending\` nunca é estado pronto para implementação ou archive.
28
39
  3. **Apply** — implemente cada tarefa de \`tarefas.md\` com disciplina **wk-tdd**
29
40
  (teste vermelho antes do código). Marque \`- [x]\` ao concluir. Declare nas tarefas:
30
41
  - \`[sensor:<id>]\` — a prova automatizada (roda no verify).
@@ -133,6 +144,7 @@ qualquer código.
133
144
  Antes de fechar o design, resolva cada zona cinza: decide com o usuário, ou registra como
134
145
  **assumption assinada** ("assumo X porque Y — corrija se errado"). Cinza declinado pelo
135
146
  usuário é *registrado*, não descartado no silêncio. Nada sai do design silenciosamente ambíguo.
147
+ Declare também a capability e se o design tem \`spec_impact: required\` ou \`none\`.
136
148
 
137
149
  ## Tabela out-of-scope
138
150
 
@@ -164,6 +176,10 @@ Mapeie os arquivos: quais criar/modificar e a responsabilidade de cada um. Arqui
164
176
  mudam juntos ficam juntos; um arquivo, uma responsabilidade. É aqui que a decomposição
165
177
  trava.
166
178
 
179
+ Resolva o contrato antes de decompor: \`spec_impact: required\` exige capability + delta real em
180
+ \`specs/<capability>/spec.md\`; \`spec_impact: none\` exige justificativa. Cada comportamento do
181
+ delta recebe ID e as tarefas correspondentes usam \`[req:ID]\`.
182
+
167
183
  ## Tarefas bite-sized (TDD)
168
184
 
169
185
  Cada tarefa termina num entregável testável de forma independente. Cada passo é uma ação
@@ -222,12 +238,21 @@ const WORKFLOW_EN = `# The a2 loop — wendkeep's work cycle
222
238
  Use it when starting any non-trivial change. The loop keeps memory (vault) and proof
223
239
  (sensors) together, wikilinked in the Obsidian graph.
224
240
 
241
+ <HARD-GATE>
242
+ Do NOT edit code files before step 2 (Propose / \`wendkeep change new\`).
243
+ Every non-trivial task goes through the loop — planning in chat and editing right away
244
+ leaves the vault blind. Single exception: a trivial change (typo, one line).
245
+ </HARD-GATE>
246
+
225
247
  ## Steps
226
248
 
227
249
  1. **Explore** — understand the problem before proposing.
228
250
  2. **Propose** — \`wendkeep change new <slug>\` scaffolds \`08-Changes/<slug>/\`
229
251
  (proposta/design/tarefas + a \`specs/\` delta). The change becomes *active* and is
230
252
  injected at the next SessionStart.
253
+ Before implementation, resolve \`spec_impact\`: \`required\` needs the capability listed in
254
+ \`specs:\` plus a real \`specs/<capability>/spec.md\` delta and \`[req:ID]\` links; \`none\`
255
+ needs a real \`spec_impact_reason\`. \`pending\` is never ready for implementation/archive.
231
256
  3. **Apply** — implement each task in tarefas.md with **wk-tdd** (red test first). Tag tasks:
232
257
  \`[sensor:<id>]\` (automated proof) and \`[req:<ID>]\` (the spec requirement it satisfies).
233
258
  4. **Verify** — \`wendkeep verify\` runs the sensors; then \`wendkeep verify --deep\` builds
@@ -298,6 +323,7 @@ Use it when the idea is still vague. Turn it into an approved design BEFORE writ
298
323
  ## Closure gate — no dangling ambiguity
299
324
  Resolve every gray area: decide with the user, or log a **signed-off assumption** ("assuming X
300
325
  because Y — correct me if wrong"). A declined gray area is recorded, not silently dropped.
326
+ Also declare the capability and whether the design has \`spec_impact: required\` or \`none\`.
301
327
 
302
328
  ## Out-of-scope table
303
329
  List explicitly what the change does **not** do. Undeclared scope becomes creep.
@@ -319,6 +345,10 @@ Use it after an approved design. Produce a plan an engineer with no project cont
319
345
  Map the files: what to create/modify and each one's responsibility. Files that change together
320
346
  live together; one file, one responsibility.
321
347
 
348
+ Resolve the contract first: \`spec_impact: required\` needs a capability and a real delta at
349
+ \`specs/<capability>/spec.md\`; \`spec_impact: none\` needs a reason. Give each behaviour an ID
350
+ and link the corresponding tasks with \`[req:ID]\`.
351
+
322
352
  ## Bite-sized tasks (TDD)
323
353
  Each task ends in an independently testable deliverable. Each step is a 2–5 min action:
324
354
  write the failing test (show code) → run and see it fail (exact command + expected) → minimal
@@ -422,6 +452,10 @@ comes from the package (freshness seal; edit a task later and the gate rejects i
422
452
 
423
453
  const PLAN_TEMPLATE_PT = `# Template — plano de tarefas (TDD, bite-sized)
424
454
 
455
+ ## Impacto em specs
456
+ - \`spec_impact\`: \`required\` | \`none\`
457
+ - Capability/delta: \`specs/<capability>/spec.md\` ou justificativa de \`none\`
458
+
425
459
  ## Arquivos
426
460
  - Criar: \`caminho/exato.mjs\`
427
461
  - Modificar: \`caminho/existente.mjs:120-140\`
@@ -445,6 +479,10 @@ tarefas (uma função é \`x()\` em toda parte). Cada tarefa termina num entreg
445
479
 
446
480
  const PLAN_TEMPLATE_EN = `# Template — task plan (TDD, bite-sized)
447
481
 
482
+ ## Spec impact
483
+ - \`spec_impact\`: \`required\` | \`none\`
484
+ - Capability/delta: \`specs/<capability>/spec.md\` or the \`none\` rationale
485
+
448
486
  ## Files
449
487
  - Create: \`exact/path.mjs\`
450
488
  - Modify: \`exact/existing.mjs:120-140\`
@@ -518,21 +556,24 @@ size of the problem>
518
556
  <verifiable criteria: for each requirement, the test that fails → passes>
519
557
  `;
520
558
 
559
+ // As descriptions são o gatilho de ativação: o harness casa a description com o PEDIDO do
560
+ // usuário ("implementa X", "corrige Y"), não com abstrações ("mudança não-trivial"). Gatilhos
561
+ // concretos + instrução imperativa = a skill dispara sozinha (paridade Superpowers).
521
562
  const WK_SKILLS_PT = [
522
- skill('wk-workflow', 'Use ao começar qualquer mudança não-trivial orquestra o loop a2 (explore, propose, apply, verify, archive) nos comandos wendkeep.', WORKFLOW),
563
+ skill('wk-workflow', 'Use SEMPRE que o usuário pedir para implementar, criar, corrigir, refatorar, adicionar ou alterar código — qualquer tarefa de código não-trivial. Invoque ANTES de editar qualquer arquivo: orquestra o loop a2 (wendkeep change new → tarefas → verify archive) e registra tudo no vault.', WORKFLOW),
523
564
  skill('wk-tdd', 'Use ao implementar qualquer comportamento — Red/Green/Refactor com testes que discriminam (derivados do spec, litmus não-raso, adequação).', TDD),
524
- skill('wk-debugging', 'Use quando algo falha ou quebra — depuração sistemática por hipótese antes de corrigir.', DEBUGGING),
525
- skill('wk-brainstorming', 'Use quando a ideia ainda é vaga — vira design aprovado, com closure gate e tabela out-of-scope, antes de código.', BRAINSTORMING, [{ name: 'design-template.md', content: DESIGN_TEMPLATE_PT }]),
526
- skill('wk-planning', 'Use após um design aprovado — decompõe em plano de tarefas TDD bite-sized.', PLANNING, [{ name: 'plan-template.md', content: PLAN_TEMPLATE_PT }]),
565
+ skill('wk-debugging', 'Use quando algo falha, quebra, dá erro ou regride — depuração sistemática por hipótese antes de corrigir.', DEBUGGING),
566
+ skill('wk-brainstorming', 'Use quando a ideia ainda é vaga ou o usuário quer discutir/planejar uma feature (inclusive em plan mode) — vira design aprovado, com closure gate e tabela out-of-scope, antes de código.', BRAINSTORMING, [{ name: 'design-template.md', content: DESIGN_TEMPLATE_PT }]),
567
+ skill('wk-planning', 'Use após um design aprovado ou um plano aceito (inclusive plan mode) — decompõe em plano de tarefas TDD bite-sized e registra na change ativa.', PLANNING, [{ name: 'plan-template.md', content: PLAN_TEMPLATE_PT }]),
527
568
  skill('wk-verify', 'Use no verify deep — passe independente read-only (autor≠verificador) que re-deriva a cobertura do spec e grava verdict.json.', VERIFY, [{ name: 'spec-reviewer-prompt.md', content: REVIEWER_PROMPT_PT }, { name: 'verdict-template.json', content: VERDICT_TEMPLATE }]),
528
569
  ];
529
570
 
530
571
  const WK_SKILLS_EN = [
531
- skill('wk-workflow', 'Use when starting any non-trivial change orchestrates the a2 loop (explore, propose, apply, verify, archive) over the wendkeep commands.', WORKFLOW_EN),
572
+ skill('wk-workflow', 'Use WHENEVER the user asks to implement, create, fix, refactor, add or change code — any non-trivial coding task. Invoke BEFORE editing any file: it orchestrates the a2 loop (wendkeep change new → tasks → verify archive) and records everything in the vault.', WORKFLOW_EN),
532
573
  skill('wk-tdd', 'Use when implementing any behaviour — Red/Green/Refactor with tests that discriminate (spec-derived, non-shallow litmus, adequacy).', TDD_EN),
533
- skill('wk-debugging', 'Use when something fails or breaks — systematic hypothesis-driven debugging before fixing.', DEBUGGING_EN),
534
- skill('wk-brainstorming', 'Use when the idea is still vague — turns it into an approved design, with a closure gate and out-of-scope table, before code.', BRAINSTORMING_EN, [{ name: 'design-template.md', content: DESIGN_TEMPLATE_EN }]),
535
- skill('wk-planning', 'Use after an approved design — decomposes it into a bite-sized TDD task plan.', PLANNING_EN, [{ name: 'plan-template.md', content: PLAN_TEMPLATE_EN }]),
574
+ skill('wk-debugging', 'Use when something fails, breaks, errors or regresses — systematic hypothesis-driven debugging before fixing.', DEBUGGING_EN),
575
+ skill('wk-brainstorming', 'Use when the idea is still vague or the user wants to discuss/plan a feature (plan mode included) — turns it into an approved design, with a closure gate and out-of-scope table, before code.', BRAINSTORMING_EN, [{ name: 'design-template.md', content: DESIGN_TEMPLATE_EN }]),
576
+ skill('wk-planning', 'Use after an approved design or an accepted plan (plan mode included) — decomposes it into a bite-sized TDD task plan recorded in the active change.', PLANNING_EN, [{ name: 'plan-template.md', content: PLAN_TEMPLATE_EN }]),
536
577
  skill('wk-verify', 'Use in verify deep — an independent read-only pass (author≠verifier) that re-derives spec coverage and writes verdict.json.', VERIFY_EN, [{ name: 'spec-reviewer-prompt.md', content: REVIEWER_PROMPT_EN }, { name: 'verdict-template.json', content: VERDICT_TEMPLATE }]),
537
578
  ];
538
579
 
@@ -545,20 +586,22 @@ export const WK_SKILLS = WK_SKILLS_PT;
545
586
  // Seed each skill into <brainDir>/skills/<name>/ if absent (non-destructive): SKILL.md plus any
546
587
  // bundled template/prompt files. Existing files are never overwritten, so re-seeding an older
547
588
  // install just fills in the new template files alongside its SKILL.md.
548
- export function seedWkSkills(brainDir, localeId = 'pt-BR') {
589
+ // { refresh: true } (sync-defs --reseed) SOBRESCREVE as wk-* com os seeds atuais — é como um
590
+ // vault existente recebe descriptions/HARD-GATE novos (edições manuais nas wk-* são perdidas).
591
+ export function seedWkSkills(brainDir, localeId = 'pt-BR', { refresh = false } = {}) {
549
592
  const created = [];
550
593
  for (const s of wkSkills(localeId)) {
551
594
  const dir = join(brainDir, 'skills', s.name);
552
595
  mkdirSync(dir, { recursive: true });
553
- const writeIfAbsent = (name, content) => {
596
+ const write = (name, content) => {
554
597
  const f = join(dir, name);
555
- if (!existsSync(f)) {
598
+ if (refresh || !existsSync(f)) {
556
599
  writeFileSync(f, content, 'utf8');
557
600
  created.push(f);
558
601
  }
559
602
  };
560
- writeIfAbsent('SKILL.md', s.body);
561
- for (const file of s.files || []) writeIfAbsent(file.name, file.content);
603
+ write('SKILL.md', s.body);
604
+ for (const file of s.files || []) write(file.name, file.content);
562
605
  }
563
606
  return created;
564
607
  }
package/src/sync-defs.mjs CHANGED
@@ -7,6 +7,8 @@
7
7
  // cross-platform robustness.
8
8
  import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
9
9
  import { isAbsolute, join, resolve } from 'node:path';
10
+ import { seedWkSkills } from './skills-seed.mjs';
11
+ import { getLocale } from '../hooks/locale.mjs';
10
12
 
11
13
  // Managed AGENTS.md section (0.8.0): the agent-agnostic distribution channel. Codex, Amp,
12
14
  // Cursor, Zed et al. read AGENTS.md — one file covers them all. Only the content between
@@ -114,6 +116,12 @@ export function runSyncDefs(argv) {
114
116
  }
115
117
  const vaultBase = isAbsolute(base) ? base : resolve(process.cwd(), base);
116
118
  const projectPath = resolve(project || process.cwd());
119
+ // --reseed (0.31.0): sobrescreve as wk-* de .brain/skills com os seeds da versão instalada
120
+ // ANTES de copiar — é como um vault existente recebe descriptions/HARD-GATE novos.
121
+ if (argv.includes('--reseed')) {
122
+ const n = seedWkSkills(join(vaultBase, '.brain'), getLocale(vaultBase).id, { refresh: true });
123
+ process.stdout.write(`wendkeep sync-defs: ${n.length} arquivo(s) de skill re-semeados em .brain/skills\n`);
124
+ }
117
125
  const r = syncDefs(vaultBase, projectPath);
118
126
  process.stdout.write(
119
127
  `wendkeep sync-defs: ${r.agents.length} agent(s) -> .codex/agents, ${r.skills.length} skill(s) -> .claude/skills\n`,
package/src/taxonomy.mjs CHANGED
@@ -50,6 +50,11 @@ export const HOOK_FILES = [
50
50
  'task-log.mjs',
51
51
  'vault-health.mjs',
52
52
  'understand-inject.mjs',
53
+ 'change-context.mjs',
54
+ 'change-warn.mjs',
55
+ 'change-guard.mjs',
56
+ 'change-nag.mjs',
57
+ 'plan-capture.mjs',
53
58
  ];
54
59
 
55
60
  // Hook scripts that are safe to invoke directly via `wendkeep hook <name>`.
@@ -68,6 +73,11 @@ export const RUNNABLE_HOOKS = [
68
73
  'brain-reindex',
69
74
  'vault-health',
70
75
  'understand-inject',
76
+ 'change-context',
77
+ 'change-warn',
78
+ 'change-guard',
79
+ 'change-nag',
80
+ 'plan-capture',
71
81
  ];
72
82
 
73
83
  // The MCP server entry wendkeep wires into .mcp.json so the agent can read/write the
@@ -107,6 +117,31 @@ export function hookCommand(name) {
107
117
  return `npx wendkeep hook ${name}`;
108
118
  }
109
119
 
120
+ // Forma node-direta do comando de hook: 1 processo (~100-250ms) em vez dos 3 do npx (cold-start
121
+ // de segundos no Windows). Usada pelos hooks de ALTA FREQUÊNCIA (por prompt / por tool-call)
122
+ // quando o projeto tem wendkeep instalado localmente; o init decide (hookCommandFor).
123
+ export function hookCommandLocal(name) {
124
+ return `node "${'${CLAUDE_PROJECT_DIR}'}/node_modules/wendkeep/hooks/${name}.mjs"`;
125
+ }
126
+
127
+ export function hookCommandLocalLegacy(name) {
128
+ return `node node_modules/wendkeep/hooks/${name}.mjs`;
129
+ }
130
+
131
+ // Hooks do lifecycle de change (0.31.0) — enforcement do loop a2. Nudges (contexto/aviso/
132
+ // cobrança/captura de plano) e gate (deny/ask no Bash). Separados em dois grupos para
133
+ // preservar a opção futura de gates opt-in; hoje o init wira TODOS por default.
134
+ // preferLocal: alta frequência → invocação node-direta quando houver instalação local.
135
+ export const CHANGE_NUDGE_HOOKS = [
136
+ { event: 'UserPromptSubmit', matcher: null, name: 'change-context', timeout: 15, order: 10, preferLocal: true, statusMessage: 'wendkeep: change ping' },
137
+ { event: 'PostToolUse', matcher: 'Edit|Write|MultiEdit', name: 'change-warn', timeout: 10, order: 10, preferLocal: true, statusMessage: 'wendkeep: change warn' },
138
+ { event: 'PostToolUse', matcher: 'ExitPlanMode', name: 'plan-capture', timeout: 15, order: 10, preferLocal: true, statusMessage: 'wendkeep: capturing approved plan' },
139
+ { event: 'Stop', matcher: null, name: 'change-nag', timeout: 15, order: 10, preferLocal: true, statusMessage: 'wendkeep: open tasks check' },
140
+ ];
141
+ export const CHANGE_GATE_HOOKS = [
142
+ { event: 'PreToolUse', matcher: 'Bash', name: 'change-guard', timeout: 10, order: 10, preferLocal: true, statusMessage: 'wendkeep: change gate' },
143
+ ];
144
+
110
145
  // --- companion plugins / MCP --------------------------------------------------
111
146
  // Optional tools wendkeep init can pin alongside the vault. Each is wired through
112
147
  // the MOST agent-agnostic mechanism it supports; the Claude Code plugin entry