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 +23 -0
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/docs/en/commands/changes-and-verification.md +8 -1
- package/docs/en/commands/verify.md +22 -5
- package/docs/pt-BR/commands/changes-and-verification.md +8 -1
- package/docs/pt-BR/commands/verify.md +22 -6
- package/hooks/change-core.mjs +2 -1
- package/hooks/harness-doctor.mjs +51 -1
- package/hooks/spec-core.mjs +26 -8
- package/package.json +2 -2
- package/packages/harness/src/sensors-core.mjs +57 -3
- package/packages/vault/src/evidence-envelope.mjs +73 -0
- package/packages/vault/src/index.mjs +1 -0
- package/packages/vault/src/memory-handoff.mjs +46 -5
- package/packages/vault/src/vault-path-safety.mjs +11 -0
- package/schema/wendkeep.evidence-envelope-v2.schema.json +92 -0
- package/src/change.mjs +93 -10
- package/src/evidence-envelope.mjs +288 -0
- package/src/skills-seed.mjs +11 -5
- package/src/verify.mjs +85 -22
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
|
|
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
|
|
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,
|
|
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`
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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`
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
package/hooks/change-core.mjs
CHANGED
|
@@ -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 = (
|
|
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 {
|
package/hooks/harness-doctor.mjs
CHANGED
|
@@ -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, {
|
|
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
|
}
|
package/hooks/spec-core.mjs
CHANGED
|
@@ -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
|
-
//
|
|
11
|
+
// Canonical SHA-256 fingerprint of tarefas.md — freshness binding between package/verdict and gate.
|
|
12
12
|
export function tasksHashOf(md) {
|
|
13
|
-
return createHash('
|
|
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, {
|
|
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
|
-
|
|
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, {
|
|
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.
|
|
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) {
|
|
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
|
|
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';
|