wendkeep 0.37.0 → 0.38.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/CHANGELOG.md CHANGED
@@ -4,6 +4,27 @@ 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.38.1] — 2026-07-12
8
+
9
+ ### Fixed
10
+
11
+ - Sessão Claude nova não perde mais o 1º turno: `resolveSessionIdentity` usa o `session_id` do hook como identidade canônica quando o transcript ainda não materializou em disco, em vez de adiar a criação da nota. Codex mantém a exigência de rollout/registry — a barreira anti-contaminação do 0.38.0 segue intacta.
12
+ - `task-log` e `subagent-stop` passam a honrar `input.provider` ao resolver a sessão (paridade com `decision-capture`), evitando deferimento falso quando o provider do ambiente diverge do transcript.
13
+
14
+ ## [0.38.0] — 2026-07-12
15
+
16
+ ### Added
17
+
18
+ - Registry multi-sessão v2: `SESSION_REGISTRY.json` passa a ser a autoridade por conversa, enquanto `CURRENT_SESSION.md` vira um dashboard compatível com todas as sessões ativas.
19
+ - `wendkeep session list|show|use` e `wendkeep change bind <slug> --session <id>` permitem inspecionar sessões concorrentes e transferir explicitamente o vínculo de uma change.
20
+ - Metadados de auditoria da observabilidade registram caller, conversa canônica, transcript e instante da atualização.
21
+
22
+ ### Fixed
23
+
24
+ - Roteamento cross-provider agora é fail-closed: Codex usa `session_meta.payload.session_id`, Claude usa o `sessionId` do transcript, e writers não recorrem ao foco global quando a identidade é ambígua.
25
+ - Atualizações concorrentes do registry usam lock, releitura e rename atômico; patches vazios não apagam transcript ou metadados válidos.
26
+ - `cost rebuild` reporta entradas órfãs, ausentes ou incompatíveis como estado não verde em vez de omiti-las silenciosamente.
27
+
7
28
  ## [0.37.0] — 2026-07-11
8
29
 
9
30
  ### Added
package/README.md CHANGED
@@ -119,6 +119,8 @@ No re‑copying, no snapshot to re‑sync — the package is the single source o
119
119
  | `wendkeep cost [--since d]` | Aggregate AI-coding spend across the vault's sessions — total, by model, by day (`--json`). |
120
120
  | `wendkeep import [opts]` | **Retroactive memory** — backfill past **Claude + Codex** sessions into the vault (deduped by `session_id`). `--source all\|claude\|codex` / `--from <dir>` / `--codex-from <dir>` / `--since d` / `--limit n` / `--dry-run` / `--json`. |
121
121
  | `wendkeep cost rebuild [opts]` | Recalcula custos históricos do transcript principal e subagents usando `SESSION_REGISTRY`. Dry-run por padrão; `--apply` atualiza notas e grava `.brain/COST_REBUILD.json`. Aceita `--session`, `--limit` e `--json`. |
122
+ | `wendkeep session list\|show\|use` | Lista o registry multi-sessão, mostra uma conversa ou muda somente o foco humano de `CURRENT_SESSION.md`. |
123
+ | `wendkeep change bind <slug> --session <id>` | Vincula ou transfere uma change para uma conversa canônica sem esconder as demais pendências. |
122
124
 
123
125
  Session notes use one live `## Agentes, tokens e custos` snapshot. Main-agent and subagent hooks recompose it atomically, with costs, token dimensions, reasoning tokens and effort per model/source.
124
126
  | `wendkeep lesson add "t" "l"` | Record a project-local lesson (injected at the next SessionStart). |
package/README.pt-BR.md CHANGED
@@ -116,6 +116,8 @@ Sem recopiar, sem snapshot pra re‑sincronizar — o pacote é a única fonte d
116
116
  | `wendkeep sensors <sub>` | `list` / `add <id> "<comando>"` — vê/edita `wendkeep.sensors.json` (JSON Schema incluso). |
117
117
  | `wendkeep cost [opts]` | Agrega o gasto de IA nas sessões do cofre — total, por modelo, por dia · `--top [N]` · `--trend [day\|week\|month]` (+ projeção) · `--write` (gera `00-Custo.md`) · `--json`. |
118
118
  | `wendkeep cost rebuild [opts]` | Reconstrói custos históricos do transcript principal e subagents via `SESSION_REGISTRY`. Dry-run por padrão; `--apply` grava notas e `.brain/COST_REBUILD.json`. |
119
+ | `wendkeep session list\|show\|use` | Lista o registry multi-sessão, mostra uma conversa ou muda somente o foco humano de `CURRENT_SESSION.md`. |
120
+ | `wendkeep change bind <slug> --session <id>` | Vincula ou transfere uma change para uma conversa canônica sem esconder as demais pendências. |
119
121
 
120
122
  As notas de sessão usam um único snapshot vivo `## Agentes, tokens e custos`. Os hooks do agente principal e dos subagents recompõem o bloco atomicamente, incluindo custo, dimensões de tokens, reasoning e effort por modelo/origem.
121
123
  | `wendkeep stats [--vault P]` | Uma linha compartilhável: sessões · prompts · gasto · período · modelos (`--json`). |
package/bin/wendkeep.mjs CHANGED
@@ -43,9 +43,10 @@ Usage:
43
43
  agent's JSON on stdin. Names: ${RUNNABLE_HOOKS.join(', ')}.
44
44
 
45
45
  wendkeep doctor [--vault P] Run a vault health check.
46
- wendkeep change <sub> Change lifecycle: new [--simple] | use | continue | list | show |
46
+ wendkeep change <sub> Change lifecycle: new [--simple] | use | bind <slug> --session <id> | continue | list | show |
47
47
  status | done <id> | undone <id> | diff | archive [--force] | abandon.
48
48
  archive exige verdict (rode verify --deep); abandon descarta sem ADR.
49
+ wendkeep session <sub> Session registry: list | show <id> | use <id>.
49
50
  wendkeep spec <sub> Specs: list | show | effective [--change] [--json] | migrate | rebase.
50
51
  wendkeep sensors <sub> list | add <id> "<command>" [--severity --type --report].
51
52
  wendkeep cost [opts] Aggregate AI-coding spend across the vault's sessions.
@@ -132,6 +133,11 @@ async function main() {
132
133
  runChange(rest);
133
134
  break;
134
135
  }
136
+ case 'session': {
137
+ const { runSession } = await import('../src/session.mjs');
138
+ runSession(rest);
139
+ break;
140
+ }
135
141
  case 'verify': {
136
142
  const { runVerify } = await import('../src/verify.mjs');
137
143
  runVerify(rest);
@@ -1,85 +1,92 @@
1
- // .agent/hooks/brain-inject.mjs
2
- // Injeção da camada quente no SessionStart (Claude/Codex/Copilot): CORE curado +
1
+ // .agent/hooks/brain-inject.mjs
2
+ // Injeção da camada quente no SessionStart (Claude/Codex/Copilot): CORE curado +
3
3
  // DIGEST auto + 1-linha pointer do recall + backlog completo de changes. Nunca derruba o hook.
4
- // Uso (hook): node .agent/hooks/brain-inject.mjs (input JSON via stdin)
5
- import { readFileSync } from 'node:fs';
6
- import { join } from 'node:path';
7
- import { pathToFileURL } from 'node:url';
8
- import { getVaultBase, readHookInput, writeHookOutput } from './obsidian-common.mjs';
9
- import { brainDir } from './brain-core.mjs';
10
- import { buildActiveChangeInjection, changeCtxState, writeSentinel } from './change-core.mjs';
11
- import { buildLessonsInjection } from './lessons-core.mjs';
12
- import { getLocale } from './locale.mjs';
13
-
14
- // The process ROUTER — the enforcement layer. The wk-* skills are passive files; without a
15
- // standing instruction the model plans in chat, leaves the change scaffold raw and forces the
16
- // gate (seen in production: change archived with `(primeira tarefa)` open, via --force). This
17
- // block is injected EVERY session so planning always routes through the a2 loop.
18
- function processRouter(localeId) {
19
- if (localeId === 'en') {
20
- return [
21
- '<wk_process>',
22
- 'Spec-driven process (mandatory for any non-trivial task): INVOKE the wk-workflow Skill BEFORE editing any file.',
23
- '1. Plan: invoke the wk-brainstorming Skill (approved design) wk-planning (task plan).',
4
+ // Uso (hook): node .agent/hooks/brain-inject.mjs (input JSON via stdin)
5
+ import { readFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { pathToFileURL } from 'node:url';
8
+ import { getVaultBase, readHookInput, writeHookOutput } from './obsidian-common.mjs';
9
+ import { brainDir } from './brain-core.mjs';
10
+ import { buildActiveChangeInjection, changeCtxState, writeSentinel } from './change-core.mjs';
11
+ import { buildLessonsInjection } from './lessons-core.mjs';
12
+ import { getLocale } from './locale.mjs';
13
+ import { resolveSessionEntry } from './session-identity.mjs';
14
+
15
+ // The process ROUTER the enforcement layer. The wk-* skills are passive files; without a
16
+ // standing instruction the model plans in chat, leaves the change scaffold raw and forces the
17
+ // gate (seen in production: change archived with `(primeira tarefa)` open, via --force). This
18
+ // block is injected EVERY session so planning always routes through the a2 loop.
19
+ function processRouter(localeId) {
20
+ if (localeId === 'en') {
21
+ return [
22
+ '<wk_process>',
23
+ 'Spec-driven process (mandatory for any non-trivial task): INVOKE the wk-workflow Skill BEFORE editing any file.',
24
+ '1. Plan: invoke the wk-brainstorming Skill (approved design) → wk-planning (task plan).',
24
25
  '2. Record: `wendkeep change new <slug>` and FILL proposta/design/tasks. Resolve `spec_impact`: `required` needs `specs/<capability>/spec.md` + [req:ID]; `none` needs a reason. Never leave pending/placeholders.',
25
- '3. Implement: wk-tdd per task; tick `- [x]` as you finish. Something broke? wk-debugging.',
26
- '4. Close: `wendkeep verify` (+ `--deep` + the wk-verify Skill) → `wendkeep change archive`.',
27
- 'NEVER `archive --force` on your own — a red gate means pending work; --force is the user\'s call, not yours. Dead end? `wendkeep change abandon`.',
28
- '</wk_process>',
29
- ].join('\n');
30
- }
31
- return [
32
- '<wk_process>',
33
- 'Processo spec-driven (obrigatório em tarefa não-trivial): INVOQUE a Skill wk-workflow ANTES de editar qualquer arquivo.',
34
- '1. Planejar: invoque a Skill wk-brainstorming (design aprovado) → wk-planning (plano de tarefas).',
26
+ '3. Implement: wk-tdd per task; tick `- [x]` as you finish. Something broke? wk-debugging.',
27
+ '4. Close: `wendkeep verify` (+ `--deep` + the wk-verify Skill) → `wendkeep change archive`.',
28
+ 'NEVER `archive --force` on your own — a red gate means pending work; --force is the user\'s call, not yours. Dead end? `wendkeep change abandon`.',
29
+ '</wk_process>',
30
+ ].join('\n');
31
+ }
32
+ return [
33
+ '<wk_process>',
34
+ 'Processo spec-driven (obrigatório em tarefa não-trivial): INVOQUE a Skill wk-workflow ANTES de editar qualquer arquivo.',
35
+ '1. Planejar: invoque a Skill wk-brainstorming (design aprovado) → wk-planning (plano de tarefas).',
35
36
  '2. Registrar: `wendkeep change new <slug>` e PREENCHA proposta/design/tarefas. Resolva `spec_impact`: `required` exige `specs/<capability>/spec.md` + [req:ID]; `none` exige justificativa. Nunca deixe pending/placeholders.',
36
- '3. Implementar: wk-tdd por tarefa; marque `- [x]` ao concluir. Quebrou algo? wk-debugging.',
37
- '4. Fechar: `wendkeep verify` (+ `--deep` + Skill wk-verify) → `wendkeep change archive`.',
38
- 'PROIBIDO `archive --force` por conta própria — gate vermelho significa trabalho pendente; --force é decisão do usuário, não sua. Beco sem saída? `wendkeep change abandon`.',
39
- '</wk_process>',
40
- ].join('\n');
41
- }
42
-
43
- const MAX_LINES = 45; // CORE ≤25 + DIGEST ≤15 + folga; salvaguarda se o CORE crescer à mão
44
-
45
- export function buildInjection(vaultBase) {
46
- const dir = brainDir(vaultBase);
47
- const read = (name) => {
48
- try { return readFileSync(join(dir, name), 'utf8').trim(); } catch { return ''; }
49
- };
50
- const pointer = 'Memória profunda sob demanda: /brain-recall <tópico> (índice .brain/index.jsonl).';
51
- // Quando CORE e DIGEST não existem, ''.split('\n') vira [''] — o filter derruba essa
52
- // linha vazia para o caso "só pointer" ficar com exatamente 3 linhas.
53
- let lines = [read('CORE.md'), read('DIGEST.md')].filter(Boolean).join('\n\n').split('\n').filter((l, i, a) => a.length > 1 || l);
54
- if (lines.length > MAX_LINES) {
55
- lines = lines.slice(0, MAX_LINES);
56
- lines.push('*…truncado pelo budget — fonte completa: .brain/CORE.md + .brain/DIGEST.md*');
57
- }
58
- const brain = ['<brain_memory>', ...lines, pointer, '</brain_memory>'].join('\n');
59
- const router = processRouter(getLocale(vaultBase).id);
60
- const change = buildActiveChangeInjection(vaultBase);
61
- const lessons = buildLessonsInjection(vaultBase);
62
- return [brain, router, change, lessons].filter(Boolean).join('\n');
63
- }
64
-
65
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
66
- try {
67
- const input = readHookInput();
68
- const vaultBase = getVaultBase(input);
69
- writeHookOutput({
70
- hookSpecificOutput: {
71
- hookEventName: 'SessionStart',
72
- additionalContext: buildInjection(vaultBase),
73
- },
74
- });
37
+ '3. Implementar: wk-tdd por tarefa; marque `- [x]` ao concluir. Quebrou algo? wk-debugging.',
38
+ '4. Fechar: `wendkeep verify` (+ `--deep` + Skill wk-verify) → `wendkeep change archive`.',
39
+ 'PROIBIDO `archive --force` por conta própria — gate vermelho significa trabalho pendente; --force é decisão do usuário, não sua. Beco sem saída? `wendkeep change abandon`.',
40
+ '</wk_process>',
41
+ ].join('\n');
42
+ }
43
+
44
+ const MAX_LINES = 45; // CORE ≤25 + DIGEST ≤15 + folga; salvaguarda se o CORE crescer à mão
45
+
46
+ export function buildInjection(vaultBase, input = {}) {
47
+ const dir = brainDir(vaultBase);
48
+ const read = (name) => {
49
+ try { return readFileSync(join(dir, name), 'utf8').trim(); } catch { return ''; }
50
+ };
51
+ const pointer = 'Memória profunda sob demanda: /brain-recall <tópico> (índice .brain/index.jsonl).';
52
+ // Quando CORE e DIGEST não existem, ''.split('\n') vira [''] — o filter derruba essa
53
+ // linha vazia para o caso "só pointer" ficar com exatamente 3 linhas.
54
+ let lines = [read('CORE.md'), read('DIGEST.md')].filter(Boolean).join('\n\n').split('\n').filter((l, i, a) => a.length > 1 || l);
55
+ if (lines.length > MAX_LINES) {
56
+ lines = lines.slice(0, MAX_LINES);
57
+ lines.push('*…truncado pelo budget — fonte completa: .brain/CORE.md + .brain/DIGEST.md*');
58
+ }
59
+ const brain = ['<brain_memory>', ...lines, pointer, '</brain_memory>'].join('\n');
60
+ const router = processRouter(getLocale(vaultBase).id);
61
+ const change = buildActiveChangeInjection(vaultBase);
62
+ const lessons = buildLessonsInjection(vaultBase);
63
+ const { identity, entry } = resolveSessionEntry(vaultBase, input);
64
+ const focus = identity.state === 'resolved' && entry?.change_slug
65
+ ? `<session_change>Change vinculada a esta sessão: ${entry.change_slug}. Este vínculo prevalece para writes automáticos; todas as pendências continuam visíveis acima.</session_change>`
66
+ : '';
67
+ return [brain, router, focus, change, lessons].filter(Boolean).join('\n');
68
+ }
69
+
70
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
71
+ try {
72
+ const input = readHookInput();
73
+ const vaultBase = getVaultBase(input);
74
+ writeHookOutput({
75
+ hookSpecificOutput: {
76
+ hookEventName: 'SessionStart',
77
+ additionalContext: buildInjection(vaultBase, input),
78
+ },
79
+ });
75
80
  // Sentinela do change-context: o backlog completo acabou de ser injetado aqui, então o hook
76
81
  // UserPromptSubmit não precisa re-pingar no 1º prompt. Bônus — nunca derruba a injeção.
77
- try {
78
- const st = changeCtxState(vaultBase);
79
- if (st) writeSentinel(vaultBase, 'ctx', input.session_id || input.sessionId || '', st.hash);
80
- } catch { /* sentinela é bônus */ }
81
- } catch (error) {
82
- process.stderr.write(`[brain] inject falhou: ${error.message}\n`);
83
- writeHookOutput({});
84
- }
85
- }
82
+ try {
83
+ const st = changeCtxState(vaultBase);
84
+ const { identity } = resolveSessionEntry(vaultBase, input);
85
+ const sid = identity.state === 'resolved' ? identity.canonicalConversationId : (input.session_id || input.sessionId || '');
86
+ if (st) writeSentinel(vaultBase, 'ctx', sid, st.hash);
87
+ } catch { /* sentinela é bônus */ }
88
+ } catch (error) {
89
+ process.stderr.write(`[brain] inject falhou: ${error.message}\n`);
90
+ writeHookOutput({});
91
+ }
92
+ }
@@ -8,6 +8,7 @@
8
8
  import { pathToFileURL } from 'node:url';
9
9
  import { getVaultBase, readHookInput, writeHookOutput } from './obsidian-common.mjs';
10
10
  import { changeCtxState, readSentinel, renderOpenChanges, writeSentinel } from './change-core.mjs';
11
+ import { resolveSessionEntry } from './session-identity.mjs';
11
12
 
12
13
  // Conservador de propósito: verbos de tarefa comuns (pt+en) + tamanho mínimo. Falso-negativo
13
14
  // custa só o nudge; falso-positivo em pergunta curta viraria ruído.
@@ -19,12 +20,13 @@ export function looksLikeTask(prompt) {
19
20
  }
20
21
 
21
22
  // Retorna { context, hash? } quando há algo a injetar; null = silêncio.
22
- export function buildChangePing(vaultBase, sessionId, prompt = '') {
23
+ export function buildChangePing(vaultBase, sessionId, prompt = '', changeSlug = '') {
23
24
  const st = changeCtxState(vaultBase);
24
25
  if (st) {
25
26
  if (readSentinel(vaultBase, 'ctx', sessionId) === st.hash) return null;
26
27
  writeSentinel(vaultBase, 'ctx', sessionId, st.hash);
27
- return { context: renderOpenChanges(st, { tag: 'open_changes_ping' }), hash: st.hash };
28
+ const focus = changeSlug ? `\n<session_change>Change vinculada a esta sessão: ${changeSlug}.</session_change>` : '';
29
+ return { context: `${renderOpenChanges(st, { tag: 'open_changes_ping' })}${focus}`, hash: st.hash };
28
30
  }
29
31
  // Sem changes abertas: gate de skill para prompt-tarefa, 1x por sessão.
30
32
  if (!looksLikeTask(prompt)) return null;
@@ -42,8 +44,10 @@ export function buildChangePing(vaultBase, sessionId, prompt = '') {
42
44
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
43
45
  try {
44
46
  const input = readHookInput();
45
- const sid = input.session_id || input.sessionId || '';
46
- const ping = buildChangePing(getVaultBase(input), sid, input.prompt || '');
47
+ const vaultBase = getVaultBase(input);
48
+ const { identity, entry } = resolveSessionEntry(vaultBase, input);
49
+ const sid = identity.state === 'resolved' ? identity.canonicalConversationId : (input.session_id || input.sessionId || '');
50
+ const ping = buildChangePing(vaultBase, sid, input.prompt || '', entry?.change_slug || '');
47
51
  if (!ping) { writeHookOutput({}); }
48
52
  else writeHookOutput({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: ping.context } });
49
53
  } catch {
@@ -12,6 +12,7 @@ import {
12
12
  wikilinkFromRel, readControl, toVaultRelative, getNextAdrNumber, derivedContentKey,
13
13
  } from './obsidian-common.mjs';
14
14
  import { getLocale } from './locale.mjs';
15
+ import { resolveSessionEntry } from './session-identity.mjs';
15
16
 
16
17
  // Decision notes follow the ADR naming convention: ADR-NNNN-<slug>, NNNN a 4-digit sequential
17
18
  // number assigned in the order decisions are made (getNextAdrNumber scans the whole 04-Decisões).
@@ -168,8 +169,9 @@ export function captureDecision(vaultBase, input) {
168
169
  const dateStr = formatDate(now);
169
170
  const provider = providerMeta(input.provider);
170
171
 
171
- const matched = input.transcript_path ? findActiveSessionByTranscript(vaultBase, input.transcript_path) : null;
172
- const sessionRel = matched?.session_file || readControl(vaultBase).session_file || '';
172
+ const { identity, entry } = resolveSessionEntry(vaultBase, input, provider.id);
173
+ if (identity.state !== 'resolved') return null;
174
+ const sessionRel = entry?.session_file || '';
173
175
 
174
176
  const dir = join(vaultBase, monthFolderRelFromDateStr(loc.folders.decisions, dateStr, vaultBase));
175
177
  ensureDir(dir);