wendkeep 0.77.0 → 0.78.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,29 @@ 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.78.0] — 2026-08-22
8
+
9
+ ### Added
10
+
11
+ - **Evidence Envelope v2.** `verify` liga cada prova a projeto, repositório, worktree, work session,
12
+ branch, base/HEAD, árvore do índice e digest determinístico de staged/unstaged/untracked por
13
+ SHA-256 canônico; texto normaliza CRLF/CR para LF e binários preservam bytes.
14
+ - **Proveniência por sensor.** Cada resultado registra comando efetivo sanitizado e hash,
15
+ timestamps, duração, exit code, digest da saída e tail sanitizado limitado; alteração da
16
+ configuração invalida o binding anterior.
17
+ - **Diagnóstico causal.** `change status`, doctor, handoff e Observer distinguem evidência `bound`,
18
+ `stale`, `context-mismatch` e `legacy-unbound`; pacote deep e verdict carregam o mesmo
19
+ `evidenceEnvelopeId` e `evidenceBinding` completo e não podem ser reutilizados entre worktrees.
20
+
21
+ ### Changed
22
+
23
+ - **Publicação fail-closed.** Mudança de HEAD durante os sensores preserva a evidência anterior e
24
+ retorna `WENDKEEP_EVIDENCE_HEAD_CHANGED`. Evidência, pacote, verdict trivial e seals são gravados
25
+ por temporário path-safe no mesmo diretório e rename atômico.
26
+ - **Compatibilidade explícita.** Evidência v1 continua legível para histórico, mas sensores v1 não
27
+ satisfazem autoridade v2 no archive. README, schema e guias PT-BR/EN documentam formato,
28
+ normalização, recovery e vínculo do passe independente.
29
+
7
30
  ## [0.77.0] — 2026-08-22
8
31
 
9
32
  ### Added
package/README.en.md CHANGED
@@ -531,7 +531,7 @@ explore → propose → apply (TDD) → verify → archive
531
531
 
532
532
  - **Propose** — `wendkeep change new <slug>` scaffolds `08-Mudanças/<slug>/` (`proposta.md`, `design.md`, `tarefas.md`; `--simple` skips the design). `--guide` creates the compact GUIDE contract and omits automatic design/spec/ADR when `contract_impact:none`. It becomes the global *current* change. When the change declares `spec_impact: required`, you author the delta yourself at `specs/<capability>/spec.md` — there is no placeholder to delete. Multiple changes may remain open: `change list`/`status` and the hooks show every pending one, while commands without `--change` act on the current one alone. `change use <slug>` changes focus and `change continue <archived> <new>` creates an auditable continuation.
533
533
  - **Apply** — implement each `tarefas.md` task. Mark machine proof with one or more `[sensor:<id>]` tags on the same task: every distinct ID enters the gate once, in declaration order. Also mark satisfied requirements with one or more `[req:<ID>]` tags.
534
- - **Verify** — `wendkeep verify` runs the sensors your tasks declared (from `wendkeep.sensors.json` at the project root) and writes `evidencia.json`. A red `critical` fails the gate; a red `warning` is advisory. Failures retain only a bounded, sanitized diagnostic; green output is not persisted. `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`. Every change needs a `verdict.json` to archive; `verify --deep` writes a trivial one automatically when the change declares no `[req:]`.
534
+ - **Verify** — `wendkeep verify` runs declared sensors and writes an **Evidence Envelope v2** to `evidencia.json`, SHA-256-bound to project/repository/worktree/session, HEAD, index tree, normalized worktree digest, tasks, effective spec, and sensor configuration. Each sensor records a sanitized command, execution window, duration, exit code, output digest, and a sanitized tail bounded to 2,000 characters. If HEAD changes during the run, no new envelope is published. `change status` reports `bound`, `stale`, `context-mismatch`, or `legacy-unbound`; v1 evidence remains readable but cannot satisfy v2 authority. The public schema is [`schema/wendkeep.evidence-envelope-v2.schema.json`](schema/wendkeep.evidence-envelope-v2.schema.json). `verify --deep` binds both package and verdict to the current `envelope_id`.
535
535
  - **Archive** — `wendkeep change archive <slug>` **gates** on the evidence (blocks unless every declared critical sensor is green), promotes each applicable spec delta (`ADDED`/`MODIFIED`/`REMOVED`) into the living `07-Specs/<capability>.md` and moves the change to `_arquivo/`. GOVERN/ASSURE mint an ADR in `04-Decisões/`; compact GUIDE with no contract impact does not mint one automatically.
536
536
 
537
537
  > The gate blocks unless the scaffold is filled, no task is open, evidence is fresh, and every declared requirement is covered. **`--force` waives exactly one of those — the open-task check — and is the human's call, never the agent's.** An unfilled scaffold, a red critical sensor, stale evidence, an orphan requirement or a missing verdict block regardless.
package/README.md CHANGED
@@ -531,7 +531,7 @@ explore → propose → apply (TDD) → verify → archive
531
531
 
532
532
  - **Propose** — `wendkeep change new <slug>` scaffolds `08-Mudanças/<slug>/` (`proposta.md`, `design.md`, `tarefas.md`; `--simple` skips the design). `--guide` creates the compact GUIDE contract and omits automatic design/spec/ADR when `contract_impact:none`. It becomes the global *current* change. When the change declares `spec_impact: required`, you author the delta yourself at `specs/<capability>/spec.md` — there is no placeholder to delete. Multiple changes may remain open: `change list`/`status` and the hooks show every pending one, while commands without `--change` act on the current one alone. `change use <slug>` changes focus and `change continue <archived> <new>` creates an auditable continuation.
533
533
  - **Apply** — implement each `tarefas.md` task. Mark machine proof with one or more `[sensor:<id>]` tags on the same task: every distinct ID enters the gate once, in declaration order. Also mark satisfied requirements with one or more `[req:<ID>]` tags.
534
- - **Verify** — `wendkeep verify` runs the sensors your tasks declared (from `wendkeep.sensors.json` at the project root) and writes `evidencia.json`. A red `critical` fails the gate; a red `warning` is advisory. Failures retain only a bounded, sanitized diagnostic; green output is not persisted. `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`. Every change needs a `verdict.json` to archive; `verify --deep` writes a trivial one automatically when the change declares no `[req:]`.
534
+ - **Verify** — `wendkeep verify` runs declared sensors and writes an **Evidence Envelope v2** to `evidencia.json`, SHA-256-bound to project/repository/worktree/session, HEAD, index tree, normalized worktree digest, tasks, effective spec, and sensor configuration. Each sensor records a sanitized command, execution window, duration, exit code, output digest, and a sanitized tail bounded to 2,000 characters. If HEAD changes during the run, no new envelope is published. `change status` reports `bound`, `stale`, `context-mismatch`, or `legacy-unbound`; v1 evidence remains readable but cannot satisfy v2 authority. The public schema is [`schema/wendkeep.evidence-envelope-v2.schema.json`](schema/wendkeep.evidence-envelope-v2.schema.json). `verify --deep` binds both package and verdict to the current `envelope_id`.
535
535
  - **Archive** — `wendkeep change archive <slug>` **gates** on the evidence (blocks unless every declared critical sensor is green), promotes each applicable spec delta (`ADDED`/`MODIFIED`/`REMOVED`) into the living `07-Specs/<capability>.md` and moves the change to `_arquivo/`. GOVERN/ASSURE mint an ADR in `04-Decisões/`; compact GUIDE with no contract impact does not mint one automatically.
536
536
 
537
537
  > The gate blocks unless the scaffold is filled, no task is open, evidence is fresh, and every declared requirement is covered. **`--force` waives exactly one of those — the open-task check — and is the human's call, never the agent's.** An unfilled scaffold, a red critical sensor, stale evidence, an orphan requirement or a missing verdict block regardless.
@@ -76,7 +76,12 @@ npx wendkeep sensors add api-contracts "npm run test:contracts" --severity criti
76
76
  An archived change promotes its delta into the living spec when applicable and preserves proposal,
77
77
  tasks/proof, and design when present. GOVERN/ASSURE mint an ADR; compact GUIDE with no contract
78
78
  impact does not mint one automatically. Archive passes only with closed tasks, green required
79
- sensors, and a fresh verdict.
79
+ sensors, and a fresh verdict bound to the same Evidence Envelope v2. V1 evidence is reported as
80
+ `legacy-unbound`; `change status <slug>` also diagnoses `bound`, `stale`, and `context-mismatch`.
81
+ Archive compares the package/verdict `evidenceEnvelopeId` and complete `evidenceBinding` with the
82
+ proven checkout. On a mismatch, return to the correct worktree/session and rerun `verify`,
83
+ `verify --deep`, and `wk-verify`. Fields, text/binary normalization, error codes, and recovery are
84
+ detailed in the [verify guide](verify.md).
80
85
 
81
86
  ## Tool-scope fence
82
87
 
@@ -105,6 +110,8 @@ selection/lease; never carry authorization from another conversation.
105
110
  - Sensor not executed: keep one or more `[sensor:id]` tags on the same checkbox line. Every
106
111
  distinct ID on that line is required and runs once, in declaration order.
107
112
  - Stale evidence: rerun `verify` and `verify --deep` after task/spec edits.
113
+ - Evidence from another worktree/session: return to the correct causal context; it cannot satisfy
114
+ the current archive even when every sensor is green.
108
115
  - Rebase conflict: resolve the delta or use `--accept-current` only when that is the decision.
109
116
 
110
117
  ## Next steps
@@ -42,7 +42,8 @@ npx wendkeep change use <slug>
42
42
  - **Exit 0:** all required sensors passed and evidence was written.
43
43
  - **Exit 1:** the gate ran, but at least one critical sensor was red or a mutant survived.
44
44
  - **Exit 2:** invalid usage/context, including `no change (--change or active)`, missing vault,
45
- unknown change, or invalid `wendkeep.sensors.json`.
45
+ unknown change, a project outside a Git repository, invalid `wendkeep.sensors.json`, or
46
+ `WENDKEEP_EVIDENCE_HEAD_CHANGED`.
46
47
 
47
48
  `verify --deep` writes `verificacao.json`; it does not replace the reviewer. The `wk-verify` skill
48
49
  must be run by a different author and writes `verdict.json`.
@@ -72,10 +73,22 @@ npx wendkeep memory status --gate --vault .MyApp-vault
72
73
 
73
74
  ## Expected result
74
75
 
75
- `evidencia.json` contains sensor results and a seal binds proof to the current `tarefas.md` hash.
76
- When a sensor is red, its entry receives only a local, sanitized diagnostic bounded to 2,000
77
- characters; stdout/stderr from green sensors is not persisted. Deep mode packages requirements,
78
- tasks, and evidence for read-only review; the verdict covers every `[req:]` before archive.
76
+ `evidencia.json` follows the [public v2 schema](../../../schema/wendkeep.evidence-envelope-v2.schema.json).
77
+ The envelope binds `project_id`, `repository_id`, `worktree_id`, `work_session_id`, change, and
78
+ branch to `base_sha`, `head_sha`, `index_tree_sha`, `worktree_digest`, tasks, effective spec, and
79
+ sensor configuration with complete SHA-256 digests. The worktree digest covers staged, unstaged,
80
+ untracked, rename, and delete state; paths use `/`, text normalizes CRLF/CR to LF, and binaries keep
81
+ their bytes. Binary classification honors Git `binary`/`-text` attributes and known binary
82
+ extensions (including `.bin`); ignored files are excluded.
83
+
84
+ Each sensor records its sanitized command and hash, start/end, duration, exit code, output digest,
85
+ and a sanitized tail bounded to 2,000 characters. Authority artifacts publish through a path-safe
86
+ temporary in the same directory and an atomic rename. In deep mode, `verificacao.json` and
87
+ `verdict.json` carry the same `evidenceEnvelopeId` and complete `evidenceBinding`; the independent
88
+ reviewer must preserve both.
89
+
90
+ V1 evidence remains readable as `legacy-unbound`, never as equivalent authority. Run
91
+ `wendkeep change status <slug>` to inspect `bound`, `stale`, or `context-mismatch`.
79
92
 
80
93
  ## Common errors and diagnosis
81
94
 
@@ -85,6 +98,10 @@ tasks, and evidence for read-only review; the verdict covers every `[req:]` befo
85
98
  - Red gate: inspect the bounded `note` field on the `evidencia.json` entry, fix the cause, and
86
99
  rerun; never choose `archive --force` on your own.
87
100
  - Missing/stale verdict: regenerate `--deep` and request a fresh independent pass.
101
+ - `WENDKEEP_EVIDENCE_HEAD_CHANGED`: HEAD moved while sensors ran; stabilize the checkout and rerun.
102
+ The previous evidence was not replaced.
103
+ - `legacy-unbound`, `stale`, or `context-mismatch`: return to the correct worktree/session, recover
104
+ the context when needed, and rerun `verify` plus `verify --deep`.
88
105
  - Surviving mutants: strengthen the discriminating test; after three rounds, review manually.
89
106
 
90
107
  ## Next steps
@@ -76,7 +76,12 @@ npx wendkeep sensors add api-contracts "npm run test:contracts" --severity criti
76
76
  A change arquivada move seu delta para o spec vivo quando aplicável e preserva proposta,
77
77
  tarefas/evidência e design quando existente. GOVERN/ASSURE geram ADR; GUIDE compacta sem impacto
78
78
  de contrato não gera ADR automático. O archive só passa com tarefas fechadas, sensores exigidos
79
- verdes e verdict atual.
79
+ verdes e verdict atual ligado ao mesmo Evidence Envelope v2. Evidência v1 é mostrada como
80
+ `legacy-unbound`; `change status <slug>` também diagnostica `bound`, `stale` e `context-mismatch`.
81
+ O archive compara `evidenceEnvelopeId` e o `evidenceBinding` completo de package/verdict com o
82
+ checkout provado. Se houver divergência, volte à worktree/sessão correta e rode `verify`,
83
+ `verify --deep` e `wk-verify` novamente. Campos, normalização textual/binária, códigos e recovery
84
+ estão detalhados no [guia de verify](verify.md).
80
85
 
81
86
  ## Cerca de escopo para ferramentas
82
87
 
@@ -105,6 +110,8 @@ exige uma nova seleção/lease; não use autorização de outra conversa.
105
110
  - Sensor não executado: mantenha uma ou mais tags `[sensor:id]` na mesma linha do checkbox. Todos
106
111
  os IDs distintos dessa linha são exigidos e executados uma vez, na ordem declarada.
107
112
  - Evidência stale: rode novamente `verify` e `verify --deep` depois de alterar tarefas/spec.
113
+ - Evidência de outra worktree/sessão: retorne ao contexto causal correto; ela não satisfaz o
114
+ archive atual mesmo que todos os sensores estejam verdes.
108
115
  - Rebase em conflito: resolva o delta ou use `--accept-current` apenas quando isso for a decisão.
109
116
 
110
117
  ## Próximos passos
@@ -43,7 +43,8 @@ npx wendkeep change use <slug>
43
43
  - **Exit 1:** o gate executou, mas ao menos um sensor crítico ficou vermelho ou um mutante
44
44
  sobreviveu.
45
45
  - **Exit 2:** uso/contexto inválido, como `no change (--change or active)`, vault ausente,
46
- change inexistente ou `wendkeep.sensors.json` inválido.
46
+ change inexistente, projeto fora de um repositório Git, `wendkeep.sensors.json` inválido ou
47
+ `WENDKEEP_EVIDENCE_HEAD_CHANGED`.
47
48
 
48
49
  `verify --deep` gera `verificacao.json`; ele não substitui o verificador. A skill `wk-verify`
49
50
  precisa ser executada por autor diferente e grava `verdict.json`.
@@ -73,11 +74,22 @@ npx wendkeep memory status --gate --vault .MeuApp-vault
73
74
 
74
75
  ## Resultado esperado
75
76
 
76
- `evidencia.json` contém resultados dos sensores e um selo liga a prova ao hash atual de
77
- `tarefas.md`. Quando um sensor fica vermelho, sua entrada recebe somente um diagnóstico local
78
- sanitizado e limitado a 2.000 caracteres; stdout/stderr de sensores verdes não é persistido. No
79
- deep, o pacote contém requisitos, tarefas e evidência suficientes para revisão read-only; o
80
- verdict cobre cada `[req:]` antes do archive.
77
+ `evidencia.json` segue o [schema público v2](../../../schema/wendkeep.evidence-envelope-v2.schema.json).
78
+ O envelope liga `project_id`, `repository_id`, `worktree_id`, `work_session_id`, change e branch a
79
+ `base_sha`, `head_sha`, `index_tree_sha`, `worktree_digest`, tarefas, spec e configuração dos
80
+ sensores por SHA-256 completo. O digest cobre staged, unstaged, untracked, rename e delete; paths
81
+ usam `/`, texto normaliza CRLF/CR para LF e binários preservam bytes. A classificação binária
82
+ respeita atributos Git `binary`/`-text` e extensões binárias conhecidas (incluindo `.bin`); arquivos
83
+ ignorados não entram.
84
+
85
+ Cada sensor registra comando sanitizado e seu hash, início/fim, duração, exit code, digest da saída
86
+ e tail sanitizado de até 2.000 caracteres. Os artefatos são publicados por temporário path-safe no
87
+ mesmo diretório e rename atômico. No deep, `verificacao.json` e `verdict.json` carregam o mesmo
88
+ `evidenceEnvelopeId` e `evidenceBinding` completo; o verificador independente deve preservar ambos
89
+ no verdict.
90
+
91
+ Evidência v1 continua legível como `legacy-unbound`, nunca como autoridade equivalente. Rode
92
+ `wendkeep change status <slug>` para ver `bound`, `stale` ou `context-mismatch`.
81
93
 
82
94
  ## Erros comuns e diagnóstico
83
95
 
@@ -87,6 +99,10 @@ verdict cobre cada `[req:]` antes do archive.
87
99
  - Gate vermelho: consulte o campo `note` limitado da entrada em `evidencia.json`, corrija a causa
88
100
  e repita; não use `archive --force` por conta própria.
89
101
  - Verdict stale/ausente: regenere `--deep` e peça novo passe independente.
102
+ - `WENDKEEP_EVIDENCE_HEAD_CHANGED`: o HEAD mudou enquanto os sensores rodavam; estabilize o
103
+ checkout e repita. A evidência anterior não foi substituída.
104
+ - `legacy-unbound`, `stale` ou `context-mismatch`: volte à worktree/sessão correta, recupere o
105
+ contexto se necessário e rode `verify` + `verify --deep` novamente.
90
106
  - Mutantes sobreviventes: fortaleça o teste discriminante; após três rodadas, revise manualmente.
91
107
 
92
108
  ## Próximos passos
@@ -15,6 +15,7 @@ import {
15
15
  resolveActiveContext,
16
16
  setActiveContextChange,
17
17
  } from './active-context-store.mjs';
18
+ import { evidenceSensors } from '../packages/vault/src/evidence-envelope.mjs';
18
19
 
19
20
  export const ARCHIVE_DIR = '_arquivo';
20
21
  const POINTER = '.brain/CURRENT_CHANGE.md';
@@ -442,7 +443,7 @@ export function quickGateState(vaultBase, { context } = {}) {
442
443
  let redCritical = false;
443
444
  try {
444
445
  const ev = JSON.parse(readFileSync(join(dir, 'evidencia.json'), 'utf8'));
445
- redCritical = (Array.isArray(ev) ? ev : []).some((e) => e.status !== 'green' && (e.severity || 'critical') !== 'warning');
446
+ redCritical = evidenceSensors(ev).some((e) => e.status !== 'green' && (e.severity || 'critical') !== 'warning');
446
447
  } catch { /* sem/ilegível = não conta contra o nudge */ }
447
448
  let evidenceStale = false;
448
449
  try {
@@ -17,6 +17,14 @@ import {
17
17
  import { parseObservabilityCheckpoint } from './session-observability-state.mjs';
18
18
  import { readObservabilityStore } from './session-observability-store.mjs';
19
19
  import { assessObservabilityFreshness } from './session-observability-lifecycle.mjs';
20
+ import { evaluateEvidenceBinding } from '../packages/vault/src/evidence-envelope.mjs';
21
+ import { evidenceCheckoutBinding } from '../packages/vault/src/evidence-envelope.mjs';
22
+ import { loadSensorsDetailed, requiredSensors } from './sensors-core.mjs';
23
+ import {
24
+ captureGitSnapshot,
25
+ resolveEvidenceIdentity,
26
+ sensorConfigSha256,
27
+ } from '../src/evidence-envelope.mjs';
20
28
 
21
29
  export function checkSessionObservability(vaultBase, deps = {}) {
22
30
  const readRegistry = deps.readRegistry || readSessionRegistry;
@@ -144,10 +152,52 @@ export function checkHarness(vaultBase, projectRoot) {
144
152
  const effective = buildEffectiveRequirementPackage(vaultBase, dir, reqIds);
145
153
  errors.push(...effective.errors.map((e) => `${name}: spec efetiva inválida: ${e}`));
146
154
  if (effective.missing.length) errors.push(`req órfão em ${name}: ${effective.missing.map((id) => `[req:${id}]`).join(', ')} não existe na spec efetiva`);
155
+ let evidence = null;
156
+ try { evidence = JSON.parse(readFileSync(join(dir, 'evidencia.json'), 'utf8')); } catch { /* sem evidência */ }
157
+ if (evidence) {
158
+ const expected = {
159
+ change_slug: name,
160
+ tasks_sha256: tasksHashOf(tarefasMd),
161
+ effective_spec_sha256: `sha256:${effective.hash}`,
162
+ };
163
+ let bindingUnavailable = '';
164
+ if (evidence.schema_version === 2 && projectRoot) {
165
+ try {
166
+ const loaded = loadSensorsDetailed(projectRoot);
167
+ if (loaded.error) throw new Error(`wendkeep.sensors.json inválido: ${loaded.error}`);
168
+ expected.identity = resolveEvidenceIdentity({
169
+ vaultBase,
170
+ projectRoot,
171
+ changeSlug: name,
172
+ sessionId: evidence.work_session_id,
173
+ });
174
+ expected.snapshot = captureGitSnapshot(projectRoot);
175
+ expected.sensor_config_sha256 = sensorConfigSha256(
176
+ loaded.sensors,
177
+ requiredSensors(tasks),
178
+ );
179
+ } catch (error) {
180
+ bindingUnavailable = error.code || error.message;
181
+ }
182
+ }
183
+ const binding = evaluateEvidenceBinding(evidence, expected);
184
+ if (binding.state === 'legacy-unbound') {
185
+ attention.push(`${name}: evidence legacy-unbound — rode wendkeep verify novamente`);
186
+ } else if (binding.state !== 'bound') {
187
+ attention.push(`${name}: evidence ${binding.state} (${binding.reasons.join('; ')}) — rode wendkeep verify novamente`);
188
+ } else if (bindingUnavailable) {
189
+ attention.push(`${name}: evidence binding atual indisponível (${bindingUnavailable}) — rode doctor da raiz Git e depois wendkeep verify`);
190
+ }
191
+ }
147
192
  let verdict = null;
148
193
  try { verdict = JSON.parse(readFileSync(join(dir, 'verdict.json'), 'utf8')); } catch { /* sem verdict */ }
149
194
  if (verdict && reqIds.length) {
150
- const v = evaluateVerdict(verdict, reqIds, { tasksHash: tasksHashOf(tarefasMd), effectiveSpecHash: effective.hash });
195
+ const v = evaluateVerdict(verdict, reqIds, {
196
+ tasksHash: tasksHashOf(tarefasMd),
197
+ effectiveSpecHash: effective.hash,
198
+ evidenceEnvelopeId: evidence?.schema_version === 2 ? evidence.envelope_id : undefined,
199
+ evidenceBinding: evidence?.schema_version === 2 ? evidenceCheckoutBinding(evidence) : undefined,
200
+ });
151
201
  if (!v.ok) attention.push(`verdict stale/incompleto em ${name}${v.missing.length ? `: falta cobrir ${v.missing.join(', ')}` : ''}`);
152
202
  }
153
203
  }
@@ -5,12 +5,12 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
  import { getLocale } from './locale.mjs';
7
7
  import {
8
- assertVaultPathSafe, assertVaultPathsSafe, mkdirVaultPath, writeVaultFileSync,
8
+ assertVaultPathSafe, assertVaultPathsSafe, mkdirVaultPath, writeVaultFileAtomic, writeVaultFileSync,
9
9
  } from './vault-path-safety.mjs';
10
10
 
11
- // Short stable fingerprint of tarefas.md — freshness check between package/verdict and gate.
11
+ // Canonical SHA-256 fingerprint of tarefas.md — freshness binding between package/verdict and gate.
12
12
  export function tasksHashOf(md) {
13
- return createHash('sha1').update(String(md)).digest('hex').slice(0, 12);
13
+ return `sha256:${createHash('sha256').update(String(md).replace(/\r\n?/g, '\n')).digest('hex')}`;
14
14
  }
15
15
 
16
16
  export function contentHashOf(value) {
@@ -168,7 +168,11 @@ function recordPromotedSpecs(vaultBase, capabilities) {
168
168
  return state;
169
169
  }
170
170
 
171
- export function captureSpecBaseline(vaultBase, changeDir, { refresh = false } = {}) {
171
+ export function captureSpecBaseline(vaultBase, changeDir, {
172
+ refresh = false,
173
+ writeAtomic = writeVaultFileAtomic,
174
+ beforeRename,
175
+ } = {}) {
172
176
  const path = join(changeDir, SPEC_BASELINE_FILE);
173
177
  const checked = assertVaultPathSafe(vaultBase, path, {
174
178
  expectedType: 'file', label: 'baseline de specs da change',
@@ -177,12 +181,12 @@ export function captureSpecBaseline(vaultBase, changeDir, { refresh = false } =
177
181
  try { return JSON.parse(readFileSync(path, 'utf8')); } catch { /* rebuild malformed baseline */ }
178
182
  }
179
183
  const baseline = { version: 1, capturedAt: new Date().toISOString(), specs: readLivingSpecs(vaultBase) };
180
- writeVaultFileSync(
184
+ writeAtomic(
181
185
  vaultBase,
182
186
  path,
183
187
  `${JSON.stringify(baseline, null, 2)}\n`,
184
188
  'utf8',
185
- { label: 'baseline de specs da change' },
189
+ { scopeRoot: changeDir, label: 'baseline de specs da change', beforeRename },
186
190
  );
187
191
  return baseline;
188
192
  }
@@ -474,10 +478,24 @@ export function promoteSpecs(vaultBase, changeDir, specs, { changeWikilink, date
474
478
  // Gate check for the independent verdict (Wave A). A requirement-bearing change must have
475
479
  // a verdict that is ok and covers every declared req id. A requirement-less change passes:
476
480
  // nothing for an independent verifier to check — the sensor gate is already the proof.
477
- export function evaluateVerdict(verdict, reqIds, { tasksHash, effectiveSpecHash } = {}) {
481
+ export function evaluateVerdict(verdict, reqIds, {
482
+ tasksHash,
483
+ effectiveSpecHash,
484
+ evidenceEnvelopeId,
485
+ evidenceBinding,
486
+ } = {}) {
478
487
  const ids = reqIds || [];
479
- if (ids.length === 0) return { ok: true, missing: [] };
488
+ if (ids.length === 0 && !evidenceEnvelopeId) return { ok: true, missing: [] };
480
489
  if (!verdict || verdict.ok !== true) return { ok: false, missing: [] };
490
+ if (evidenceEnvelopeId && verdict.evidenceEnvelopeId !== evidenceEnvelopeId) {
491
+ return { ok: false, missing: [], stale: true };
492
+ }
493
+ if (evidenceBinding && (!verdict.evidenceBinding || Object.entries(evidenceBinding).some(
494
+ ([key, value]) => verdict.evidenceBinding[key] !== value,
495
+ ))) {
496
+ return { ok: false, missing: [], stale: true };
497
+ }
498
+ if (ids.length === 0) return { ok: true, missing: [] };
481
499
  // Freshness (G3/#6): a verdict minted against a different tarefas.md is stale. Verdicts
482
500
  // without a hash (pre-0.6.1) are accepted for backward compat.
483
501
  if (tasksHash && verdict.tasksHash && verdict.tasksHash !== tasksHash) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.77.0",
3
+ "version": "0.78.0",
4
4
  "description": "Vault-first persistent memory for AI coding agents, with an optional profile-aware governance runtime: OFF, FLOW, GUIDE, GOVERN, or ASSURE. Local-first and agent-agnostic (Claude Code, Codex, Cursor…).",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -41,7 +41,7 @@
41
41
  "node": ">=18"
42
42
  },
43
43
  "scripts": {
44
- "precheck": "node --check src/worktree.mjs && node --check src/worktree-cleanup.mjs && node --check src/context.mjs && node --check src/active-context-health.mjs && node --check src/active-context-runtime.mjs && node --check hooks/active-context-store.mjs && node --check hooks/change-core.mjs && node --check hooks/brain-inject.mjs && node --check hooks/change-context.mjs && node --check packages/vault/src/worktree-metadata.mjs",
44
+ "precheck": "node --check src/worktree.mjs && node --check src/worktree-cleanup.mjs && node --check src/evidence-envelope.mjs && node --check src/context.mjs && node --check src/active-context-health.mjs && node --check src/active-context-runtime.mjs && node --check hooks/active-context-store.mjs && node --check hooks/change-core.mjs && node --check hooks/brain-inject.mjs && node --check hooks/change-context.mjs && node --check packages/vault/src/worktree-metadata.mjs && node --check packages/vault/src/evidence-envelope.mjs",
45
45
  "check": "node --check scripts/release.mjs && node --check scripts/release-plan.mjs && node --check scripts/release-provenance.mjs && node --check scripts/run-scope.mjs && node --check src/release-provenance.mjs && node --check bin/wendkeep.mjs && node --check packages/cli/src/index.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/active-context-health.mjs && node --check src/project-vault.mjs && node --check src/observer-auth.mjs && node --check src/observer-privacy.mjs && node --check src/observer-snapshot.mjs && node --check src/observer-store.mjs && node --check src/observer-memory.mjs && node --check src/observer-memory-publish.mjs && node --check src/observer-sql-store.mjs && node --check src/observer-sql-migrate.mjs && node --check src/observer-sql-publish.mjs && node --check src/observer-transcript-store.mjs && node --check src/observer-server.mjs && node --check src/observer.mjs && node --check src/observer-publish.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check src/work-kind.mjs && node --check src/delivery.mjs && node --check web/observer/app.mjs && node --check hooks/observer-publish.mjs && node --check hooks/evidence-context.mjs && node --check hooks/active-context-handoff-evidence.mjs && node --check hooks/evidence-recall.mjs && node --check hooks/memory-scope.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/operating-profile-task-store.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/flow-store.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/integrations/src/host-hooks.mjs && node --check packages/integrations/src/hook-envelope.mjs && node --check packages/integrations/src/prompt-content.mjs && node --check packages/integrations/src/transcript-usage.mjs && node --check packages/integrations/src/transcripts.mjs && node --check packages/integrations/src/session-identity.mjs && node --check packages/integrations/src/index.mjs && node --check packages/mcp/src/config.mjs && node --check packages/mcp/src/index.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/locale.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-scope.mjs && node --check packages/vault/src/memory-candidate-policy.mjs && node --check packages/vault/src/evidence-recall.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
46
46
  "test": "node --test --test-concurrency=2",
47
47
  "test:core": "node scripts/run-scope.mjs core",
@@ -2,6 +2,7 @@
2
2
  // Pure-ish: `spawn` is injectable so runs are testable without a shell. Config lives
3
3
  // at the PROJECT ROOT (wendkeep.sensors.json); evidence lives per-change in the vault.
4
4
  import { spawnSync } from 'node:child_process';
5
+ import { createHash } from 'node:crypto';
5
6
  import { existsSync, readFileSync } from 'node:fs';
6
7
  import { dirname, join, resolve } from 'node:path';
7
8
 
@@ -22,6 +23,22 @@ function sanitizeSensorDiagnostic(value) {
22
23
  .trim();
23
24
  }
24
25
 
26
+ function sha256(value) {
27
+ return `sha256:${createHash('sha256').update(String(value || '')).digest('hex')}`;
28
+ }
29
+
30
+ function sensorNow(now) {
31
+ const value = typeof now === 'function' ? now() : (now || new Date().toISOString());
32
+ if (value instanceof Date) return value.toISOString();
33
+ if (typeof value === 'string') return value;
34
+ return new Date(value).toISOString();
35
+ }
36
+
37
+ function elapsedMilliseconds(startedAt, finishedAt) {
38
+ const elapsed = Date.parse(finishedAt) - Date.parse(startedAt);
39
+ return Number.isFinite(elapsed) ? Math.max(0, elapsed) : 0;
40
+ }
41
+
25
42
  function sensorFailureNote(result = {}) {
26
43
  const status = result.status ?? 'null';
27
44
  const header = [
@@ -86,11 +103,29 @@ export function requiredSensors(tasks) {
86
103
 
87
104
  export function runSensors(sensors, ids, { spawn = spawnSync, cwd, env, now } = {}) {
88
105
  const byId = Object.fromEntries((sensors || []).map((s) => [s.id, s]));
89
- const ts = now || new Date().toISOString();
90
106
  const evidence = [];
91
107
  for (const id of ids) {
108
+ const startedAt = sensorNow(now);
92
109
  const s = byId[id];
93
- if (!s) { evidence.push({ id, status: 'red', ts, severity: 'critical', note: 'sensor não definido' }); continue; }
110
+ if (!s) {
111
+ const finishedAt = sensorNow(now);
112
+ evidence.push({
113
+ id,
114
+ status: 'red',
115
+ ts: startedAt,
116
+ severity: 'critical',
117
+ started_at: startedAt,
118
+ finished_at: finishedAt,
119
+ duration_ms: elapsedMilliseconds(startedAt, finishedAt),
120
+ exit_code: null,
121
+ command: '',
122
+ command_sha256: sha256(''),
123
+ output_sha256: sha256(''),
124
+ output_tail: '',
125
+ note: 'sensor não definido',
126
+ });
127
+ continue;
128
+ }
94
129
  const r = spawn(s.command, [], {
95
130
  cwd,
96
131
  shell: true,
@@ -99,7 +134,26 @@ export function runSensors(sensors, ids, { spawn = spawnSync, cwd, env, now } =
99
134
  stdio: ['ignore', 'pipe', 'pipe'],
100
135
  ...(env ? { env } : {}),
101
136
  });
102
- const entry = { id, status: (r.status ?? 1) === 0 ? 'green' : 'red', ts, severity: s.severity || 'critical' };
137
+ const finishedAt = sensorNow(now);
138
+ const rawOutput = [r.stdout, r.stderr].filter(Boolean).join('\n');
139
+ const outputTail = sanitizeSensorDiagnostic(rawOutput);
140
+ const boundedOutputTail = outputTail.length > SENSOR_DIAGNOSTIC_MAX_LENGTH
141
+ ? `…${outputTail.slice(-(SENSOR_DIAGNOSTIC_MAX_LENGTH - 1))}`
142
+ : outputTail;
143
+ const entry = {
144
+ id,
145
+ status: (r.status ?? 1) === 0 ? 'green' : 'red',
146
+ ts: startedAt,
147
+ severity: s.severity || 'critical',
148
+ command: sanitizeSensorDiagnostic(s.command),
149
+ command_sha256: sha256(s.command),
150
+ started_at: startedAt,
151
+ finished_at: finishedAt,
152
+ duration_ms: elapsedMilliseconds(startedAt, finishedAt),
153
+ exit_code: Number.isInteger(r.status) ? r.status : null,
154
+ output_sha256: sha256(rawOutput),
155
+ output_tail: boundedOutputTail,
156
+ };
103
157
  if (entry.status === 'red') entry.note = sensorFailureNote(r);
104
158
  if (s.type === 'mutation' && s.report) {
105
159
  // Delegated mutation (Wave B): read the tool's mutation-testing-elements report and
@@ -0,0 +1,73 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ function stableValue(value) {
4
+ if (Array.isArray(value)) return value.map(stableValue);
5
+ if (value && typeof value === 'object' && !Buffer.isBuffer(value)) {
6
+ return Object.fromEntries(
7
+ Object.keys(value).sort().map((key) => [key, stableValue(value[key])]),
8
+ );
9
+ }
10
+ return value;
11
+ }
12
+
13
+ export function canonicalSha256(value) {
14
+ const bytes = Buffer.isBuffer(value) || value instanceof Uint8Array
15
+ ? value
16
+ : JSON.stringify(stableValue(value));
17
+ return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
18
+ }
19
+
20
+ export function evidenceSensors(evidence) {
21
+ if (Array.isArray(evidence)) return evidence;
22
+ return Array.isArray(evidence?.sensors) ? evidence.sensors : [];
23
+ }
24
+
25
+ const CHECKOUT_BINDING_KEYS = [
26
+ 'project_id', 'repository_id', 'worktree_id', 'head_sha', 'index_tree_sha', 'worktree_digest',
27
+ ];
28
+
29
+ export function evidenceCheckoutBinding(evidence = {}) {
30
+ return Object.fromEntries(CHECKOUT_BINDING_KEYS.map((key) => [key, evidence[key]]));
31
+ }
32
+
33
+ export function evidenceCheckoutBindingMatches(actual, expected) {
34
+ return Boolean(actual && expected) && CHECKOUT_BINDING_KEYS.every(
35
+ (key) => actual[key] === expected[key] && expected[key] != null,
36
+ );
37
+ }
38
+
39
+ export function evaluateEvidenceBinding(evidence, expected = {}) {
40
+ if (!evidence) return { state: 'unproven', reasons: ['evidence missing'] };
41
+ if (Array.isArray(evidence) || evidence.schema_version !== 2) {
42
+ return { state: 'legacy-unbound', reasons: ['evidence schema v1 has no checkout binding'] };
43
+ }
44
+
45
+ const contextReasons = [];
46
+ if (expected.change_slug != null && evidence.change_slug !== expected.change_slug) {
47
+ contextReasons.push('change_slug mismatch');
48
+ }
49
+ for (const key of ['project_id', 'repository_id', 'worktree_id', 'work_session_id']) {
50
+ if (expected.identity?.[key] != null && evidence[key] !== expected.identity[key]) {
51
+ contextReasons.push(`${key} mismatch`);
52
+ }
53
+ }
54
+ if (contextReasons.length) return { state: 'context-mismatch', reasons: contextReasons };
55
+
56
+ const staleReasons = [];
57
+ const unsigned = { ...evidence };
58
+ delete unsigned.envelope_id;
59
+ if (!evidence.envelope_id || canonicalSha256(unsigned) !== evidence.envelope_id) {
60
+ staleReasons.push('envelope_id invalid');
61
+ }
62
+ for (const key of ['branch', 'base_sha', 'head_sha', 'index_tree_sha', 'worktree_digest', 'dirty']) {
63
+ if (expected.snapshot?.[key] != null && evidence[key] !== expected.snapshot[key]) {
64
+ staleReasons.push(`${key} changed`);
65
+ }
66
+ }
67
+ for (const key of ['tasks_sha256', 'effective_spec_sha256', 'sensor_config_sha256']) {
68
+ if (expected[key] != null && evidence[key] !== expected[key]) staleReasons.push(`${key} changed`);
69
+ }
70
+ return staleReasons.length
71
+ ? { state: 'stale', reasons: staleReasons }
72
+ : { state: 'bound', reasons: [] };
73
+ }
@@ -8,5 +8,6 @@ export * from './memory-handoff.mjs';
8
8
  export * from './memory-store.mjs';
9
9
  export * from './memory-scope.mjs';
10
10
  export * from './evidence-recall.mjs';
11
+ export * from './evidence-envelope.mjs';
11
12
  export * from './validate-core.mjs';
12
13
  export * from './validate-memory.mjs';