wendkeep 0.29.2 → 0.31.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,12 @@ 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,
15
18
  deriveVaultDirName,
16
19
  selectableCompanions,
17
20
  resolveCompanions,
@@ -91,27 +94,45 @@ function backup(path) {
91
94
 
92
95
  // --- merges -----------------------------------------------------------------
93
96
 
94
- export function mergeSettings(existing, { vaultPath, withMcp, force, companions = [], skipMcp = [], dotcontextHookLevel = 'full' }) {
97
+ // Comando preferido para um hook: node-direto quando o projeto tem o pacote local (alta
98
+ // frequência sem cold-start de npx — ver R3 do design 0.31.0); senão o npx portátil.
99
+ export function hookCommandFor(name, projectPath) {
100
+ try {
101
+ if (projectPath && existsSync(join(projectPath, 'node_modules', 'wendkeep', 'hooks', `${name}.mjs`))) {
102
+ return hookCommandLocal(name);
103
+ }
104
+ } catch { /* fs indisponível — npx */ }
105
+ return hookCommand(name);
106
+ }
107
+
108
+ export function mergeSettings(existing, { vaultPath, withMcp, force, companions = [], skipMcp = [], dotcontextHookLevel = 'full', projectPath = '' }) {
95
109
  const s = existing && typeof existing === 'object' ? { ...existing } : {};
96
110
  s.hooks = { ...(s.hooks || {}) };
97
111
  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
- );
112
+ // wendkeep's own session hooks + the change-lifecycle hooks (0.31.0, default) + any
113
+ // companion-authored hooks. Stable-sort by `order` (default 0) so higher-order companion
114
+ // hooks (e.g. dotcontext's order 100) fold AFTER wendkeep's own within each event. A spec
115
+ // may carry its own `command` (dotcontext dispatch) instead of a wendkeep hook `name`.
116
+ const allSpecs = [
117
+ ...SESSION_HOOKS,
118
+ ...CHANGE_NUDGE_HOOKS,
119
+ ...CHANGE_GATE_HOOKS,
120
+ ...companionHookSpecs(companions, { dotcontextHookLevel }),
121
+ ].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
105
122
  for (const h of allSpecs) {
106
- const command = h.command ?? hookCommand(h.name);
123
+ const command = h.command ?? (h.preferLocal ? hookCommandFor(h.name, projectPath) : hookCommand(h.name));
124
+ // Dual-recognition: um hook nomeado é reconhecido tanto na forma npx quanto na node-direta,
125
+ // para que trocar a forma preferida (ou re-initar noutra máquina) nunca duplique o grupo.
126
+ const candidates = h.command ? [h.command] : [hookCommand(h.name), hookCommandLocal(h.name)];
107
127
  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));
128
+ const owning = groups.find((g) => (g.hooks || []).some((x) => candidates.includes(x.command)));
109
129
  if (owning) {
110
130
  // Already wired: never add a duplicate group (the old `if (present && !force)` fell through
111
131
  // under --force and appended a second identical group). Under --force, refresh the managed
112
132
  // entry's fields in place — without disturbing any sibling hooks the user grouped with it.
113
133
  if (force) {
114
- const hk = owning.hooks.find((x) => x.command === command);
134
+ const hk = owning.hooks.find((x) => candidates.includes(x.command));
135
+ hk.command = command;
115
136
  hk.timeout = h.timeout;
116
137
  if (h.statusMessage) hk.statusMessage = h.statusMessage;
117
138
  if (h.matcher && (owning.hooks || []).length === 1) owning.matcher = h.matcher;
@@ -477,12 +498,12 @@ export async function runInit(argv) {
477
498
  const settingsRead = readJsonSafe(settingsPath);
478
499
  if (!settingsRead.ok) {
479
500
  // 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;
501
+ const fresh = mergeSettings(null, { vaultPath, withMcp: args.mcp, force: true, companions, skipMcp, dotcontextHookLevel, projectPath }).settings;
481
502
  writeJson(`${settingsPath}.new`, fresh);
482
503
  log(M.settingsBadJson(settingsPath));
483
504
  } else {
484
505
  const hadFile = settingsRead.data !== null;
485
- const { settings, added } = mergeSettings(settingsRead.data, { vaultPath, withMcp: args.mcp, force: args.force, companions, skipMcp, dotcontextHookLevel });
506
+ const { settings, added } = mergeSettings(settingsRead.data, { vaultPath, withMcp: args.mcp, force: args.force, companions, skipMcp, dotcontextHookLevel, projectPath });
486
507
  if (hadFile) backup(settingsPath);
487
508
  writeJson(settingsPath, settings);
488
509
  log(M.settings(hadFile ? M.merged : M.created, added, hadFile ? M.bakSaved : ''));
@@ -0,0 +1,32 @@
1
+ // `wendkeep renumber-decisions` — retroactive ADR fix. Renumbers every note in 04-Decisões to
2
+ // `ADR-<NNNN>-<slug>` in chronological order, renames the files, and rewrites every wikilink to
3
+ // them across the vault. Preview by default; pass --apply to write. Idempotent.
4
+ import { existsSync } from 'node:fs';
5
+ import { isAbsolute, resolve } from 'node:path';
6
+ import { renumberDecisions } from '../hooks/renumber-decisions.mjs';
7
+
8
+ function opt(argv, name) {
9
+ const i = argv.indexOf(name);
10
+ if (i >= 0) return argv[i + 1];
11
+ const eq = argv.find((a) => a.startsWith(`${name}=`));
12
+ return eq ? eq.slice(name.length + 1) : undefined;
13
+ }
14
+
15
+ export function runRenumberDecisions(argv) {
16
+ const vaultRaw = opt(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
17
+ if (!vaultRaw) { process.stderr.write('wendkeep renumber-decisions: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
18
+ const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
19
+ if (!existsSync(vaultBase)) { process.stderr.write(`wendkeep renumber-decisions: vault not found: ${vaultBase}\n`); process.exit(2); }
20
+
21
+ const apply = argv.includes('--apply');
22
+ const report = renumberDecisions(vaultBase, { apply });
23
+
24
+ if (argv.includes('--json')) { process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); process.exit(0); }
25
+
26
+ process.stdout.write(`${report.total} decisão(ões) · ${report.renamed} a renomear${apply ? ` · ${report.filesTouched} arquivo(s) com links atualizados` : ''}\n`);
27
+ for (const p of report.plan.filter((x) => x.renamed)) {
28
+ process.stdout.write(` ADR-${String(p.num).padStart(4, '0')} ${p.from}\n → ${p.to}\n`);
29
+ }
30
+ if (!apply) process.stdout.write('\nNada foi escrito (preview). Rode com --apply para renomear e atualizar os wikilinks.\n');
31
+ process.exit(0);
32
+ }
@@ -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.
@@ -222,6 +228,12 @@ const WORKFLOW_EN = `# The a2 loop — wendkeep's work cycle
222
228
  Use it when starting any non-trivial change. The loop keeps memory (vault) and proof
223
229
  (sensors) together, wikilinked in the Obsidian graph.
224
230
 
231
+ <HARD-GATE>
232
+ Do NOT edit code files before step 2 (Propose / \`wendkeep change new\`).
233
+ Every non-trivial task goes through the loop — planning in chat and editing right away
234
+ leaves the vault blind. Single exception: a trivial change (typo, one line).
235
+ </HARD-GATE>
236
+
225
237
  ## Steps
226
238
 
227
239
  1. **Explore** — understand the problem before proposing.
@@ -518,21 +530,24 @@ size of the problem>
518
530
  <verifiable criteria: for each requirement, the test that fails → passes>
519
531
  `;
520
532
 
533
+ // As descriptions são o gatilho de ativação: o harness casa a description com o PEDIDO do
534
+ // usuário ("implementa X", "corrige Y"), não com abstrações ("mudança não-trivial"). Gatilhos
535
+ // concretos + instrução imperativa = a skill dispara sozinha (paridade Superpowers).
521
536
  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),
537
+ 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
538
  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 }]),
539
+ skill('wk-debugging', 'Use quando algo falha, quebra, dá erro ou regride — depuração sistemática por hipótese antes de corrigir.', DEBUGGING),
540
+ 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 }]),
541
+ 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
542
  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
543
  ];
529
544
 
530
545
  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),
546
+ 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
547
  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 }]),
548
+ skill('wk-debugging', 'Use when something fails, breaks, errors or regresses — systematic hypothesis-driven debugging before fixing.', DEBUGGING_EN),
549
+ 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 }]),
550
+ 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
551
  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
552
  ];
538
553
 
@@ -545,20 +560,22 @@ export const WK_SKILLS = WK_SKILLS_PT;
545
560
  // Seed each skill into <brainDir>/skills/<name>/ if absent (non-destructive): SKILL.md plus any
546
561
  // bundled template/prompt files. Existing files are never overwritten, so re-seeding an older
547
562
  // install just fills in the new template files alongside its SKILL.md.
548
- export function seedWkSkills(brainDir, localeId = 'pt-BR') {
563
+ // { refresh: true } (sync-defs --reseed) SOBRESCREVE as wk-* com os seeds atuais — é como um
564
+ // vault existente recebe descriptions/HARD-GATE novos (edições manuais nas wk-* são perdidas).
565
+ export function seedWkSkills(brainDir, localeId = 'pt-BR', { refresh = false } = {}) {
549
566
  const created = [];
550
567
  for (const s of wkSkills(localeId)) {
551
568
  const dir = join(brainDir, 'skills', s.name);
552
569
  mkdirSync(dir, { recursive: true });
553
- const writeIfAbsent = (name, content) => {
570
+ const write = (name, content) => {
554
571
  const f = join(dir, name);
555
- if (!existsSync(f)) {
572
+ if (refresh || !existsSync(f)) {
556
573
  writeFileSync(f, content, 'utf8');
557
574
  created.push(f);
558
575
  }
559
576
  };
560
- writeIfAbsent('SKILL.md', s.body);
561
- for (const file of s.files || []) writeIfAbsent(file.name, file.content);
577
+ write('SKILL.md', s.body);
578
+ for (const file of s.files || []) write(file.name, file.content);
562
579
  }
563
580
  return created;
564
581
  }
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
@@ -45,10 +45,16 @@ export const HOOK_FILES = [
45
45
  'session-backfill.mjs',
46
46
  'import-sessions.mjs',
47
47
  'decision-capture.mjs',
48
+ 'renumber-decisions.mjs',
48
49
  'subagent-stop.mjs',
49
50
  'task-log.mjs',
50
51
  'vault-health.mjs',
51
52
  'understand-inject.mjs',
53
+ 'change-context.mjs',
54
+ 'change-warn.mjs',
55
+ 'change-guard.mjs',
56
+ 'change-nag.mjs',
57
+ 'plan-capture.mjs',
52
58
  ];
53
59
 
54
60
  // Hook scripts that are safe to invoke directly via `wendkeep hook <name>`.
@@ -67,6 +73,11 @@ export const RUNNABLE_HOOKS = [
67
73
  'brain-reindex',
68
74
  'vault-health',
69
75
  'understand-inject',
76
+ 'change-context',
77
+ 'change-warn',
78
+ 'change-guard',
79
+ 'change-nag',
80
+ 'plan-capture',
70
81
  ];
71
82
 
72
83
  // The MCP server entry wendkeep wires into .mcp.json so the agent can read/write the
@@ -106,6 +117,27 @@ export function hookCommand(name) {
106
117
  return `npx wendkeep hook ${name}`;
107
118
  }
108
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 node_modules/wendkeep/hooks/${name}.mjs`;
125
+ }
126
+
127
+ // Hooks do lifecycle de change (0.31.0) — enforcement do loop a2. Nudges (contexto/aviso/
128
+ // cobrança/captura de plano) e gate (deny/ask no Bash). Separados em dois grupos para
129
+ // preservar a opção futura de gates opt-in; hoje o init wira TODOS por default.
130
+ // preferLocal: alta frequência → invocação node-direta quando houver instalação local.
131
+ export const CHANGE_NUDGE_HOOKS = [
132
+ { event: 'UserPromptSubmit', matcher: null, name: 'change-context', timeout: 15, order: 10, preferLocal: true, statusMessage: 'wendkeep: change ping' },
133
+ { event: 'PostToolUse', matcher: 'Edit|Write|MultiEdit', name: 'change-warn', timeout: 10, order: 10, preferLocal: true, statusMessage: 'wendkeep: change warn' },
134
+ { event: 'PostToolUse', matcher: 'ExitPlanMode', name: 'plan-capture', timeout: 15, order: 10, preferLocal: true, statusMessage: 'wendkeep: capturing approved plan' },
135
+ { event: 'Stop', matcher: null, name: 'change-nag', timeout: 15, order: 10, preferLocal: true, statusMessage: 'wendkeep: open tasks check' },
136
+ ];
137
+ export const CHANGE_GATE_HOOKS = [
138
+ { event: 'PreToolUse', matcher: 'Bash', name: 'change-guard', timeout: 10, order: 10, preferLocal: true, statusMessage: 'wendkeep: change gate' },
139
+ ];
140
+
109
141
  // --- companion plugins / MCP --------------------------------------------------
110
142
  // Optional tools wendkeep init can pin alongside the vault. Each is wired through
111
143
  // the MOST agent-agnostic mechanism it supports; the Claude Code plugin entry