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 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` (some ship bundled templates, e.g. `wk-verify`'s `verdict-template.json` + reviewer prompt). `init` runs `wendkeep sync-defs` for you, delivering them to `.codex/agents/` + `.claude/skills/` (re‑run `sync-defs` after editing `.brain`).
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>` / `undone <id>` / `diff` / `archive [--force]`. |
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>` | Living specs: `list` / `show <capability>`. |
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 the project (`.codex/agents`, `.claude/skills`). |
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). The change becomes *active* and is injected at the next `SessionStart`, so the agent resumes work‑in‑progress.
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` 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.
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` also seeds **native process skills** (`wk-workflow`, `wk-tdd`, `wk-debugging`, `wk-brainstorming`, `wk-planning`, `wk-verify`) into `.brain/skills` and delivers them to `.claude/skills` the *how* layer, zero‑dep. Optional companions (`context-mode`, `dotcontext`, `understand-anything`, `caveman`) remain an opt‑in extra layer.
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` (`## ADDED Requirements` `### Requisito: UI-1 — dark mode toggle`). Then:
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 the living `07-Specs/ui.md`, mints an ADR, and the
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 the curated memory (CORE + DIGEST), the active change, project lessons, and a `<wk_process>` router that routes any non‑trivial task through the a2 loop (plan `change new` + fill the scaffold TDD → verify → archive).
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>` / `undone <id>` / `diff` / `archive [--force]`. |
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 *ativa* e é injetada no próximo `SessionStart`, então o agente retoma o trabalho em curso.
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 de volta a memória curada (CORE + DIGEST), a change ativa, as lições do projeto e um roteador `<wk_process>` que roteia qualquer tarefa não‑trivial pelo loop a2 (planejar `change new` + preencher o scaffold TDD → verify → archive).
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] | list | show | status |
47
- done <id> | undone <id> | diff | archive [--force] | abandon [slug].
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> Living specs: list | show <capability>.
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
  `;
@@ -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. Budget-capada. Nunca derruba o hook.
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: a change ativa acabou de ser injetada aqui, então o hook de
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);
@@ -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 change ativa: re-injeta <active_change_ping> (slug + tarefas abertas) SÓ quando o
4
- // estado mudou desde a última injeção (hash em sentinela por sessão) — o nudge do
5
- // SessionStart dilui/compacta; este devolve o loop ao contexto no momento certo.
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
- const lines = st.openTasks.map((t) => `- [ ] ${t.id} ${t.text}`);
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 change ativa: gate de skill para prompt-tarefa, 1x por sessão.
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);
@@ -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
- export function buildActiveChangeInjection(vaultBase, { maxTasks = 8 } = {}) {
174
- const slug = activeChange(vaultBase);
175
- if (!slug) return '';
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
- let md = '';
178
- try { md = readFileSync(join(vaultBase, chDir, slug, 'tarefas.md'), 'utf8'); } catch { return ''; }
179
- const open = parseTasks(md).filter((t) => !t.done).slice(0, maxTasks);
180
- const lines = open.map((t) => `- [ ] ${t.id} ${t.text}`);
181
- const more = open.length === maxTasks ? '\n*…mais tarefas em tarefas.md*' : '';
182
- return `<active_change>
183
- Mudança ativa: ${slug} — [[${chDir}/${slug}/proposta]]. Tarefas abertas:
184
- ${lines.join('\n')}${more}
185
- </active_change>`;
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 da change ativa para o ping do change-context: hash inclui o slug para que trocar de
236
- // change com tarefas idênticas ainda re-pingue.
237
- export function changeCtxState(vaultBase, { maxTasks = 5 } = {}) {
238
- const slug = activeChange(vaultBase);
239
- if (!slug) return null;
240
- let md = '';
241
- try { md = readFileSync(join(vaultBase, getLocale(vaultBase).folders.changes, slug, 'tarefas.md'), 'utf8'); } catch { /* sem tarefas */ }
242
- return { slug, hash: tasksHashOf(`${slug}\n${md}`), openTasks: parseTasks(md).filter((t) => !t.done).slice(0, maxTasks) };
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
@@ -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 { parseRequirements, parseSpecsList, parseDelta, evaluateVerdict, validateSpecImpact } from './spec-core.mjs';
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
- // Known requirement ids across the living specs (+ spec-without-source warning).
27
- const knownReqs = new Set();
28
- try {
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
- if (name === active) {
50
- try {
51
- for (const cap of parseSpecsList(readFileSync(join(dir, 'proposta.md'), 'utf8'))) {
52
- try {
53
- const d = parseDelta(readFileSync(join(dir, 'specs', cap, 'spec.md'), 'utf8'));
54
- for (const r of [...d.added, ...d.modified]) if (r.id) knownReqs.add(r.id);
55
- } catch { /* sem delta pra essa cap */ }
56
- }
57
- } catch { /* proposta ilegível */ }
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