wendkeep 0.32.0 → 0.34.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 +51 -0
- package/README.md +14 -11
- package/README.pt-BR.md +3 -3
- package/bin/wendkeep.mjs +5 -4
- package/hooks/brain-inject.mjs +3 -3
- package/hooks/change-context.mjs +6 -15
- package/hooks/change-core.mjs +105 -22
- package/hooks/harness-doctor.mjs +16 -32
- package/hooks/spec-core.mjs +163 -3
- package/package.json +1 -1
- package/src/change.mjs +62 -9
- package/src/doctor.mjs +6 -0
- package/src/init.mjs +10 -5
- package/src/skills-seed.mjs +35 -24
- package/src/spec.mjs +68 -2
- package/src/sync-defs.mjs +84 -14
- package/src/verify.mjs +27 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,57 @@ 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.34.0] — 2026-07-11
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Spec efetiva por change**: `wendkeep spec effective --change <slug> [--json]` combina o
|
|
12
|
+
contrato consolidado com somente o delta selecionado, incluindo origem e operação por requisito.
|
|
13
|
+
- `change use <slug>` troca o ponteiro global; `change continue <arquivada> <novo-slug>` cria
|
|
14
|
+
continuação com backlink sem reabrir arquivo nem herdar evidência/verdict.
|
|
15
|
+
- `sync-defs --check`, metadata de versão/hash e entrega idêntica de skills em `.claude/skills`
|
|
16
|
+
e `.agents/skills`.
|
|
17
|
+
- `SPECS_STATE.json`, baseline por change e `spec migrate/rebase` para detectar edição direta e
|
|
18
|
+
conflitos concorrentes no mesmo requisito.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- `08-Mudanças/<slug>/specs/` é o único local de autoria. `07-Specs` permanece como contrato
|
|
23
|
+
consolidado gerado/read-only, preservando ADRs e links históricos.
|
|
24
|
+
- `verify --deep` agora grava requisitos completos em `verificacao.json` e sela o pacote com
|
|
25
|
+
`effectiveSpecHash`; `wk-verify` não relê `07-Specs`.
|
|
26
|
+
- `doctor` valida todas as changes abertas e avisa quando skills precisam de reseed + reinício.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Requisitos `ADDED` ainda não arquivados deixam de ficar invisíveis ao verificador independente.
|
|
31
|
+
- Archives concorrentes bloqueiam somente quando outra change alterou o mesmo requisito; mudanças
|
|
32
|
+
não relacionadas na mesma capability podem prosseguir.
|
|
33
|
+
|
|
34
|
+
### Migration
|
|
35
|
+
|
|
36
|
+
- Rode `wendkeep spec migrate --vault <vault>` uma vez para adotar os contratos consolidados atuais.
|
|
37
|
+
- Rode `wendkeep sync-defs --reseed --vault <vault> --project .` e reinicie Claude Code/Codex.
|
|
38
|
+
|
|
39
|
+
## [0.33.0] — 2026-07-11
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **Visão global de changes abertas**: `SessionStart`, `UserPromptSubmit`, `wendkeep change list`
|
|
44
|
+
e `wendkeep change status` sem slug mostram todas as pendências, inclusive as iniciadas por
|
|
45
|
+
outro agente.
|
|
46
|
+
- Ações de takeover explícitas no contexto: Claude, Codex ou outro agente podem retomar uma
|
|
47
|
+
change existente sem perder o restante do backlog.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- `.brain/CURRENT_CHANGE.md` continua como ponteiro global único, agora marcado como change
|
|
52
|
+
**atual**. Comandos implícitos (`done`, `verify`, `archive`, `abandon`) continuam restritos a ela.
|
|
53
|
+
- Mudanças em qualquer `tarefas.md` invalidam o hash do hook de contexto e reinjetam a lista global
|
|
54
|
+
na sessão afetada.
|
|
55
|
+
- `change-nag` permanece local à change atual; pendências de outra frente não bloqueiam o agente
|
|
56
|
+
em foco.
|
|
57
|
+
|
|
7
58
|
## [0.32.0] — 2026-07-09
|
|
8
59
|
|
|
9
60
|
### Added
|
package/README.md
CHANGED
|
@@ -71,7 +71,7 @@ npx wendkeep init
|
|
|
71
71
|
Control with `--companions <csv>` or `--no-companions`. The Claude Code plugin layer (`extraKnownMarketplaces` + `enabledPlugins`) is wired as a bonus where the companion has one.
|
|
72
72
|
5. Install a **color system** into the vault's `.obsidian/`: a CSS snippet that accents notes by type (session/decision/bug/learning, via the `cssclasses` the hooks emit) plus graph color groups by folder. Non-destructive merge into `appearance.json`/`graph.json`; skip with `--no-colors`.
|
|
73
73
|
6. Seed the **curated memory layer**: `.brain/CORE.md` (the hand-curated hot layer, with the 3 required sections) and `.brain/COMPACTION_PROTOCOL.md` (the protocol guide). The auto layers (`DIGEST.md`, `index.jsonl`) are generated by the hooks. Validate the curated layer with `wendkeep validate-memory` (cap 25 lines, 3 sections, no secrets/PII).
|
|
74
|
-
7. Seed the **definitions + skills layer**: `.brain/agents/` + `.brain/skills/` (versioned source of truth), including the native process skills `wk-workflow` / `wk-tdd` / `wk-debugging` / `wk-brainstorming` / `wk-planning` / `wk-verify
|
|
74
|
+
7. Seed the **definitions + skills layer**: `.brain/agents/` + `.brain/skills/` (versioned source of truth), including the native process skills `wk-workflow` / `wk-tdd` / `wk-debugging` / `wk-brainstorming` / `wk-planning` / `wk-verify`. `init` delivers them to `.codex/agents/`, `.claude/skills/`, and `.agents/skills/`; `sync-defs --check` detects stale copies.
|
|
75
75
|
8. Seed the **change/spec lifecycle**: the `07-Specs/` + `08-Mudanças/` folders and a native `wendkeep.sensors.json` (a `validate-memory` sensor plus one per detected `package.json` script). Drives `wendkeep change` / `wendkeep verify` — see **Change lifecycle** below.
|
|
76
76
|
|
|
77
77
|
```bash
|
|
@@ -112,14 +112,14 @@ 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
|
-
| `wendkeep spec <sub>` |
|
|
117
|
+
| `wendkeep spec <sub>` | `list` / `show` generated contracts; `effective --change <slug>`; `migrate`; `rebase`. |
|
|
118
118
|
| `wendkeep sensors <sub>` | `list` / `add <id> "<command>"` — view/edit `wendkeep.sensors.json` (JSON Schema shipped). |
|
|
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 lesson add "t" "l"` | Record a project-local lesson (injected at the next SessionStart). |
|
|
122
|
-
| `wendkeep sync-defs` | Copy `.brain/agents\|skills` into
|
|
122
|
+
| `wendkeep sync-defs` | Copy `.brain/agents\|skills` into `.codex/agents`, `.claude/skills`, `.agents/skills`; `--check` detects drift. |
|
|
123
123
|
| `wendkeep validate-memory [path]` | Validate `.brain/CORE.md` (cap 25, 3 sections, no secrets/PII). |
|
|
124
124
|
| `wendkeep doctor [--vault P]` | Run a vault health check (integrity of sessions, registry, links). |
|
|
125
125
|
| `wendkeep --version` / `--help` | Version / usage. |
|
|
@@ -148,14 +148,14 @@ 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; `change use <slug>` changes focus and `change continue <archived> <new>` creates an auditable continuation.
|
|
152
152
|
- **Apply** — implement each `tarefas.md` task. Tag a task that needs machine proof with `[sensor:<id>]`.
|
|
153
|
-
- **Verify** — `wendkeep verify`
|
|
153
|
+
- **Verify** — `wendkeep verify` records sensor evidence. `verify --deep` builds a self-contained package with complete effective requirements (living contract + this change's delta), so the independent verifier never needs to reconstruct unarchived requirements from `07-Specs`.
|
|
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/`.
|
|
155
155
|
|
|
156
156
|
> The gate blocks unless the scaffold is filled, no task is open, evidence is fresh, and every declared requirement is covered. **`--force` is the human's call — never the agent's.**
|
|
157
157
|
|
|
158
|
-
`wendkeep init`
|
|
158
|
+
`wendkeep init` seeds process skills into `.brain/skills` and delivers identical copies to Claude Code and Codex. Every skill carries source hash/version metadata; `doctor` warns when reseed + agent restart is required.
|
|
159
159
|
|
|
160
160
|
### The loop in five minutes
|
|
161
161
|
|
|
@@ -170,11 +170,14 @@ Edit `tarefas.md` — tag proof and requirement per task:
|
|
|
170
170
|
- [ ] 1.1 toggle persists across sessions [req:UI-1] [sensor:tests]
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
-
Declare the capability in `proposta.md` (`specs: [ui]`) and author its delta in
|
|
174
|
-
`specs/ui/spec.md
|
|
173
|
+
Declare the capability in `proposta.md` (`specs: [ui]`) and author its delta only in
|
|
174
|
+
`08-Mudanças/<slug>/specs/ui/spec.md`. `07-Specs` is generated/read-only. Then:
|
|
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
|
|
180
|
+
npx wendkeep spec effective --change dark-mode # living contract + this change's delta
|
|
178
181
|
npx wendkeep change done 1.1 # tick a task from the CLI
|
|
179
182
|
npx wendkeep verify # run the declared sensors -> evidencia.json
|
|
180
183
|
npx wendkeep verify --deep # assemble the verification package
|
|
@@ -183,7 +186,7 @@ npx wendkeep change diff # preview what will land in 07-Sp
|
|
|
183
186
|
npx wendkeep change archive dark-mode # gate: sensors + verdict + no open tasks
|
|
184
187
|
```
|
|
185
188
|
|
|
186
|
-
The archive promotes the delta into
|
|
189
|
+
The archive promotes the delta into generated `07-Specs/ui.md`, mints an ADR, and the
|
|
187
190
|
Obsidian graph now links *session ↔ change ↔ requirement ↔ decision*. A change that names
|
|
188
191
|
no `[req:]` skips the independent verdict — the sensor gate is its proof.
|
|
189
192
|
|
|
@@ -194,7 +197,7 @@ agent session ──hooks──▶ wendkeep ──▶ Markdown in vault ──
|
|
|
194
197
|
(Claude/Codex) (Node) (02-Sessões/…) (CORE+DIGEST, backlinks)
|
|
195
198
|
```
|
|
196
199
|
|
|
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
|
|
200
|
+
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
201
|
|
|
199
202
|
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
203
|
|
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/bin/wendkeep.mjs
CHANGED
|
@@ -43,10 +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] |
|
|
47
|
-
done <id> | undone <id> | diff | archive [--force] | abandon
|
|
46
|
+
wendkeep change <sub> Change lifecycle: new [--simple] | use | continue | list | show |
|
|
47
|
+
status | done <id> | undone <id> | diff | archive [--force] | abandon.
|
|
48
48
|
archive exige verdict (rode verify --deep); abandon descarta sem ADR.
|
|
49
|
-
wendkeep spec <sub>
|
|
49
|
+
wendkeep spec <sub> Specs: list | show | effective [--change] [--json] | migrate | rebase.
|
|
50
50
|
wendkeep sensors <sub> list | add <id> "<command>" [--severity --type --report].
|
|
51
51
|
wendkeep cost [opts] Aggregate AI-coding spend across the vault's sessions.
|
|
52
52
|
--since <date> · --top [N] (priciest) · --trend [day|week|month]
|
|
@@ -70,9 +70,10 @@ Usage:
|
|
|
70
70
|
--vault <path> or OBSIDIAN_VAULT_PATH if no path given.
|
|
71
71
|
wendkeep sync-defs [opts] Copy versioned defs from the vault's .brain into the
|
|
72
72
|
project: .brain/agents/*.toml -> .codex/agents,
|
|
73
|
-
.brain/skills/<name> -> .claude/skills. --vault P --project P.
|
|
73
|
+
.brain/skills/<name> -> .claude/skills + .agents/skills. --vault P --project P.
|
|
74
74
|
--reseed re-semeia as skills wk-* com os seeds da versão instalada
|
|
75
75
|
(sobrescreve edições manuais nas wk-*) antes de copiar.
|
|
76
|
+
--check detecta drift sem modificar arquivos.
|
|
76
77
|
wendkeep --version Print version.
|
|
77
78
|
wendkeep --help Show this help.
|
|
78
79
|
`;
|
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';
|
|
@@ -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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { dirname, join } from 'node:path';
|
|
6
6
|
import { ensureDir, wikilinkFromRel, monthFolderRelFromDateStr } from './obsidian-common.mjs';
|
|
7
|
-
import { parseSpecsList, promoteSpecs, discoverSpecDeltas, tasksHashOf } from './spec-core.mjs';
|
|
7
|
+
import { parseSpecsList, promoteSpecs, discoverSpecDeltas, tasksHashOf, captureSpecBaseline } from './spec-core.mjs';
|
|
8
8
|
import { getLocale } from './locale.mjs';
|
|
9
9
|
|
|
10
10
|
export const ARCHIVE_DIR = '_arquivo';
|
|
@@ -119,10 +119,44 @@ export function newChange(vaultBase, slug, { sessionRel = '', dateStr, simple =
|
|
|
119
119
|
writeFileSync(exampleDelta, files.specDelta, 'utf8');
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
+
if (!existed) captureSpecBaseline(vaultBase, dir);
|
|
122
123
|
setActiveChange(vaultBase, slug);
|
|
123
124
|
return { rel: changeDirRel(slug, vaultBase), created: !existed };
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
export function useChange(vaultBase, slug) {
|
|
128
|
+
const dir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|
|
129
|
+
if (!existsSync(join(dir, 'proposta.md'))) return { ok: false, error: `change aberta não encontrada: ${slug}` };
|
|
130
|
+
setActiveChange(vaultBase, slug);
|
|
131
|
+
return { ok: true, rel: changeDirRel(slug, vaultBase) };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function continueChange(vaultBase, archivedSlug, newSlug, options = {}) {
|
|
135
|
+
const loc = getLocale(vaultBase);
|
|
136
|
+
const archiveDir = join(vaultBase, loc.folders.changes, ARCHIVE_DIR);
|
|
137
|
+
let names = [];
|
|
138
|
+
try { names = readdirSync(archiveDir).filter((name) => statSync(join(archiveDir, name)).isDirectory()); } catch { /* no archive */ }
|
|
139
|
+
const matches = names.filter((name) => name === archivedSlug || name.endsWith(`-${archivedSlug}`));
|
|
140
|
+
if (!matches.length) return { ok: false, error: `change arquivada não encontrada: ${archivedSlug}` };
|
|
141
|
+
if (matches.length > 1) return { ok: false, error: `change arquivada ambígua: ${archivedSlug} (${matches.join(', ')})` };
|
|
142
|
+
if (existsSync(join(vaultBase, loc.folders.changes, newSlug, 'proposta.md'))) {
|
|
143
|
+
return { ok: false, error: `change de continuação já existe: ${newSlug}` };
|
|
144
|
+
}
|
|
145
|
+
const archivedName = matches[0];
|
|
146
|
+
const archivedProposal = join(loc.folders.changes, ARCHIVE_DIR, archivedName, 'proposta');
|
|
147
|
+
const result = newChange(vaultBase, newSlug, options);
|
|
148
|
+
const proposalPath = join(vaultBase, loc.folders.changes, newSlug, 'proposta.md');
|
|
149
|
+
let proposal = readFileSync(proposalPath, 'utf8');
|
|
150
|
+
proposal = proposal.replace(/^specs:/m, `continues: "${wikilinkFromRel(archivedProposal)}"\nspecs:`);
|
|
151
|
+
const heading = loc.id === 'en' ? '## Continuation' : '## Continuação';
|
|
152
|
+
const note = loc.id === 'en'
|
|
153
|
+
? `Continues ${wikilinkFromRel(archivedProposal)}. Archived evidence and verdict are not inherited.`
|
|
154
|
+
: `Continua ${wikilinkFromRel(archivedProposal)}. Evidências e verdict da change arquivada não são herdados.`;
|
|
155
|
+
proposal = `${proposal.trimEnd()}\n\n${heading}\n\n${note}\n`;
|
|
156
|
+
writeFileSync(proposalPath, proposal, 'utf8');
|
|
157
|
+
return { ok: true, ...result, archived: archivedName };
|
|
158
|
+
}
|
|
159
|
+
|
|
126
160
|
export function parseTasks(md) {
|
|
127
161
|
const tasks = [];
|
|
128
162
|
const re = /^-\s+\[( |x)\]\s+(\S+)\s+(.*)$/gm;
|
|
@@ -167,22 +201,67 @@ export function listChanges(vaultBase) {
|
|
|
167
201
|
try {
|
|
168
202
|
archived = readdirSync(join(base, ARCHIVE_DIR)).filter((n) => !n.startsWith('.'));
|
|
169
203
|
} catch { /* none */ }
|
|
170
|
-
return { active, archived };
|
|
204
|
+
return { active: active.sort(), archived: archived.sort() };
|
|
171
205
|
}
|
|
172
206
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
207
|
+
// Visão derivada de TODAS as changes abertas. CURRENT_CHANGE continua sendo o único foco para
|
|
208
|
+
// comandos implícitos; provider/session nunca filtram a fila, para que outro agente possa assumir
|
|
209
|
+
// o trabalho. O hash leva o conteúdo inteiro de cada tarefas.md — não apenas as contagens — pois
|
|
210
|
+
// ele controla a reinjeção por sessão dos hooks.
|
|
211
|
+
export function allChangesState(vaultBase) {
|
|
212
|
+
const current = activeChange(vaultBase);
|
|
213
|
+
const { active } = listChanges(vaultBase);
|
|
176
214
|
const chDir = getLocale(vaultBase).folders.changes;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
215
|
+
const fingerprint = [`current:${current}`];
|
|
216
|
+
const changes = active.map((slug) => {
|
|
217
|
+
let md = '';
|
|
218
|
+
let warning = '';
|
|
219
|
+
try { md = readFileSync(join(vaultBase, chDir, slug, 'tarefas.md'), 'utf8'); }
|
|
220
|
+
catch { warning = 'tarefas.md ausente ou ilegível'; }
|
|
221
|
+
const tasks = parseTasks(md);
|
|
222
|
+
const openTasks = tasks.filter((t) => !t.done);
|
|
223
|
+
fingerprint.push(`slug:${slug}`, `tasks:${md}`, `warning:${warning}`);
|
|
224
|
+
return {
|
|
225
|
+
slug,
|
|
226
|
+
current: slug === current,
|
|
227
|
+
openTasks,
|
|
228
|
+
openCount: openTasks.length,
|
|
229
|
+
doneCount: tasks.length - openTasks.length,
|
|
230
|
+
warning,
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
changes.sort((a, b) => Number(b.current) - Number(a.current) || a.slug.localeCompare(b.slug));
|
|
234
|
+
const pointerWarning = current && !changes.some((change) => change.current)
|
|
235
|
+
? `CURRENT_CHANGE aponta para change inexistente: ${current}`
|
|
236
|
+
: '';
|
|
237
|
+
if (pointerWarning) fingerprint.push(`pointer-warning:${pointerWarning}`);
|
|
238
|
+
return { current, changes, pointerWarning, hash: tasksHashOf(fingerprint.join('\n')) };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function renderOpenChanges(state, { tag = 'open_changes' } = {}) {
|
|
242
|
+
if (!state?.changes?.length && !state?.pointerWarning) return '';
|
|
243
|
+
const lines = [];
|
|
244
|
+
if (tag) lines.push(`<${tag}>`);
|
|
245
|
+
if (state.current) lines.push(`Change atual (comandos sem --change): ${state.current}.`);
|
|
246
|
+
else lines.push('Nenhuma change atual selecionada; comandos sem --change continuam recusados.');
|
|
247
|
+
if (state.pointerWarning) lines.push(`Aviso: ${state.pointerWarning}.`);
|
|
248
|
+
for (const change of state.changes || []) {
|
|
249
|
+
const label = change.current ? 'ATUAL' : 'ABERTA';
|
|
250
|
+
lines.push(`### ${label} — ${change.slug} (${change.openCount} aberta(s), ${change.doneCount} concluída(s))`);
|
|
251
|
+
if (change.warning) lines.push(`- Aviso: ${change.warning}.`);
|
|
252
|
+
else if (!change.openTasks.length) lines.push('- Nenhuma tarefa aberta.');
|
|
253
|
+
else for (const task of change.openTasks) lines.push(`- [ ] ${task.id} ${task.text}`);
|
|
254
|
+
}
|
|
255
|
+
if (state.current) lines.push('Para change atual: `wendkeep change done <id>`; antes de archive: `wendkeep verify`.');
|
|
256
|
+
lines.push('Qualquer agente pode assumir uma change: selecione-a com `wendkeep change use <slug>` ou use `--change <slug>` quando disponível.');
|
|
257
|
+
if (tag) lines.push(`</${tag}>`);
|
|
258
|
+
return lines.join('\n');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Mantém o nome exportado para consumidores internos existentes, mas agora injeta o backlog
|
|
262
|
+
// completo em vez de ocultar changes não selecionadas.
|
|
263
|
+
export function buildActiveChangeInjection(vaultBase) {
|
|
264
|
+
return renderOpenChanges(allChangesState(vaultBase));
|
|
186
265
|
}
|
|
187
266
|
|
|
188
267
|
export function activeChangeLink(vaultBase) {
|
|
@@ -232,14 +311,18 @@ export function writeSentinel(vaultBase, kind, sid, value = '1') {
|
|
|
232
311
|
} catch { /* fail-open: pior caso = aviso repetido */ }
|
|
233
312
|
}
|
|
234
313
|
|
|
235
|
-
// Estado
|
|
236
|
-
// change
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
return {
|
|
314
|
+
// Estado global usado pelo change-context: hash cobre qualquer tarefa aberta, inclusive de uma
|
|
315
|
+
// change que não esteja no ponteiro. As propriedades slug/openTasks preservam compatibilidade com
|
|
316
|
+
// consumidores antigos e descrevem somente a atual.
|
|
317
|
+
export function changeCtxState(vaultBase) {
|
|
318
|
+
const state = allChangesState(vaultBase);
|
|
319
|
+
if (!state.changes.length && !state.pointerWarning) return null;
|
|
320
|
+
const selected = state.changes.find((change) => change.current);
|
|
321
|
+
return {
|
|
322
|
+
...state,
|
|
323
|
+
slug: state.current,
|
|
324
|
+
openTasks: selected?.openTasks || [],
|
|
325
|
+
};
|
|
243
326
|
}
|
|
244
327
|
|
|
245
328
|
// GC das sentinelas (>7 dias) — seleção pura separada da execução (testável sem depender de
|
package/hooks/harness-doctor.mjs
CHANGED
|
@@ -3,13 +3,12 @@
|
|
|
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 {
|
|
6
|
+
import { buildEffectiveRequirementPackage, checkSpecsState, evaluateVerdict, tasksHashOf, validateSpecImpact } from './spec-core.mjs';
|
|
7
7
|
import { getLocale } from './locale.mjs';
|
|
8
8
|
|
|
9
9
|
export function checkHarness(vaultBase, projectRoot) {
|
|
10
10
|
const loc = getLocale(vaultBase);
|
|
11
11
|
const CHANGES_DIR = loc.folders.changes;
|
|
12
|
-
const SPECS_DIR = loc.folders.specs;
|
|
13
12
|
const errors = [];
|
|
14
13
|
const warnings = [];
|
|
15
14
|
|
|
@@ -23,16 +22,9 @@ export function checkHarness(vaultBase, projectRoot) {
|
|
|
23
22
|
} catch { errors.push('wendkeep.sensors.json: JSON inválido'); }
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
for (const f of readdirSync(join(vaultBase, SPECS_DIR))) {
|
|
30
|
-
if (!f.endsWith('.md') || f === 'README.md') continue;
|
|
31
|
-
const md = readFileSync(join(vaultBase, SPECS_DIR, f), 'utf8');
|
|
32
|
-
for (const r of parseRequirements(md)) if (r.id) knownReqs.add(r.id);
|
|
33
|
-
if (!/^>\s+Atualizado por/m.test(md)) warnings.push(`spec sem origem: ${SPECS_DIR}/${f}`);
|
|
34
|
-
}
|
|
35
|
-
} catch { /* sem 07-Specs */ }
|
|
25
|
+
const specState = checkSpecsState(vaultBase);
|
|
26
|
+
if (specState.missing) warnings.push('SPECS_STATE ausente — rode `wendkeep spec migrate`; 07-Specs deve ser gerado/read-only');
|
|
27
|
+
else if (!specState.ok) errors.push(`07-Specs alterado fora do WendKeep: ${specState.changed.join(', ')} — mova a alteração para 08-Mudanças/<change>/specs`);
|
|
36
28
|
|
|
37
29
|
// 2/3. Changes: malformed dirs; the active change's deltas add to knownReqs.
|
|
38
30
|
const active = activeChange(vaultBase);
|
|
@@ -46,15 +38,18 @@ export function checkHarness(vaultBase, projectRoot) {
|
|
|
46
38
|
const impact = validateSpecImpact(dir);
|
|
47
39
|
errors.push(...impact.errors.map((e) => `${name}: ${e}`));
|
|
48
40
|
warnings.push(...impact.warnings.map((w) => `${name}: ${w}`));
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
41
|
+
let tasks = [];
|
|
42
|
+
let tarefasMd = '';
|
|
43
|
+
try { tarefasMd = readFileSync(join(dir, 'tarefas.md'), 'utf8'); tasks = parseTasks(tarefasMd); } catch { /* sem tarefas */ }
|
|
44
|
+
const reqIds = [...new Set(tasks.map((t) => t.req).filter(Boolean))];
|
|
45
|
+
const effective = buildEffectiveRequirementPackage(vaultBase, dir, reqIds);
|
|
46
|
+
errors.push(...effective.errors.map((e) => `${name}: spec efetiva inválida: ${e}`));
|
|
47
|
+
if (effective.missing.length) errors.push(`req órfão em ${name}: ${effective.missing.map((id) => `[req:${id}]`).join(', ')} não existe na spec efetiva`);
|
|
48
|
+
let verdict = null;
|
|
49
|
+
try { verdict = JSON.parse(readFileSync(join(dir, 'verdict.json'), 'utf8')); } catch { /* sem verdict */ }
|
|
50
|
+
if (verdict && reqIds.length) {
|
|
51
|
+
const v = evaluateVerdict(verdict, reqIds, { tasksHash: tasksHashOf(tarefasMd), effectiveSpecHash: effective.hash });
|
|
52
|
+
if (!v.ok) warnings.push(`verdict stale/incompleto em ${name}${v.missing.length ? `: falta cobrir ${v.missing.join(', ')}` : ''}`);
|
|
58
53
|
}
|
|
59
54
|
}
|
|
60
55
|
|
|
@@ -63,17 +58,6 @@ export function checkHarness(vaultBase, projectRoot) {
|
|
|
63
58
|
const dir = join(vaultBase, CHANGES_DIR, active);
|
|
64
59
|
if (!existsSync(join(dir, 'proposta.md'))) {
|
|
65
60
|
errors.push(`ponteiro CURRENT_CHANGE aponta pra change inexistente: ${active}`);
|
|
66
|
-
} else {
|
|
67
|
-
let tasks = [];
|
|
68
|
-
try { tasks = parseTasks(readFileSync(join(dir, 'tarefas.md'), 'utf8')); } catch { /* sem tarefas */ }
|
|
69
|
-
const reqIds = [...new Set(tasks.map((t) => t.req).filter(Boolean))];
|
|
70
|
-
for (const rid of reqIds) if (!knownReqs.has(rid)) errors.push(`req órfão em ${active}: [req:${rid}] não existe em 07-Specs nem no delta`);
|
|
71
|
-
let verdict = null;
|
|
72
|
-
try { verdict = JSON.parse(readFileSync(join(dir, 'verdict.json'), 'utf8')); } catch { /* sem verdict */ }
|
|
73
|
-
if (verdict && reqIds.length) {
|
|
74
|
-
const v = evaluateVerdict(verdict, reqIds);
|
|
75
|
-
if (!v.ok) warnings.push(`verdict stale em ${active}: falta cobrir ${v.missing.join(', ')}`);
|
|
76
|
-
}
|
|
77
61
|
}
|
|
78
62
|
}
|
|
79
63
|
|