wendkeep 0.31.0 → 0.33.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/CHANGELOG.md +63 -0
- package/README.md +5 -3
- package/README.pt-BR.md +3 -3
- package/hooks/brain-inject.mjs +5 -5
- package/hooks/change-context.mjs +6 -15
- package/hooks/change-core.mjs +99 -24
- package/hooks/decision-capture.mjs +41 -2
- package/hooks/harness-doctor.mjs +4 -1
- package/hooks/plan-capture.mjs +61 -9
- package/hooks/spec-core.mjs +49 -4
- package/hooks/vault-health.mjs +6 -5
- package/package.json +1 -1
- package/src/change.mjs +19 -6
- package/src/init.mjs +26 -5
- package/src/skills-seed.mjs +38 -6
- package/src/taxonomy.mjs +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,69 @@ 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.33.0] — 2026-07-11
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Visão global de changes abertas**: `SessionStart`, `UserPromptSubmit`, `wendkeep change list`
|
|
12
|
+
e `wendkeep change status` sem slug mostram todas as pendências, inclusive as iniciadas por
|
|
13
|
+
outro agente.
|
|
14
|
+
- Ações de takeover explícitas no contexto: Claude, Codex ou outro agente podem retomar uma
|
|
15
|
+
change existente sem perder o restante do backlog.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- `.brain/CURRENT_CHANGE.md` continua como ponteiro global único, agora marcado como change
|
|
20
|
+
**atual**. Comandos implícitos (`done`, `verify`, `archive`, `abandon`) continuam restritos a ela.
|
|
21
|
+
- Mudanças em qualquer `tarefas.md` invalidam o hash do hook de contexto e reinjetam a lista global
|
|
22
|
+
na sessão afetada.
|
|
23
|
+
- `change-nag` permanece local à change atual; pendências de outra frente não bloqueiam o agente
|
|
24
|
+
em foco.
|
|
25
|
+
|
|
26
|
+
## [0.32.0] — 2026-07-09
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **Contrato explícito `spec_impact`** para changes novas: `pending`, `required` ou `none`.
|
|
31
|
+
Changes materiais (`required`) precisam listar a capability e manter um delta real em
|
|
32
|
+
`specs/<capability>/spec.md`; `none` exige justificativa em `spec_impact_reason`.
|
|
33
|
+
- **Snapshots imutáveis de planos aprovados** em `planos/<sha256-12>.md`, deduplicados por
|
|
34
|
+
conteúdo. `plano-aprovado.md` passa a ser o índice dos snapshots, sem sobrescrever planos
|
|
35
|
+
anteriores da mesma change.
|
|
36
|
+
- Diagnósticos de `spec_impact` no `wendkeep doctor`, incluindo estado pendente, delta ausente e
|
|
37
|
+
divergência entre `specs:` e o conteúdo real no disco.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- Hooks Claude de alta frequência usam `node` com caminho ancorado em
|
|
42
|
+
`${CLAUDE_PROJECT_DIR}`. O `init` migra automaticamente os comandos relativos de 0.31.0 sem
|
|
43
|
+
duplicar grupos, inclusive quando o agente muda o `cwd` para um subprojeto.
|
|
44
|
+
- `wk-workflow`, `wk-brainstorming`, `wk-planning` e o roteador de SessionStart agora exigem a
|
|
45
|
+
classificação do impacto, o delta da capability e a rastreabilidade `[req:ID]` antes do archive.
|
|
46
|
+
- O archive passa a ser **fail-closed** para specs: placeholder, arquivo ausente ou falha de
|
|
47
|
+
promoção bloqueiam o move e a criação do ADR.
|
|
48
|
+
- Ao arquivar, links da sessão para a change ativa são reescritos para o caminho em `_arquivo`.
|
|
49
|
+
Decisões capturadas também entram imediatamente na seção de decisões da sessão ativa.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- **Planos aprovados descartados no Claude Code atual**: `plan-capture` agora lê
|
|
54
|
+
`tool_response.plan` no payload estruturado de `PostToolUse:ExitPlanMode`, mantendo os formatos
|
|
55
|
+
textuais legados e rejeições como no-op.
|
|
56
|
+
- **52 falhas `MODULE_NOT_FOUND` observadas em produção** quando `change-warn`/`change-guard`
|
|
57
|
+
rodavam a partir de `mobile-app` ou `backend-core`.
|
|
58
|
+
- A criação automática de change por plano aprovado agora preserva o backlink da sessão resolvido
|
|
59
|
+
pelo `transcript_path`/registry.
|
|
60
|
+
- `vault-health` não exige `## Encerramento` de uma sessão ainda ativa; continua validando a ordem
|
|
61
|
+
completa quando a sessão está finalizada.
|
|
62
|
+
|
|
63
|
+
### Migration
|
|
64
|
+
|
|
65
|
+
- Rode `wendkeep init --force` para migrar hooks relativos instalados pela 0.31.0.
|
|
66
|
+
- Rode `wendkeep sync-defs --reseed` para atualizar as skills wk-* em vaults existentes.
|
|
67
|
+
- Changes antigas sem `spec_impact` continuam legíveis e são diagnosticadas como legadas; antes do
|
|
68
|
+
próximo archive, classifique-as explicitamente como `required` ou `none`.
|
|
69
|
+
|
|
7
70
|
## [0.31.0] — 2026-07-09
|
|
8
71
|
|
|
9
72
|
### Added — enforcement do loop a2 (o loop deixa de ser opcional na prática)
|
package/README.md
CHANGED
|
@@ -112,7 +112,7 @@ No re‑copying, no snapshot to re‑sync — the package is the single source o
|
|
|
112
112
|
|---|---|
|
|
113
113
|
| `wendkeep init` | Set up wendkeep in a project (vault taxonomy + settings + MCP + skills). |
|
|
114
114
|
| `wendkeep hook <name>` | Run a session hook; invoked by `settings.json` (reads agent JSON on stdin). |
|
|
115
|
-
| `wendkeep change <sub>` | Change lifecycle: `new [--simple]` / `list` / `show` / `status` / `done <id
|
|
115
|
+
| `wendkeep change <sub>` | Change lifecycle: `new [--simple]` / `list` (global backlog) / `show` / `status [slug]` / `done <id> [--change slug]` / `undone <id> [--change slug]` / `diff` / `archive [--force]`. |
|
|
116
116
|
| `wendkeep verify [--deep]` | Run the change's task sensors; `--deep` assembles the independent-verification package. |
|
|
117
117
|
| `wendkeep spec <sub>` | Living specs: `list` / `show <capability>`. |
|
|
118
118
|
| `wendkeep sensors <sub>` | `list` / `add <id> "<command>"` — view/edit `wendkeep.sensors.json` (JSON Schema shipped). |
|
|
@@ -148,7 +148,7 @@ Beyond capturing sessions, wendkeep is a **harness**: a native, zero‑dependenc
|
|
|
148
148
|
explore → propose → apply (TDD) → verify → archive
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
-
- **Propose** — `wendkeep change new <slug>` scaffolds `08-Mudanças/<slug>/` (`proposta.md`, `design.md`, `tarefas.md`, and a `specs/` delta).
|
|
151
|
+
- **Propose** — `wendkeep change new <slug>` scaffolds `08-Mudanças/<slug>/` (`proposta.md`, `design.md`, `tarefas.md`, and a `specs/` delta). It becomes the global *current* change. Multiple changes may remain open: hooks and `change list/status` expose every pending task, while commands without `--change` use only the current one.
|
|
152
152
|
- **Apply** — implement each `tarefas.md` task. Tag a task that needs machine proof with `[sensor:<id>]`.
|
|
153
153
|
- **Verify** — `wendkeep verify` runs the sensors your tasks declared (from `wendkeep.sensors.json` at the project root) and records `evidencia.json`. A critical red fails the gate; a `warning` red is advisory.
|
|
154
154
|
- **Archive** — `wendkeep change archive <slug>` **gates** on the evidence (blocks unless every declared critical sensor is green), promotes each capability's spec delta (`ADDED`/`MODIFIED`/`REMOVED`) into the living `07-Specs/<capability>.md`, moves the change to `_arquivo/`, and mints an ADR in `04-Decisões/`.
|
|
@@ -175,6 +175,8 @@ Declare the capability in `proposta.md` (`specs: [ui]`) and author its delta in
|
|
|
175
175
|
|
|
176
176
|
```bash
|
|
177
177
|
npx wendkeep change status # one screen: tasks / sensors / verdict
|
|
178
|
+
npx wendkeep change list # all open changes + pending tasks
|
|
179
|
+
npx wendkeep change status dark-mode # one change in detail
|
|
178
180
|
npx wendkeep change done 1.1 # tick a task from the CLI
|
|
179
181
|
npx wendkeep verify # run the declared sensors -> evidencia.json
|
|
180
182
|
npx wendkeep verify --deep # assemble the verification package
|
|
@@ -194,7 +196,7 @@ agent session ──hooks──▶ wendkeep ──▶ Markdown in vault ──
|
|
|
194
196
|
(Claude/Codex) (Node) (02-Sessões/…) (CORE+DIGEST, backlinks)
|
|
195
197
|
```
|
|
196
198
|
|
|
197
|
-
The agent's settings.json points each hook at `npx wendkeep hook …`. On `Stop`, wendkeep parses the session transcript, appends the turn, updates the token/cost table, and (idempotently) emits any decision/bug/learning notes. On every `SessionStart`, `brain-inject` injects back
|
|
199
|
+
The agent's settings.json points each hook at `npx wendkeep hook …`. On `Stop`, wendkeep parses the session transcript, appends the turn, updates the token/cost table, and (idempotently) emits any decision/bug/learning notes. On every `SessionStart`, `brain-inject` injects back curated memory (CORE + DIGEST), every open change with its pending tasks, the global current-change marker, project lessons, and a `<wk_process>` router. Claude, Codex, or another agent can therefore resume work started elsewhere without hiding the rest of the backlog.
|
|
198
200
|
|
|
199
201
|
The archive **gate** blocks unless: the change scaffold is filled (G0), no task is open (G1), every declared critical sensor is green (with fresh evidence), and — when the change declares `[req:]` — an independent `verdict.json` covers them. `--force` is the human escape hatch; the agent is instructed never to use it on its own.
|
|
200
202
|
|
package/README.pt-BR.md
CHANGED
|
@@ -110,7 +110,7 @@ Sem recopiar, sem snapshot pra re‑sincronizar — o pacote é a única fonte d
|
|
|
110
110
|
|---|---|
|
|
111
111
|
| `wendkeep init` | Configura o wendkeep num projeto (taxonomia do cofre + settings + MCP + skills). |
|
|
112
112
|
| `wendkeep hook <name>` | Roda um hook de sessão; invocado pelo `settings.json` (lê o JSON do agente no stdin). |
|
|
113
|
-
| `wendkeep change <sub>` | Ciclo de mudança: `new [--simple]` / `list` / `show` / `status` / `done <id
|
|
113
|
+
| `wendkeep change <sub>` | Ciclo de mudança: `new [--simple]` / `list` (backlog global) / `show` / `status [slug]` / `done <id> [--change slug]` / `undone <id> [--change slug]` / `diff` / `archive [--force]`. |
|
|
114
114
|
| `wendkeep verify [--deep]` | Roda os sensores das tarefas da change; `--deep` monta o pacote de verificação independente. |
|
|
115
115
|
| `wendkeep spec <sub>` | Specs vivos: `list` / `show <capability>`. |
|
|
116
116
|
| `wendkeep sensors <sub>` | `list` / `add <id> "<comando>"` — vê/edita `wendkeep.sensors.json` (JSON Schema incluso). |
|
|
@@ -147,7 +147,7 @@ Além de capturar sessões, o wendkeep é um **harness**: um loop nativo e sem d
|
|
|
147
147
|
explore → propose → apply (TDD) → verify → archive
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
-
- **Propose** — `wendkeep change new <slug>` faz o scaffold de `08-Mudanças/<slug>/` (`proposta.md`, `design.md`, `tarefas.md` e um delta `specs/`). A change vira *
|
|
150
|
+
- **Propose** — `wendkeep change new <slug>` faz o scaffold de `08-Mudanças/<slug>/` (`proposta.md`, `design.md`, `tarefas.md` e um delta `specs/`). A change vira a *atual* global. Várias changes podem ficar abertas: hooks e `change list/status` mostram todas as pendências, enquanto comandos sem `--change` usam somente a atual.
|
|
151
151
|
- **Apply** — implemente cada tarefa de `tarefas.md`. Taggeie a tarefa que precisa de prova de máquina com `[sensor:<id>]`.
|
|
152
152
|
- **Verify** — `wendkeep verify` roda os sensores que suas tarefas declararam (do `wendkeep.sensors.json` na raiz do projeto) e grava `evidencia.json`. Um vermelho crítico falha o gate; um vermelho `warning` é aviso.
|
|
153
153
|
- **Archive** — `wendkeep change archive <slug>` faz **gate** na evidência (bloqueia a não ser que todo sensor crítico declarado esteja verde), promove o delta de cada capability (`ADDED`/`MODIFIED`/`REMOVED`) pro `07-Specs/<capability>.md` vivo, move a change pro `_arquivo/` e cunha um ADR em `04-Decisões/`.
|
|
@@ -163,7 +163,7 @@ sessão do agente ──hooks──▶ wendkeep ──▶ Markdown no cofre ─
|
|
|
163
163
|
(Claude/Codex) (Node) (02-Sessões/…) (CORE+DIGEST, backlinks)
|
|
164
164
|
```
|
|
165
165
|
|
|
166
|
-
O settings.json do agente aponta cada hook pra `npx wendkeep hook …`. No `Stop`, o wendkeep parseia o transcript, anexa o turno, atualiza a tabela de tokens/custo e (idempotentemente) emite qualquer nota de decisão/bug/aprendizado. Em todo `SessionStart`, o `brain-inject` injeta
|
|
166
|
+
O settings.json do agente aponta cada hook pra `npx wendkeep hook …`. No `Stop`, o wendkeep parseia o transcript, anexa o turno, atualiza a tabela de tokens/custo e (idempotentemente) emite qualquer nota de decisão/bug/aprendizado. Em todo `SessionStart`, o `brain-inject` injeta a memória curada (CORE + DIGEST), todas as changes abertas com suas pendências, o marcador global da change atual, as lições do projeto e o roteador `<wk_process>`. Claude, Codex ou outro agente podem assim retomar trabalho iniciado em outro lugar sem ocultar o restante do backlog.
|
|
167
167
|
|
|
168
168
|
O **gate** do archive bloqueia a não ser que: o scaffold da change esteja preenchido (G0), nenhuma tarefa esteja aberta (G1), todo sensor crítico declarado esteja verde (com evidência fresca) e — quando a change declara `[req:]` — um `verdict.json` independente cubra eles. O `--force` é a saída de emergência humana; o agente é instruído a nunca usar por conta própria.
|
|
169
169
|
|
package/hooks/brain-inject.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// .agent/hooks/brain-inject.mjs
|
|
2
2
|
// Injeção da camada quente no SessionStart (Claude/Codex/Copilot): CORE curado +
|
|
3
|
-
// DIGEST auto + 1-linha pointer do recall
|
|
3
|
+
// DIGEST auto + 1-linha pointer do recall + backlog completo de changes. Nunca derruba o hook.
|
|
4
4
|
// Uso (hook): node .agent/hooks/brain-inject.mjs (input JSON via stdin)
|
|
5
5
|
import { readFileSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
@@ -21,7 +21,7 @@ function processRouter(localeId) {
|
|
|
21
21
|
'<wk_process>',
|
|
22
22
|
'Spec-driven process (mandatory for any non-trivial task): INVOKE the wk-workflow Skill BEFORE editing any file.',
|
|
23
23
|
'1. Plan: invoke the wk-brainstorming Skill (approved design) → wk-planning (task plan).',
|
|
24
|
-
'2. Record: `wendkeep change new <slug>` and FILL
|
|
24
|
+
'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
25
|
'3. Implement: wk-tdd per task; tick `- [x]` as you finish. Something broke? wk-debugging.',
|
|
26
26
|
'4. Close: `wendkeep verify` (+ `--deep` + the wk-verify Skill) → `wendkeep change archive`.',
|
|
27
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`.',
|
|
@@ -32,7 +32,7 @@ function processRouter(localeId) {
|
|
|
32
32
|
'<wk_process>',
|
|
33
33
|
'Processo spec-driven (obrigatório em tarefa não-trivial): INVOQUE a Skill wk-workflow ANTES de editar qualquer arquivo.',
|
|
34
34
|
'1. Planejar: invoque a Skill wk-brainstorming (design aprovado) → wk-planning (plano de tarefas).',
|
|
35
|
-
'2. Registrar: `wendkeep change new <slug>` e PREENCHA
|
|
35
|
+
'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
36
|
'3. Implementar: wk-tdd por tarefa; marque `- [x]` ao concluir. Quebrou algo? wk-debugging.',
|
|
37
37
|
'4. Fechar: `wendkeep verify` (+ `--deep` + Skill wk-verify) → `wendkeep change archive`.',
|
|
38
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`.',
|
|
@@ -72,8 +72,8 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
|
|
72
72
|
additionalContext: buildInjection(vaultBase),
|
|
73
73
|
},
|
|
74
74
|
});
|
|
75
|
-
// Sentinela do change-context:
|
|
76
|
-
// UserPromptSubmit não precisa re-pingar no 1º prompt. Bônus — nunca derruba a injeção.
|
|
75
|
+
// Sentinela do change-context: o backlog completo acabou de ser injetado aqui, então o hook
|
|
76
|
+
// UserPromptSubmit não precisa re-pingar no 1º prompt. Bônus — nunca derruba a injeção.
|
|
77
77
|
try {
|
|
78
78
|
const st = changeCtxState(vaultBase);
|
|
79
79
|
if (st) writeSentinel(vaultBase, 'ctx', input.session_id || input.sessionId || '', st.hash);
|
package/hooks/change-context.mjs
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// UserPromptSubmit hook. Dois papéis, ambos quiet-by-default (a maioria dos prompts não injeta nada):
|
|
3
|
-
// 1. Com
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// 2. Sem change ativa: prompt com cara de tarefa ganha <wk_skill_gate> mandando invocar a
|
|
3
|
+
// 1. Com changes abertas: re-injeta <open_changes_ping> (backlog completo) SÓ quando o estado
|
|
4
|
+
// mudou desde a última injeção (hash em sentinela por sessão).
|
|
5
|
+
// 2. Sem changes abertas: prompt com cara de tarefa ganha <wk_skill_gate> mandando invocar a
|
|
7
6
|
// Skill wk-workflow ANTES de editar — 1x por sessão. É o empurrão de ativação da skill.
|
|
8
7
|
// Fail-open; brain-inject grava a sentinela ctx no SessionStart para não duplicar no 1º prompt.
|
|
9
8
|
import { pathToFileURL } from 'node:url';
|
|
10
9
|
import { getVaultBase, readHookInput, writeHookOutput } from './obsidian-common.mjs';
|
|
11
|
-
import { changeCtxState, readSentinel, writeSentinel } from './change-core.mjs';
|
|
10
|
+
import { changeCtxState, readSentinel, renderOpenChanges, writeSentinel } from './change-core.mjs';
|
|
12
11
|
|
|
13
12
|
// Conservador de propósito: verbos de tarefa comuns (pt+en) + tamanho mínimo. Falso-negativo
|
|
14
13
|
// custa só o nudge; falso-positivo em pergunta curta viraria ruído.
|
|
@@ -25,17 +24,9 @@ export function buildChangePing(vaultBase, sessionId, prompt = '') {
|
|
|
25
24
|
if (st) {
|
|
26
25
|
if (readSentinel(vaultBase, 'ctx', sessionId) === st.hash) return null;
|
|
27
26
|
writeSentinel(vaultBase, 'ctx', sessionId, st.hash);
|
|
28
|
-
|
|
29
|
-
const context = [
|
|
30
|
-
'<active_change_ping>',
|
|
31
|
-
`Mudança ativa: ${st.slug}${st.openTasks.length ? ` — tarefa(s) aberta(s):` : ' — sem tarefas abertas.'}`,
|
|
32
|
-
...lines,
|
|
33
|
-
'Ao concluir uma tarefa: `wendkeep change done <id>`. Antes de arquivar: `wendkeep verify`.',
|
|
34
|
-
'</active_change_ping>',
|
|
35
|
-
].join('\n');
|
|
36
|
-
return { context, hash: st.hash };
|
|
27
|
+
return { context: renderOpenChanges(st, { tag: 'open_changes_ping' }), hash: st.hash };
|
|
37
28
|
}
|
|
38
|
-
// Sem
|
|
29
|
+
// Sem changes abertas: gate de skill para prompt-tarefa, 1x por sessão.
|
|
39
30
|
if (!looksLikeTask(prompt)) return null;
|
|
40
31
|
if (readSentinel(vaultBase, 'gate', sessionId)) return null;
|
|
41
32
|
writeSentinel(vaultBase, 'gate', sessionId);
|
package/hooks/change-core.mjs
CHANGED
|
@@ -14,9 +14,13 @@ export function changeDirRel(slug, vaultBase) {
|
|
|
14
14
|
return join(getLocale(vaultBase).folders.changes, slug);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export function renderChangeScaffold({ slug, sessionRel, dateStr, locale = 'pt-BR' }) {
|
|
17
|
+
export function renderChangeScaffold({ slug, sessionRel, dateStr, locale = 'pt-BR', simple = false }) {
|
|
18
18
|
const en = locale === 'en';
|
|
19
19
|
const source = sessionRel ? `\n - "${wikilinkFromRel(sessionRel)}"` : ' []';
|
|
20
|
+
const impact = simple ? 'none' : 'pending';
|
|
21
|
+
const impactReason = simple
|
|
22
|
+
? (en ? 'Simple change with no product-contract impact.' : 'Mudança simples sem impacto no contrato do produto.')
|
|
23
|
+
: '';
|
|
20
24
|
const proposta = `---
|
|
21
25
|
type: change
|
|
22
26
|
status: active
|
|
@@ -26,6 +30,8 @@ cssclasses:
|
|
|
26
30
|
tags:
|
|
27
31
|
- mudanca
|
|
28
32
|
source:${source}
|
|
33
|
+
spec_impact: ${impact}
|
|
34
|
+
spec_impact_reason: ${JSON.stringify(impactReason)}
|
|
29
35
|
specs: []
|
|
30
36
|
---
|
|
31
37
|
|
|
@@ -96,13 +102,14 @@ export function newChange(vaultBase, slug, { sessionRel = '', dateStr, simple =
|
|
|
96
102
|
const dir = join(vaultBase, loc.folders.changes, slug);
|
|
97
103
|
const existed = existsSync(join(dir, 'proposta.md'));
|
|
98
104
|
mkdirSync(dir, { recursive: true });
|
|
99
|
-
const files = renderChangeScaffold({ slug, sessionRel, dateStr, locale: loc.id });
|
|
105
|
+
const files = renderChangeScaffold({ slug, sessionRel, dateStr, locale: loc.id, simple });
|
|
100
106
|
const write = (name, content) => {
|
|
101
107
|
const f = join(dir, name);
|
|
102
108
|
if (!existsSync(f)) writeFileSync(f, content, 'utf8');
|
|
103
109
|
};
|
|
104
110
|
write('proposta.md', files.proposta);
|
|
105
111
|
write('tarefas.md', files.tarefas);
|
|
112
|
+
if (!existed) write('.spec-impact-v1', '1\n');
|
|
106
113
|
// Auto-sizing (Wave B): a --simple change skips the design + spec-delta scaffold.
|
|
107
114
|
if (!simple) {
|
|
108
115
|
write('design.md', files.design);
|
|
@@ -160,22 +167,67 @@ export function listChanges(vaultBase) {
|
|
|
160
167
|
try {
|
|
161
168
|
archived = readdirSync(join(base, ARCHIVE_DIR)).filter((n) => !n.startsWith('.'));
|
|
162
169
|
} catch { /* none */ }
|
|
163
|
-
return { active, archived };
|
|
170
|
+
return { active: active.sort(), archived: archived.sort() };
|
|
164
171
|
}
|
|
165
172
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
173
|
+
// Visão derivada de TODAS as changes abertas. CURRENT_CHANGE continua sendo o único foco para
|
|
174
|
+
// comandos implícitos; provider/session nunca filtram a fila, para que outro agente possa assumir
|
|
175
|
+
// o trabalho. O hash leva o conteúdo inteiro de cada tarefas.md — não apenas as contagens — pois
|
|
176
|
+
// ele controla a reinjeção por sessão dos hooks.
|
|
177
|
+
export function allChangesState(vaultBase) {
|
|
178
|
+
const current = activeChange(vaultBase);
|
|
179
|
+
const { active } = listChanges(vaultBase);
|
|
169
180
|
const chDir = getLocale(vaultBase).folders.changes;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
181
|
+
const fingerprint = [`current:${current}`];
|
|
182
|
+
const changes = active.map((slug) => {
|
|
183
|
+
let md = '';
|
|
184
|
+
let warning = '';
|
|
185
|
+
try { md = readFileSync(join(vaultBase, chDir, slug, 'tarefas.md'), 'utf8'); }
|
|
186
|
+
catch { warning = 'tarefas.md ausente ou ilegível'; }
|
|
187
|
+
const tasks = parseTasks(md);
|
|
188
|
+
const openTasks = tasks.filter((t) => !t.done);
|
|
189
|
+
fingerprint.push(`slug:${slug}`, `tasks:${md}`, `warning:${warning}`);
|
|
190
|
+
return {
|
|
191
|
+
slug,
|
|
192
|
+
current: slug === current,
|
|
193
|
+
openTasks,
|
|
194
|
+
openCount: openTasks.length,
|
|
195
|
+
doneCount: tasks.length - openTasks.length,
|
|
196
|
+
warning,
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
changes.sort((a, b) => Number(b.current) - Number(a.current) || a.slug.localeCompare(b.slug));
|
|
200
|
+
const pointerWarning = current && !changes.some((change) => change.current)
|
|
201
|
+
? `CURRENT_CHANGE aponta para change inexistente: ${current}`
|
|
202
|
+
: '';
|
|
203
|
+
if (pointerWarning) fingerprint.push(`pointer-warning:${pointerWarning}`);
|
|
204
|
+
return { current, changes, pointerWarning, hash: tasksHashOf(fingerprint.join('\n')) };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function renderOpenChanges(state, { tag = 'open_changes' } = {}) {
|
|
208
|
+
if (!state?.changes?.length && !state?.pointerWarning) return '';
|
|
209
|
+
const lines = [];
|
|
210
|
+
if (tag) lines.push(`<${tag}>`);
|
|
211
|
+
if (state.current) lines.push(`Change atual (comandos sem --change): ${state.current}.`);
|
|
212
|
+
else lines.push('Nenhuma change atual selecionada; comandos sem --change continuam recusados.');
|
|
213
|
+
if (state.pointerWarning) lines.push(`Aviso: ${state.pointerWarning}.`);
|
|
214
|
+
for (const change of state.changes || []) {
|
|
215
|
+
const label = change.current ? 'ATUAL' : 'ABERTA';
|
|
216
|
+
lines.push(`### ${label} — ${change.slug} (${change.openCount} aberta(s), ${change.doneCount} concluída(s))`);
|
|
217
|
+
if (change.warning) lines.push(`- Aviso: ${change.warning}.`);
|
|
218
|
+
else if (!change.openTasks.length) lines.push('- Nenhuma tarefa aberta.');
|
|
219
|
+
else for (const task of change.openTasks) lines.push(`- [ ] ${task.id} ${task.text}`);
|
|
220
|
+
}
|
|
221
|
+
if (state.current) lines.push('Para change atual: `wendkeep change done <id>`; antes de archive: `wendkeep verify`.');
|
|
222
|
+
lines.push('Qualquer agente pode assumir uma change: selecione-a com `wendkeep change new <slug-existente>` ou use `--change <slug>` quando disponível.');
|
|
223
|
+
if (tag) lines.push(`</${tag}>`);
|
|
224
|
+
return lines.join('\n');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Mantém o nome exportado para consumidores internos existentes, mas agora injeta o backlog
|
|
228
|
+
// completo em vez de ocultar changes não selecionadas.
|
|
229
|
+
export function buildActiveChangeInjection(vaultBase) {
|
|
230
|
+
return renderOpenChanges(allChangesState(vaultBase));
|
|
179
231
|
}
|
|
180
232
|
|
|
181
233
|
export function activeChangeLink(vaultBase) {
|
|
@@ -225,14 +277,18 @@ export function writeSentinel(vaultBase, kind, sid, value = '1') {
|
|
|
225
277
|
} catch { /* fail-open: pior caso = aviso repetido */ }
|
|
226
278
|
}
|
|
227
279
|
|
|
228
|
-
// Estado
|
|
229
|
-
// change
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
return {
|
|
280
|
+
// Estado global usado pelo change-context: hash cobre qualquer tarefa aberta, inclusive de uma
|
|
281
|
+
// change que não esteja no ponteiro. As propriedades slug/openTasks preservam compatibilidade com
|
|
282
|
+
// consumidores antigos e descrevem somente a atual.
|
|
283
|
+
export function changeCtxState(vaultBase) {
|
|
284
|
+
const state = allChangesState(vaultBase);
|
|
285
|
+
if (!state.changes.length && !state.pointerWarning) return null;
|
|
286
|
+
const selected = state.changes.find((change) => change.current);
|
|
287
|
+
return {
|
|
288
|
+
...state,
|
|
289
|
+
slug: state.current,
|
|
290
|
+
openTasks: selected?.openTasks || [],
|
|
291
|
+
};
|
|
236
292
|
}
|
|
237
293
|
|
|
238
294
|
// GC das sentinelas (>7 dias) — seleção pura separada da execução (testável sem depender de
|
|
@@ -291,6 +347,12 @@ export function archiveChange(vaultBase, slug, { gate = gateGreen, dateStr, adrN
|
|
|
291
347
|
const loc = getLocale(vaultBase);
|
|
292
348
|
const chDir = loc.folders.changes;
|
|
293
349
|
const src = join(vaultBase, chDir, slug);
|
|
350
|
+
let sourceSessionRel = '';
|
|
351
|
+
try {
|
|
352
|
+
const proposal = readFileSync(join(src, 'proposta.md'), 'utf8');
|
|
353
|
+
const m = proposal.match(/\[\[((?:02-Sessões|02-Sessions)\/[^\]|]+?)(?:\|[^\]]+)?\]\]/);
|
|
354
|
+
if (m) sourceSessionRel = m[1].endsWith('.md') ? m[1] : `${m[1]}.md`;
|
|
355
|
+
} catch { /* sem source */ }
|
|
294
356
|
const verdict = gate(src);
|
|
295
357
|
if (!verdict.ok) return { ok: false, failing: verdict.failing || [] };
|
|
296
358
|
|
|
@@ -324,7 +386,9 @@ export function archiveChange(vaultBase, slug, { gate = gateGreen, dateStr, adrN
|
|
|
324
386
|
...res.warnings,
|
|
325
387
|
];
|
|
326
388
|
}
|
|
327
|
-
} catch {
|
|
389
|
+
} catch (error) {
|
|
390
|
+
return { ok: false, failing: [`falha ao promover specs: ${error.message}`] };
|
|
391
|
+
}
|
|
328
392
|
|
|
329
393
|
let reqIds = [];
|
|
330
394
|
try { reqIds = [...new Set(parseTasks(readFileSync(join(src, 'tarefas.md'), 'utf8')).map((t) => t.req).filter(Boolean))]; } catch { /* sem tarefas */ }
|
|
@@ -343,6 +407,17 @@ export function archiveChange(vaultBase, slug, { gate = gateGreen, dateStr, adrN
|
|
|
343
407
|
writeFileSync(pp, c, 'utf8');
|
|
344
408
|
} catch { /* proposta ilegível — segue */ }
|
|
345
409
|
|
|
410
|
+
// A sessão guardava o link da change ativa; após o move, reescreva para o caminho arquivado.
|
|
411
|
+
if (sourceSessionRel) {
|
|
412
|
+
try {
|
|
413
|
+
const sessionPath = join(vaultBase, sourceSessionRel);
|
|
414
|
+
const oldLink = wikilinkFromRel(join(chDir, slug, 'proposta'));
|
|
415
|
+
const archivedLink = wikilinkFromRel(join(destRel, 'proposta'));
|
|
416
|
+
const current = readFileSync(sessionPath, 'utf8');
|
|
417
|
+
if (current.includes(oldLink)) writeFileSync(sessionPath, current.replaceAll(oldLink, archivedLink), 'utf8');
|
|
418
|
+
} catch { /* backlink é reparo auxiliar; archive já está íntegro */ }
|
|
419
|
+
}
|
|
420
|
+
|
|
346
421
|
// ADR goes in the same dated month folder as session-derived decisions (04-Decisões/ano/MM-MMM/)
|
|
347
422
|
// — not the year root — so all ADRs sit together in the vault's convention.
|
|
348
423
|
const adrDirRel = monthFolderRelFromDateStr(loc.folders.decisions, dateStr, vaultBase);
|
|
@@ -33,9 +33,20 @@ export function decisionKeyExists(dir, key) {
|
|
|
33
33
|
return false;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
//
|
|
36
|
+
// Claude Code >= 2.1 returns `{ answers: { Question: choice } }` in tool_response.
|
|
37
|
+
// Older releases expose a text tool_output: `... "Question"="chosen labels" "Q2"="..."`.
|
|
37
38
|
export function parseAnswers(output) {
|
|
38
39
|
const map = {};
|
|
40
|
+
if (output && typeof output === 'object') {
|
|
41
|
+
const answers = output.answers && typeof output.answers === 'object'
|
|
42
|
+
? output.answers
|
|
43
|
+
: output;
|
|
44
|
+
for (const [question, answer] of Object.entries(answers)) {
|
|
45
|
+
if (typeof answer === 'string') map[clean(question)] = clean(answer);
|
|
46
|
+
else if (Array.isArray(answer)) map[clean(question)] = answer.map(clean).filter(Boolean).join(',');
|
|
47
|
+
}
|
|
48
|
+
if (Object.keys(map).length) return map;
|
|
49
|
+
}
|
|
39
50
|
const re = /"([^"]+)"\s*=\s*"([^"]*)"/g;
|
|
40
51
|
let m;
|
|
41
52
|
while ((m = re.exec(String(output || '')))) map[m[1].trim()] = m[2].trim();
|
|
@@ -172,7 +183,35 @@ export function captureDecision(vaultBase, input) {
|
|
|
172
183
|
writeFileSync(filePath, buildDecisionCaptureNote({
|
|
173
184
|
questions, answers, dateStr, startedAt: formatLocalIso(now), sessionRel, provider, localeId: loc.id, adrNum, contentKey,
|
|
174
185
|
}), 'utf-8');
|
|
175
|
-
|
|
186
|
+
const rel = toVaultRelative(vaultBase, filePath);
|
|
187
|
+
if (sessionRel) {
|
|
188
|
+
try {
|
|
189
|
+
const sessionPath = join(vaultBase, sessionRel);
|
|
190
|
+
let session = readFileSync(sessionPath, 'utf8');
|
|
191
|
+
const wikilink = wikilinkFromRel(rel);
|
|
192
|
+
const link = `- ${wikilink}`;
|
|
193
|
+
if (!session.includes(wikilink)) {
|
|
194
|
+
const heading = '\n## Decisões geradas nesta sessão\n';
|
|
195
|
+
const at = session.indexOf(heading);
|
|
196
|
+
if (at !== -1) {
|
|
197
|
+
const bodyStart = at + heading.length;
|
|
198
|
+
const nextRel = session.slice(bodyStart).search(/\n## /);
|
|
199
|
+
const bodyEnd = nextRel === -1 ? session.length : bodyStart + nextRel;
|
|
200
|
+
const body = session.slice(bodyStart, bodyEnd)
|
|
201
|
+
.replace(/\n?Nenhuma decisão registrada ainda\.\s*/i, '\n')
|
|
202
|
+
.trim();
|
|
203
|
+
const merged = [body, link].filter(Boolean).join('\n');
|
|
204
|
+
session = `${session.slice(0, bodyStart)}\n${merged}\n\n${session.slice(bodyEnd).replace(/^\n+/, '')}`;
|
|
205
|
+
} else {
|
|
206
|
+
const anchor = session.indexOf('\n## Encerramento');
|
|
207
|
+
const section = `\n## Decisões geradas nesta sessão\n\n${link}\n`;
|
|
208
|
+
session = anchor === -1 ? `${session.trimEnd()}${section}` : `${session.slice(0, anchor).trimEnd()}${section}${session.slice(anchor)}`;
|
|
209
|
+
}
|
|
210
|
+
writeFileSync(sessionPath, session, 'utf8');
|
|
211
|
+
}
|
|
212
|
+
} catch { /* backlink auxiliar nunca derruba a captura */ }
|
|
213
|
+
}
|
|
214
|
+
return { rel, skipped: false };
|
|
176
215
|
}
|
|
177
216
|
|
|
178
217
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
package/hooks/harness-doctor.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { activeChange, parseTasks } from './change-core.mjs';
|
|
6
|
-
import { parseRequirements, parseSpecsList, parseDelta, evaluateVerdict } from './spec-core.mjs';
|
|
6
|
+
import { parseRequirements, parseSpecsList, parseDelta, evaluateVerdict, validateSpecImpact } from './spec-core.mjs';
|
|
7
7
|
import { getLocale } from './locale.mjs';
|
|
8
8
|
|
|
9
9
|
export function checkHarness(vaultBase, projectRoot) {
|
|
@@ -43,6 +43,9 @@ export function checkHarness(vaultBase, projectRoot) {
|
|
|
43
43
|
let entries;
|
|
44
44
|
try { entries = readdirSync(dir); } catch { continue; } // a file, not a change dir
|
|
45
45
|
if (!entries.includes('proposta.md')) { errors.push(`change sem proposta.md: ${name}`); continue; }
|
|
46
|
+
const impact = validateSpecImpact(dir);
|
|
47
|
+
errors.push(...impact.errors.map((e) => `${name}: ${e}`));
|
|
48
|
+
warnings.push(...impact.warnings.map((w) => `${name}: ${w}`));
|
|
46
49
|
if (name === active) {
|
|
47
50
|
try {
|
|
48
51
|
for (const cap of parseSpecsList(readFileSync(join(dir, 'proposta.md'), 'utf8'))) {
|
package/hooks/plan-capture.mjs
CHANGED
|
@@ -3,10 +3,20 @@
|
|
|
3
3
|
// Code e o vault: quando o usuário APROVA um plano, o plano vira registro — anexado à change
|
|
4
4
|
// ativa, ou uma change nova criada e preenchida a partir dele (proposta do Contexto, design do
|
|
5
5
|
// corpo, tarefas dos checkboxes). Não depende de a LLM lembrar do processo. Rejeição = no-op.
|
|
6
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
7
8
|
import { join } from 'node:path';
|
|
8
9
|
import { pathToFileURL } from 'node:url';
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
findActiveSessionByTranscript,
|
|
12
|
+
formatDate,
|
|
13
|
+
getVaultBase,
|
|
14
|
+
readControl,
|
|
15
|
+
readHookInput,
|
|
16
|
+
slugify,
|
|
17
|
+
wikilinkFromRel,
|
|
18
|
+
writeHookOutput,
|
|
19
|
+
} from './obsidian-common.mjs';
|
|
10
20
|
import { activeChange, newChange } from './change-core.mjs';
|
|
11
21
|
import { getLocale } from './locale.mjs';
|
|
12
22
|
|
|
@@ -17,8 +27,18 @@ export function extractPlan(input) {
|
|
|
17
27
|
const tr = input?.tool_response ?? input?.toolResponse ?? '';
|
|
18
28
|
const resp = typeof tr === 'string' ? tr : JSON.stringify(tr || '');
|
|
19
29
|
if (/doesn'?t want to proceed|user rejected|rejected the plan/i.test(resp)) return null;
|
|
20
|
-
if (!/approved (your |the )?plan|Approved Plan/i.test(resp)) return null;
|
|
21
30
|
const direct = input?.tool_input?.plan ?? input?.toolInput?.plan;
|
|
31
|
+
// Claude Code atual entrega o plano aprovado como objeto estruturado no PostToolUse.
|
|
32
|
+
// Esse evento só ocorre após sucesso; flags negativas explícitas continuam no-op.
|
|
33
|
+
if (tr && typeof tr === 'object') {
|
|
34
|
+
if (tr.approved === false || tr.rejected === true || tr.cancelled === true) return null;
|
|
35
|
+
const structured = tr.plan ?? direct;
|
|
36
|
+
if (structured && String(structured).trim()) return String(structured);
|
|
37
|
+
const structuredPath = tr.filePath ?? tr.planFilePath;
|
|
38
|
+
if (structuredPath) { try { return readFileSync(String(structuredPath), 'utf8'); } catch { /* inacessível */ } }
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if (!/approved (your |the )?plan|Approved Plan/i.test(resp)) return null;
|
|
22
42
|
if (direct && String(direct).trim()) return String(direct);
|
|
23
43
|
const marker = resp.match(/## Approved Plan[^\n]*:\s*\n([\s\S]+)$/);
|
|
24
44
|
if (marker && marker[1].trim()) return marker[1].trim();
|
|
@@ -27,6 +47,27 @@ export function extractPlan(input) {
|
|
|
27
47
|
return null;
|
|
28
48
|
}
|
|
29
49
|
|
|
50
|
+
function planIndex(slug) {
|
|
51
|
+
return `---\ntype: plan-index\ntags:\n - plano\n---\n\n# ${slug} — planos aprovados\n`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function persistApprovedPlan(dir, slug, rawNote, plan, dateStr) {
|
|
55
|
+
const hash = createHash('sha256').update(String(plan)).digest('hex').slice(0, 12);
|
|
56
|
+
const snapshotsDir = join(dir, 'planos');
|
|
57
|
+
mkdirSync(snapshotsDir, { recursive: true });
|
|
58
|
+
const snapshot = join(snapshotsDir, `${hash}.md`);
|
|
59
|
+
if (!existsSync(snapshot)) writeFileSync(snapshot, rawNote, 'utf8');
|
|
60
|
+
|
|
61
|
+
const indexPath = join(dir, 'plano-aprovado.md');
|
|
62
|
+
let index = existsSync(indexPath) ? readFileSync(indexPath, 'utf8') : planIndex(slug);
|
|
63
|
+
const entry = `- [[planos/${hash}|${dateStr} — ${hash}]]`;
|
|
64
|
+
if (!index.includes(`[[planos/${hash}|`)) {
|
|
65
|
+
index = `${index.trimEnd()}\n\n${entry}\n`;
|
|
66
|
+
writeFileSync(indexPath, index, 'utf8');
|
|
67
|
+
}
|
|
68
|
+
return { hash, snapshot };
|
|
69
|
+
}
|
|
70
|
+
|
|
30
71
|
export function planSlug(plan) {
|
|
31
72
|
const h1 = String(plan || '').match(/^#\s+(.+)$/m);
|
|
32
73
|
return h1 ? slugify(h1[1], 'plano-aprovado', 60) : 'plano-aprovado';
|
|
@@ -53,11 +94,15 @@ export function capturePlan(vaultBase, input) {
|
|
|
53
94
|
const en = loc.id === 'en';
|
|
54
95
|
const dateStr = formatDate(new Date());
|
|
55
96
|
const rawNote = `---\ntype: plan\ndate: ${dateStr}\ntags:\n - plano\n---\n\n${plan.trim()}\n`;
|
|
97
|
+
const transcriptPath = input?.transcript_path ?? input?.transcriptPath ?? '';
|
|
98
|
+
const sessionRel = findActiveSessionByTranscript(vaultBase, transcriptPath)?.session_file
|
|
99
|
+
|| readControl(vaultBase).session_file
|
|
100
|
+
|| '';
|
|
56
101
|
|
|
57
102
|
const active = activeChange(vaultBase);
|
|
58
103
|
if (active) {
|
|
59
104
|
const dir = join(vaultBase, loc.folders.changes, active);
|
|
60
|
-
|
|
105
|
+
persistApprovedPlan(dir, active, rawNote, plan, dateStr);
|
|
61
106
|
return {
|
|
62
107
|
slug: active, created: false,
|
|
63
108
|
context: `<plan_captured>\n${en
|
|
@@ -67,9 +112,10 @@ export function capturePlan(vaultBase, input) {
|
|
|
67
112
|
}
|
|
68
113
|
|
|
69
114
|
const slug = planSlug(plan);
|
|
70
|
-
newChange(vaultBase, slug, { dateStr });
|
|
115
|
+
newChange(vaultBase, slug, { dateStr, sessionRel });
|
|
71
116
|
const dir = join(vaultBase, loc.folders.changes, slug);
|
|
72
117
|
const ctx = sectionBody(plan, ['Contexto', 'Context']) || plan.trim().split('\n').slice(0, 6).join('\n');
|
|
118
|
+
const source = sessionRel ? `\n - "${wikilinkFromRel(sessionRel)}"` : ' []';
|
|
73
119
|
writeFileSync(join(dir, 'proposta.md'), `---
|
|
74
120
|
type: change
|
|
75
121
|
status: active
|
|
@@ -78,7 +124,9 @@ cssclasses:
|
|
|
78
124
|
- topic-change
|
|
79
125
|
tags:
|
|
80
126
|
- mudanca
|
|
81
|
-
source
|
|
127
|
+
source:${source}
|
|
128
|
+
spec_impact: pending
|
|
129
|
+
spec_impact_reason: ""
|
|
82
130
|
specs: []
|
|
83
131
|
---
|
|
84
132
|
|
|
@@ -95,7 +143,7 @@ ${en ? 'See design.md and plano-aprovado.md (captured from the approved plan-mod
|
|
|
95
143
|
writeFileSync(join(dir, 'design.md'), `# ${slug} — design\n\n${plan.trim()}\n`, 'utf8');
|
|
96
144
|
const tasks = planTasks(plan);
|
|
97
145
|
if (tasks.length) writeFileSync(join(dir, 'tarefas.md'), `# ${slug} — ${en ? 'tasks' : 'tarefas'}\n\n${tasks.join('\n')}\n`, 'utf8');
|
|
98
|
-
|
|
146
|
+
persistApprovedPlan(dir, slug, rawNote, plan, dateStr);
|
|
99
147
|
return {
|
|
100
148
|
slug, created: true,
|
|
101
149
|
context: `<plan_captured>\n${en
|
|
@@ -110,7 +158,11 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
|
|
110
158
|
const r = capturePlan(getVaultBase(input), input);
|
|
111
159
|
if (!r) { writeHookOutput({}); }
|
|
112
160
|
else writeHookOutput({ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: r.context } });
|
|
113
|
-
} catch {
|
|
114
|
-
|
|
161
|
+
} catch (error) {
|
|
162
|
+
process.stderr.write(`[wendkeep] plan-capture falhou: ${error.message}\n`);
|
|
163
|
+
writeHookOutput({ hookSpecificOutput: {
|
|
164
|
+
hookEventName: 'PostToolUse',
|
|
165
|
+
additionalContext: `<plan_capture_error>O plano aprovado não foi persistido: ${error.message}</plan_capture_error>`,
|
|
166
|
+
} });
|
|
115
167
|
}
|
|
116
168
|
}
|
package/hooks/spec-core.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// hooks/spec-core.mjs — living spec (07-Specs) + change delta merge (OpenSpec native).
|
|
2
2
|
// Pure parsing/merge + promoteSpecs (fs). No import from change-core (avoids a cycle).
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
-
import { readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { ensureDir } from './obsidian-common.mjs';
|
|
7
7
|
import { getLocale } from './locale.mjs';
|
|
@@ -87,6 +87,49 @@ export function parseSpecsList(propostaMd) {
|
|
|
87
87
|
return [];
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
function yamlScalar(text, key) {
|
|
91
|
+
const m = String(text).match(new RegExp(`^${key}:\\s*(.*)$`, 'm'));
|
|
92
|
+
return m ? m[1].trim().replace(/^['"]|['"]$/g, '') : '';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function parseSpecImpact(propostaMd) {
|
|
96
|
+
const text = String(propostaMd || '');
|
|
97
|
+
const status = yamlScalar(text, 'spec_impact') || yamlScalar(text, 'spec-impact');
|
|
98
|
+
const reason = yamlScalar(text, 'spec_impact_reason') || yamlScalar(text, 'spec-impact-reason');
|
|
99
|
+
return { status, reason };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function validateSpecImpact(changeDir) {
|
|
103
|
+
let proposta = '';
|
|
104
|
+
try { proposta = readFileSync(join(changeDir, 'proposta.md'), 'utf8'); }
|
|
105
|
+
catch { return { ok: false, errors: ['proposta.md ausente'], warnings: [], status: '', listed: [], onDisk: [] }; }
|
|
106
|
+
|
|
107
|
+
const { status, reason } = parseSpecImpact(proposta);
|
|
108
|
+
const listed = parseSpecsList(proposta);
|
|
109
|
+
const onDisk = discoverSpecDeltas(changeDir);
|
|
110
|
+
const enforced = existsSync(join(changeDir, '.spec-impact-v1'));
|
|
111
|
+
const errors = [];
|
|
112
|
+
const warnings = [];
|
|
113
|
+
|
|
114
|
+
if (!status) {
|
|
115
|
+
if (enforced) errors.push('spec_impact ausente — classifique como required ou none');
|
|
116
|
+
else warnings.push('change legada sem spec_impact — migre para required ou none antes do próximo ciclo');
|
|
117
|
+
return { ok: errors.length === 0, errors, warnings, status: '', listed, onDisk, legacy: !enforced };
|
|
118
|
+
}
|
|
119
|
+
if (!['pending', 'required', 'none'].includes(status)) errors.push(`spec_impact inválido: ${status}`);
|
|
120
|
+
if (status === 'pending') errors.push('spec_impact pending — classifique o impacto antes de arquivar');
|
|
121
|
+
if (status === 'none') {
|
|
122
|
+
if (!reason) errors.push('spec_impact none exige justificativa em spec_impact_reason');
|
|
123
|
+
if (listed.length || onDisk.length) errors.push('spec_impact none contradiz specs/deltas declarados');
|
|
124
|
+
}
|
|
125
|
+
if (status === 'required') {
|
|
126
|
+
if (!listed.length) errors.push('spec_impact required exige ao menos uma capability em specs');
|
|
127
|
+
for (const cap of listed) if (!onDisk.includes(cap)) errors.push(`delta real ausente para capability ${cap}`);
|
|
128
|
+
for (const cap of onDisk) if (!listed.includes(cap)) errors.push(`delta ${cap} existe no disco mas não está listado em specs`);
|
|
129
|
+
}
|
|
130
|
+
return { ok: errors.length === 0, errors, warnings, status, reason, listed, onDisk, legacy: false };
|
|
131
|
+
}
|
|
132
|
+
|
|
90
133
|
// Um delta ainda no estado do scaffold (só o requisito "(nome)"/"(name)", nada removido) não é
|
|
91
134
|
// contrato — a promoção o filtra. Um delta com REMOVED é sempre intenção real.
|
|
92
135
|
export function isPlaceholderDelta(md) {
|
|
@@ -118,9 +161,11 @@ export function promoteSpecs(vaultBase, changeDir, specs, { changeWikilink, date
|
|
|
118
161
|
const promoted = [];
|
|
119
162
|
const warnings = [];
|
|
120
163
|
for (const cap of specs) {
|
|
121
|
-
let
|
|
122
|
-
try {
|
|
123
|
-
catch {
|
|
164
|
+
let deltaMd;
|
|
165
|
+
try { deltaMd = readFileSync(join(changeDir, 'specs', cap, 'spec.md'), 'utf8'); }
|
|
166
|
+
catch { throw new Error(`delta ausente para ${cap}`); }
|
|
167
|
+
if (isPlaceholderDelta(deltaMd)) throw new Error(`delta placeholder para ${cap}`);
|
|
168
|
+
const delta = parseDelta(deltaMd);
|
|
124
169
|
const livePath = join(vaultBase, specsDir, `${cap}.md`);
|
|
125
170
|
let current = [];
|
|
126
171
|
try { current = parseRequirements(readFileSync(livePath, 'utf8')); } catch { /* nova capability */ }
|
package/hooks/vault-health.mjs
CHANGED
|
@@ -71,14 +71,14 @@ function hasDefaultPending(content) {
|
|
|
71
71
|
.some((line) => DEFAULT_PENDING_PATTERNS.some((pattern) => pattern.test(line.trim())));
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
function usageSectionIsPlaced(content) {
|
|
74
|
+
function usageSectionIsPlaced(content, { active = false } = {}) {
|
|
75
75
|
const usage = content.indexOf('\n## Uso de tokens e custos');
|
|
76
76
|
if (usage === -1) return true;
|
|
77
77
|
const changed = content.indexOf('\n## Arquivos criados ou alterados');
|
|
78
78
|
const pending = content.indexOf('\n## Pendências');
|
|
79
79
|
const closing = content.indexOf('\n## Encerramento');
|
|
80
|
-
if (pending === -1 || closing === -1) return false;
|
|
81
|
-
return usage < pending && usage < closing && (changed === -1 || usage > changed);
|
|
80
|
+
if (pending === -1 || (!active && closing === -1)) return false;
|
|
81
|
+
return usage < pending && (active || usage < closing) && (changed === -1 || usage > changed);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
function linkedNotesFromSession(content) {
|
|
@@ -102,14 +102,15 @@ function checkSession({ vaultBase, sessionRel, control, registry }) {
|
|
|
102
102
|
return { failures, warnings, metrics };
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
const content = readFileSync(sessionPath, 'utf-8');
|
|
105
|
+
const content = readFileSync(sessionPath, 'utf-8');
|
|
106
|
+
const activeSession = control.status === 'active' && control.session_file === sessionRel;
|
|
106
107
|
const duplicates = findDuplicateTurnMarkers(content);
|
|
107
108
|
metrics.turnMarkers = (content.match(/<!-- (?:wk-turn|codex-turn):/g) || []).length;
|
|
108
109
|
metrics.duplicateTurnMarkers = duplicates.length;
|
|
109
110
|
|
|
110
111
|
if (duplicates.length) failures.push(`Marcadores de turno duplicados: ${duplicates.join(', ')}`);
|
|
111
112
|
if (hasHeadingAfterClosing(content)) failures.push('Há headings/iterações após ## Encerramento.');
|
|
112
|
-
if (!usageSectionIsPlaced(content)) failures.push('## Uso de tokens e custos está fora da posição esperada.');
|
|
113
|
+
if (!usageSectionIsPlaced(content, { active: activeSession })) failures.push('## Uso de tokens e custos está fora da posição esperada.');
|
|
113
114
|
if (hasDefaultPending(content)) warnings.push('Pendências ainda contém placeholders padrão.');
|
|
114
115
|
|
|
115
116
|
const registryEntry = registry.sessions?.[control.session_id];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "A persistent-memory harness for AI coding agents on your Obsidian vault: turn-by-turn session capture plus a native, zero-dependency spec→change→verify→archive loop (sensor-gated, independent verdict, mutation discrimination). Local-first, agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/change.mjs
CHANGED
|
@@ -4,7 +4,9 @@ import { isAbsolute, join, resolve } from 'node:path';
|
|
|
4
4
|
import {
|
|
5
5
|
newChange,
|
|
6
6
|
activeChange,
|
|
7
|
+
allChangesState,
|
|
7
8
|
listChanges,
|
|
9
|
+
renderOpenChanges,
|
|
8
10
|
parseTasks,
|
|
9
11
|
setTaskDone,
|
|
10
12
|
archiveChange,
|
|
@@ -12,7 +14,7 @@ import {
|
|
|
12
14
|
scaffoldPlaceholders,
|
|
13
15
|
} from '../hooks/change-core.mjs';
|
|
14
16
|
import { evaluateGate, requiredSensors } from '../hooks/sensors-core.mjs';
|
|
15
|
-
import { evaluateVerdict, tasksHashOf, parseSpecsList, parseDelta, parseRequirements, applyDelta } from '../hooks/spec-core.mjs';
|
|
17
|
+
import { evaluateVerdict, tasksHashOf, parseSpecsList, parseDelta, parseRequirements, applyDelta, validateSpecImpact } from '../hooks/spec-core.mjs';
|
|
16
18
|
import { getNextAdrNumber, readControl } from '../hooks/obsidian-common.mjs';
|
|
17
19
|
import { getLocale } from '../hooks/locale.mjs';
|
|
18
20
|
|
|
@@ -61,9 +63,9 @@ export function runChange(argv) {
|
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
if (sub === 'list') {
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
process.stdout.write(
|
|
66
|
+
const state = allChangesState(vaultBase);
|
|
67
|
+
const { archived } = listChanges(vaultBase);
|
|
68
|
+
process.stdout.write(`${renderOpenChanges(state, { tag: '' }) || 'open changes: (none)'}\n`);
|
|
67
69
|
process.stdout.write(`archived: ${archived.join(', ') || '(none)'}\n`);
|
|
68
70
|
process.exit(0);
|
|
69
71
|
}
|
|
@@ -82,8 +84,16 @@ export function runChange(argv) {
|
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
if (sub === 'status') {
|
|
85
|
-
const slug = slugArg()
|
|
86
|
-
if (!slug) {
|
|
87
|
+
const slug = slugArg();
|
|
88
|
+
if (!slug) {
|
|
89
|
+
const state = allChangesState(vaultBase);
|
|
90
|
+
if (!state.changes.length && !state.pointerWarning) {
|
|
91
|
+
process.stderr.write('wendkeep change status: no open changes\n');
|
|
92
|
+
process.exit(2);
|
|
93
|
+
}
|
|
94
|
+
process.stdout.write(`${renderOpenChanges(state, { tag: '' })}\n`);
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
87
97
|
const dir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|
|
88
98
|
let tarefasMd;
|
|
89
99
|
try { tarefasMd = readFileSync(join(dir, 'tarefas.md'), 'utf8'); }
|
|
@@ -163,6 +173,9 @@ export function runChange(argv) {
|
|
|
163
173
|
if (placeholders.length) {
|
|
164
174
|
return { ok: false, failing: [`scaffold não preenchido (${placeholders.join('; ')}) — preencha proposta/design/tarefas antes de arquivar, ou \`wendkeep change abandon ${slug}\` se a change não vai adiante (--force não pula este check)`] };
|
|
165
175
|
}
|
|
176
|
+
const impact = validateSpecImpact(dir);
|
|
177
|
+
for (const warning of impact.warnings) process.stderr.write(`aviso spec: ${warning}\n`);
|
|
178
|
+
if (!impact.ok) return { ok: false, failing: impact.errors };
|
|
166
179
|
let tarefasMd = '';
|
|
167
180
|
try { tarefasMd = readFileSync(join(dir, 'tarefas.md'), 'utf8'); } catch { /* no tasks */ }
|
|
168
181
|
const tasks = parseTasks(tarefasMd);
|
package/src/init.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
mcpServerEntry,
|
|
16
16
|
hookCommand,
|
|
17
17
|
hookCommandLocal,
|
|
18
|
+
hookCommandLocalLegacy,
|
|
18
19
|
deriveVaultDirName,
|
|
19
20
|
selectableCompanions,
|
|
20
21
|
resolveCompanions,
|
|
@@ -105,6 +106,16 @@ export function hookCommandFor(name, projectPath) {
|
|
|
105
106
|
return hookCommand(name);
|
|
106
107
|
}
|
|
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
|
+
|
|
108
119
|
export function mergeSettings(existing, { vaultPath, withMcp, force, companions = [], skipMcp = [], dotcontextHookLevel = 'full', projectPath = '' }) {
|
|
109
120
|
const s = existing && typeof existing === 'object' ? { ...existing } : {};
|
|
110
121
|
s.hooks = { ...(s.hooks || {}) };
|
|
@@ -120,19 +131,26 @@ export function mergeSettings(existing, { vaultPath, withMcp, force, companions
|
|
|
120
131
|
...companionHookSpecs(companions, { dotcontextHookLevel }),
|
|
121
132
|
].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
122
133
|
for (const h of allSpecs) {
|
|
123
|
-
const
|
|
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;
|
|
124
137
|
// Dual-recognition: um hook nomeado é reconhecido tanto na forma npx quanto na node-direta,
|
|
125
138
|
// 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)];
|
|
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));
|
|
127
142
|
const groups = Array.isArray(s.hooks[h.event]) ? [...s.hooks[h.event]] : [];
|
|
128
|
-
const owning = groups.find((g) => (g.hooks || []).some(
|
|
143
|
+
const owning = groups.find((g) => (g.hooks || []).some(ownsHook));
|
|
129
144
|
if (owning) {
|
|
130
145
|
// Already wired: never add a duplicate group (the old `if (present && !force)` fell through
|
|
131
146
|
// under --force and appended a second identical group). Under --force, refresh the managed
|
|
132
147
|
// entry's fields in place — without disturbing any sibling hooks the user grouped with it.
|
|
133
|
-
|
|
134
|
-
|
|
148
|
+
const hk = owning.hooks.find(ownsHook);
|
|
149
|
+
const brokenRelative = hk?.command === hookCommandLocalLegacy(h.name);
|
|
150
|
+
if (force || brokenRelative) {
|
|
135
151
|
hk.command = command;
|
|
152
|
+
if (args) hk.args = args;
|
|
153
|
+
else delete hk.args;
|
|
136
154
|
hk.timeout = h.timeout;
|
|
137
155
|
if (h.statusMessage) hk.statusMessage = h.statusMessage;
|
|
138
156
|
if (h.matcher && (owning.hooks || []).length === 1) owning.matcher = h.matcher;
|
|
@@ -141,6 +159,7 @@ export function mergeSettings(existing, { vaultPath, withMcp, force, companions
|
|
|
141
159
|
continue;
|
|
142
160
|
}
|
|
143
161
|
const entry = { type: 'command', command, timeout: h.timeout, statusMessage: h.statusMessage };
|
|
162
|
+
if (args) entry.args = args;
|
|
144
163
|
const group = h.matcher ? { matcher: h.matcher, hooks: [entry] } : { hooks: [entry] };
|
|
145
164
|
groups.push(group);
|
|
146
165
|
s.hooks[h.event] = groups;
|
|
@@ -530,6 +549,8 @@ export async function runInit(argv) {
|
|
|
530
549
|
log(M.mcpSkipped);
|
|
531
550
|
}
|
|
532
551
|
|
|
552
|
+
log(' [!] ignore runtime do wendkeep no Git quando o vault for versionado: .brain/.change-*');
|
|
553
|
+
|
|
533
554
|
// 4. Vault color system (.obsidian) -----------------------------------------
|
|
534
555
|
if (args.noColors) {
|
|
535
556
|
log(M.colorsSkipped);
|
package/src/skills-seed.mjs
CHANGED
|
@@ -29,8 +29,14 @@ vault cego. Exceção única: mudança trivial (typo, 1 linha).
|
|
|
29
29
|
- \`proposta.md\` — *por quê* e *o que muda* (o WHAT).
|
|
30
30
|
- \`design.md\` — a abordagem técnica.
|
|
31
31
|
- \`tarefas.md\` — a lista de tarefas \`- [ ] N.N descrição\`.
|
|
32
|
-
A mudança vira a *
|
|
33
|
-
|
|
32
|
+
A mudança vira a *atual* (ponteiro global \`.brain/CURRENT_CHANGE.md\`). Podem existir
|
|
33
|
+
várias changes abertas: hooks e \`change list/status\` mostram todas as pendências; comandos
|
|
34
|
+
sem \`--change\` usam somente a atual.
|
|
35
|
+
Antes de implementar, resolva \`spec_impact\` na proposta:
|
|
36
|
+
- \`required\`: liste a capability em \`specs:\` e preencha
|
|
37
|
+
\`specs/<capability>/spec.md\` com ADDED/MODIFIED/REMOVED; ligue tarefas com \`[req:ID]\`.
|
|
38
|
+
- \`none\`: registre uma justificativa real em \`spec_impact_reason\`.
|
|
39
|
+
\`pending\` nunca é estado pronto para implementação ou archive.
|
|
34
40
|
3. **Apply** — implemente cada tarefa de \`tarefas.md\` com disciplina **wk-tdd**
|
|
35
41
|
(teste vermelho antes do código). Marque \`- [x]\` ao concluir. Declare nas tarefas:
|
|
36
42
|
- \`[sensor:<id>]\` — a prova automatizada (roda no verify).
|
|
@@ -48,7 +54,9 @@ vault cego. Exceção única: mudança trivial (typo, 1 linha).
|
|
|
48
54
|
|
|
49
55
|
## Regras
|
|
50
56
|
|
|
51
|
-
-
|
|
57
|
+
- Várias changes podem ficar abertas. \`CURRENT_CHANGE.md\` marca uma atual, sem esconder as
|
|
58
|
+
outras pendências. Claude, Codex ou outro agente podem assumir uma change existente com
|
|
59
|
+
\`wendkeep change new <slug-existente>\` ou \`--change <slug>\` quando disponível.
|
|
52
60
|
- Se uma tarefa não precisa de prova automatizada, não declare sensor — o gate só exige
|
|
53
61
|
o que você declarou. Sem \`[sensor:]\`, o archive não trava.
|
|
54
62
|
- A proposta linka a sessão de origem; a sessão linka a mudança ativa. É de propósito:
|
|
@@ -139,6 +147,7 @@ qualquer código.
|
|
|
139
147
|
Antes de fechar o design, resolva cada zona cinza: decide com o usuário, ou registra como
|
|
140
148
|
**assumption assinada** ("assumo X porque Y — corrija se errado"). Cinza declinado pelo
|
|
141
149
|
usuário é *registrado*, não descartado no silêncio. Nada sai do design silenciosamente ambíguo.
|
|
150
|
+
Declare também a capability e se o design tem \`spec_impact: required\` ou \`none\`.
|
|
142
151
|
|
|
143
152
|
## Tabela out-of-scope
|
|
144
153
|
|
|
@@ -170,6 +179,10 @@ Mapeie os arquivos: quais criar/modificar e a responsabilidade de cada um. Arqui
|
|
|
170
179
|
mudam juntos ficam juntos; um arquivo, uma responsabilidade. É aqui que a decomposição
|
|
171
180
|
trava.
|
|
172
181
|
|
|
182
|
+
Resolva o contrato antes de decompor: \`spec_impact: required\` exige capability + delta real em
|
|
183
|
+
\`specs/<capability>/spec.md\`; \`spec_impact: none\` exige justificativa. Cada comportamento do
|
|
184
|
+
delta recebe ID e as tarefas correspondentes usam \`[req:ID]\`.
|
|
185
|
+
|
|
173
186
|
## Tarefas bite-sized (TDD)
|
|
174
187
|
|
|
175
188
|
Cada tarefa termina num entregável testável de forma independente. Cada passo é uma ação
|
|
@@ -238,8 +251,12 @@ leaves the vault blind. Single exception: a trivial change (typo, one line).
|
|
|
238
251
|
|
|
239
252
|
1. **Explore** — understand the problem before proposing.
|
|
240
253
|
2. **Propose** — \`wendkeep change new <slug>\` scaffolds \`08-Changes/<slug>/\`
|
|
241
|
-
(proposta/design/tarefas + a \`specs/\` delta). The change becomes *
|
|
242
|
-
|
|
254
|
+
(proposta/design/tarefas + a \`specs/\` delta). The change becomes *current* through global
|
|
255
|
+
\`.brain/CURRENT_CHANGE.md\`. Multiple changes may stay open; hooks and \`change list/status\`
|
|
256
|
+
show every pending task, while commands without \`--change\` use only the current change.
|
|
257
|
+
Before implementation, resolve \`spec_impact\`: \`required\` needs the capability listed in
|
|
258
|
+
\`specs:\` plus a real \`specs/<capability>/spec.md\` delta and \`[req:ID]\` links; \`none\`
|
|
259
|
+
needs a real \`spec_impact_reason\`. \`pending\` is never ready for implementation/archive.
|
|
243
260
|
3. **Apply** — implement each task in tarefas.md with **wk-tdd** (red test first). Tag tasks:
|
|
244
261
|
\`[sensor:<id>]\` (automated proof) and \`[req:<ID>]\` (the spec requirement it satisfies).
|
|
245
262
|
4. **Verify** — \`wendkeep verify\` runs the sensors; then \`wendkeep verify --deep\` builds
|
|
@@ -250,7 +267,9 @@ leaves the vault blind. Single exception: a trivial change (typo, one line).
|
|
|
250
267
|
verdict AND no open tasks. It promotes the delta into \`07-Specs\` and mints an ADR.
|
|
251
268
|
|
|
252
269
|
## Rules
|
|
253
|
-
-
|
|
270
|
+
- Multiple changes may stay open. \`CURRENT_CHANGE.md\` marks one current change without hiding
|
|
271
|
+
other pending tasks. Any agent may take over an existing change with
|
|
272
|
+
\`wendkeep change new <existing-slug>\` or \`--change <slug>\` where available.
|
|
254
273
|
- No \`[sensor:]\` on a task = no automated gate for it. No \`[req:]\` = no independent verdict.
|
|
255
274
|
- The graph links session ↔ change ↔ requirement ↔ decision. That is the point.
|
|
256
275
|
`;
|
|
@@ -310,6 +329,7 @@ Use it when the idea is still vague. Turn it into an approved design BEFORE writ
|
|
|
310
329
|
## Closure gate — no dangling ambiguity
|
|
311
330
|
Resolve every gray area: decide with the user, or log a **signed-off assumption** ("assuming X
|
|
312
331
|
because Y — correct me if wrong"). A declined gray area is recorded, not silently dropped.
|
|
332
|
+
Also declare the capability and whether the design has \`spec_impact: required\` or \`none\`.
|
|
313
333
|
|
|
314
334
|
## Out-of-scope table
|
|
315
335
|
List explicitly what the change does **not** do. Undeclared scope becomes creep.
|
|
@@ -331,6 +351,10 @@ Use it after an approved design. Produce a plan an engineer with no project cont
|
|
|
331
351
|
Map the files: what to create/modify and each one's responsibility. Files that change together
|
|
332
352
|
live together; one file, one responsibility.
|
|
333
353
|
|
|
354
|
+
Resolve the contract first: \`spec_impact: required\` needs a capability and a real delta at
|
|
355
|
+
\`specs/<capability>/spec.md\`; \`spec_impact: none\` needs a reason. Give each behaviour an ID
|
|
356
|
+
and link the corresponding tasks with \`[req:ID]\`.
|
|
357
|
+
|
|
334
358
|
## Bite-sized tasks (TDD)
|
|
335
359
|
Each task ends in an independently testable deliverable. Each step is a 2–5 min action:
|
|
336
360
|
write the failing test (show code) → run and see it fail (exact command + expected) → minimal
|
|
@@ -434,6 +458,10 @@ comes from the package (freshness seal; edit a task later and the gate rejects i
|
|
|
434
458
|
|
|
435
459
|
const PLAN_TEMPLATE_PT = `# Template — plano de tarefas (TDD, bite-sized)
|
|
436
460
|
|
|
461
|
+
## Impacto em specs
|
|
462
|
+
- \`spec_impact\`: \`required\` | \`none\`
|
|
463
|
+
- Capability/delta: \`specs/<capability>/spec.md\` ou justificativa de \`none\`
|
|
464
|
+
|
|
437
465
|
## Arquivos
|
|
438
466
|
- Criar: \`caminho/exato.mjs\`
|
|
439
467
|
- Modificar: \`caminho/existente.mjs:120-140\`
|
|
@@ -457,6 +485,10 @@ tarefas (uma função é \`x()\` em toda parte). Cada tarefa termina num entreg
|
|
|
457
485
|
|
|
458
486
|
const PLAN_TEMPLATE_EN = `# Template — task plan (TDD, bite-sized)
|
|
459
487
|
|
|
488
|
+
## Spec impact
|
|
489
|
+
- \`spec_impact\`: \`required\` | \`none\`
|
|
490
|
+
- Capability/delta: \`specs/<capability>/spec.md\` or the \`none\` rationale
|
|
491
|
+
|
|
460
492
|
## Files
|
|
461
493
|
- Create: \`exact/path.mjs\`
|
|
462
494
|
- Modify: \`exact/existing.mjs:120-140\`
|
package/src/taxonomy.mjs
CHANGED
|
@@ -121,6 +121,10 @@ export function hookCommand(name) {
|
|
|
121
121
|
// de segundos no Windows). Usada pelos hooks de ALTA FREQUÊNCIA (por prompt / por tool-call)
|
|
122
122
|
// quando o projeto tem wendkeep instalado localmente; o init decide (hookCommandFor).
|
|
123
123
|
export function hookCommandLocal(name) {
|
|
124
|
+
return `node "${'${CLAUDE_PROJECT_DIR}'}/node_modules/wendkeep/hooks/${name}.mjs"`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function hookCommandLocalLegacy(name) {
|
|
124
128
|
return `node node_modules/wendkeep/hooks/${name}.mjs`;
|
|
125
129
|
}
|
|
126
130
|
|