wendkeep 0.61.0 → 0.62.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,25 @@ 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.62.0] — 2026-07-28
8
+
9
+ ### Added
10
+
11
+ - **`wendkeep/harness` passa a expor o policy kernel reutilizável.** Os contratos dos cinco
12
+ Perfis de Operação e a engine de sensores podem ser importados pela superfície pública do pacote
13
+ raiz, com identidade validada a partir de um tarball realmente instalado.
14
+
15
+ ### Changed
16
+
17
+ - **As implementações canônicas de perfis e sensores agora pertencem a
18
+ `packages/harness/src`.** `src/operating-profile.mjs` e `hooks/sensors-core.mjs` permanecem como
19
+ fachadas de reexport, preservando funções, constantes, objetos congelados e consumidores atuais.
20
+ - **A direção modular do Harness virou contrato estrutural.** O workspace aceita somente built-ins,
21
+ módulos internos e a superfície pública do Vault; Vault continua proibido de depender do
22
+ Harness, e CLI, MCP, Integrations e Pi permanecem adaptadores superiores.
23
+ - **OFF preserva a semântica publicada:** Keep Core/Vault continua ativo e a governança automática
24
+ fica desligada; comandos explícitos do operador não ganham bloqueios novos nesta extração.
25
+
7
26
  ## [0.61.0] — 2026-07-28
8
27
 
9
28
  ### Added
package/README.en.md CHANGED
@@ -16,11 +16,11 @@
16
16
  **Persistent memory for AI coding agents, built on your Obsidian vault.** Every Claude Code **and Codex** session is captured turn by turn into local Markdown — `init` wires both (Codex asks you to approve its hooks once; `import` backfills past sessions either way) — with token/cost tracking and automatically extracted decisions, bugs, and learnings. That always-on plane is **Keep Core**. On top of it, **Wend Runtime** provides a native, zero-dependency lifecycle (spec → change → TDD → sensor-gated archive), selected through the `OFF`, `FLOW`, `GUIDE`, `GOVERN`, and `ASSURE` Operating Profiles. 100% local, open-core.
17
17
 
18
18
  The runtime is being separated into six physical boundaries — `cli`, `harness`, `vault`, `mcp`,
19
- `integrations`, and `pi` — without fragmenting installation. The private `vault` workspace now
20
- canonically owns safe binding and the Shared Project Memory v2 kernel (schema, mode, handoff,
21
- ledger, projection, and validation), exposed by the root package through `wendkeep/vault`.
22
- Historical imports, including installed bare subpaths such as `wendkeep/hooks/...`, keep working
23
- through compatibility facades and no session data needs migration;
19
+ `integrations`, and `pi` — without fragmenting installation. The private `harness` and `vault`
20
+ workspaces now canonically own Operating Profiles/the sensor engine and safe binding/the Shared
21
+ Project Memory v2 kernel, respectively. The root package exposes those surfaces through
22
+ `wendkeep/harness` and `wendkeep/vault`; historical imports keep working through compatibility
23
+ facades and no session data needs migration;
24
24
  see the [modular architecture](docs/en/architecture.md).
25
25
 
26
26
  ```bash
@@ -189,8 +189,9 @@ Operations that deserve step-by-step guidance: [verify and exits 0/1/2](https://
189
189
  ## Operating Profiles — Keep Core is always active
190
190
 
191
191
  **Keep Core is always active**: the Vault, sessions, identity, CORE/SHARED, lessons, costs,
192
- and persistence do not turn off with the harness. **Wend Runtime** controls only the governance
193
- layer:
192
+ and persistence do not turn off with the harness. **Wend Runtime** controls only the automatic
193
+ governance layer. Even under `OFF`, explicit WendKeep commands remain available; invoking one is a
194
+ deliberate opt-in and runs that command's own validations:
194
195
 
195
196
  | Profile | Route | Use |
196
197
  |---|---|---|
package/README.md CHANGED
@@ -16,11 +16,11 @@
16
16
  **Persistent memory for AI coding agents, built on your Obsidian vault.** Every Claude Code **and Codex** session is captured turn by turn into local Markdown — `init` wires both (Codex asks you to approve its hooks once; `import` backfills past sessions either way) — with token/cost tracking and automatically extracted decisions, bugs, and learnings. That always-on plane is **Keep Core**. On top of it, **Wend Runtime** provides a native, zero-dependency lifecycle (spec → change → TDD → sensor-gated archive), selected through the `OFF`, `FLOW`, `GUIDE`, `GOVERN`, and `ASSURE` Operating Profiles. 100% local, open-core.
17
17
 
18
18
  The runtime is being separated into six physical boundaries — `cli`, `harness`, `vault`, `mcp`,
19
- `integrations`, and `pi` — without fragmenting installation. The private `vault` workspace now
20
- canonically owns safe binding and the Shared Project Memory v2 kernel (schema, mode, handoff,
21
- ledger, projection, and validation), exposed by the root package through `wendkeep/vault`.
22
- Historical imports, including installed bare subpaths such as `wendkeep/hooks/...`, keep working
23
- through compatibility facades and no session data needs migration;
19
+ `integrations`, and `pi` — without fragmenting installation. The private `harness` and `vault`
20
+ workspaces now canonically own Operating Profiles/the sensor engine and safe binding/the Shared
21
+ Project Memory v2 kernel, respectively. The root package exposes those surfaces through
22
+ `wendkeep/harness` and `wendkeep/vault`; historical imports keep working through compatibility
23
+ facades and no session data needs migration;
24
24
  see the [modular architecture](docs/en/architecture.md).
25
25
 
26
26
  ```bash
@@ -189,8 +189,9 @@ Operations that deserve step-by-step guidance: [verify and exits 0/1/2](https://
189
189
  ## Operating Profiles — Keep Core is always active
190
190
 
191
191
  **Keep Core is always active**: the Vault, sessions, identity, CORE/SHARED, lessons, costs,
192
- and persistence do not turn off with the harness. **Wend Runtime** controls only the governance
193
- layer:
192
+ and persistence do not turn off with the harness. **Wend Runtime** controls only the automatic
193
+ governance layer. Even under `OFF`, explicit WendKeep commands remain available; invoking one is a
194
+ deliberate opt-in and runs that command's own validations:
194
195
 
195
196
  | Profile | Route | Use |
196
197
  |---|---|---|
@@ -8,6 +8,10 @@ Choose how much Wend Runtime governance an execution needs without disabling **K
8
8
  Keep Core is always active: the Vault, session, identity, CORE/SHARED, lessons, costs, and
9
9
  persistence integrations continue working under every profile.
10
10
 
11
+ The `OFF` profile disables automatic governance activation, not the CLI: explicit commands such as
12
+ `profile`, `flow`, `change`, `verify`, and `sensors` remain available. Invoking one is a deliberate
13
+ opt-in and runs that command's own validations and gates.
14
+
11
15
  ## When to use
12
16
 
13
17
  Use `profile` to inspect or explicitly select an Operating Profile. Use `FLOW` for local,
@@ -44,11 +48,28 @@ Every FLOW subcommand also accepts `--project <path>`, `--vault <path>`, and `--
44
48
  provided, `--session` scopes ID-based reads and mutations to the session that owns the FLOW; an
45
49
  ID from another session fails without mutation.
46
50
 
51
+ ## Ownership and programmatic surface
52
+
53
+ The private `packages/harness` workspace canonically owns Operating Profile resolution/policy and
54
+ the sensor engine. Programmatic consumers use the public root-package subpath:
55
+
56
+ ```js
57
+ import {
58
+ resolveOperatingProfile,
59
+ runSensors,
60
+ evaluateGate,
61
+ } from 'wendkeep/harness';
62
+ ```
63
+
64
+ `src/operating-profile.mjs` and `hooks/sensors-core.mjs` are compatibility facades only. Dependency
65
+ direction is `adapters (cli/mcp/integrations/pi) -> Harness -> Vault`; Vault never depends on
66
+ Harness. The workspaces remain private and are not published as independent npm packages.
67
+
47
68
  ## Options and exit codes
48
69
 
49
70
  | Profile | Route | Contract |
50
71
  |---|---|---|
51
- | `OFF` | LLM-native harness | Wend Runtime off; Keep Core intact. |
72
+ | `OFF` | LLM-native harness | Automatic governance off; Keep Core and explicit commands available. |
52
73
  | `FLOW` | E → V | Microcontract with Git baseline, allowlist, sensors, and receipt, without a change. |
53
74
  | `GUIDE` | P → E → V | Compact change; policy recognized for compatible evolution. |
54
75
  | `GOVERN` | P → R → E → V | Current a2 loop and conservative fallback. |
@@ -145,9 +166,10 @@ npx wendkeep flow promote $flowId --change-slug another-slug
145
166
  ## Expected result
146
167
 
147
168
  Changing profile neither creates a new session nor interrupts the Vault. In `OFF`, memory and
148
- lessons are still injected and Stop still persists the session/memory lifecycle, while the
149
- router, skill gate, change context/warn/nag/guard, and plan capture are inactive. A completed FLOW
150
- leaves a durable, inspectable receipt; a promoted FLOW enters the normal change lifecycle.
169
+ lessons are still injected and Stop still persists the session/memory lifecycle, while automatic
170
+ router, skill gate, change context/warn/nag/guard, and plan capture are inactive. Explicit commands
171
+ remain available and run their own contracts. A completed FLOW leaves a durable, inspectable
172
+ receipt; a promoted FLOW enters the normal change lifecycle.
151
173
 
152
174
  ## Common errors and diagnosis
153
175
 
@@ -8,6 +8,10 @@ Escolher quanta governança do Wend Runtime uma execução precisa sem desligar
8
8
  O Keep Core permanece sempre ativo: Vault, sessão, identidade, CORE/SHARED, lessons, custos e
9
9
  integrações de persistência continuam funcionando em todos os perfis.
10
10
 
11
+ O perfil `OFF` desativa a ativação automática da governança, não a CLI: comandos explícitos como
12
+ `profile`, `flow`, `change`, `verify` e `sensors` continuam disponíveis. Invocá-los é um opt-in
13
+ deliberado e executa as validações e gates próprios daquele comando.
14
+
11
15
  ## Quando usar
12
16
 
13
17
  Use `profile` para consultar ou selecionar explicitamente um Perfil de Operação. Use `FLOW` para
@@ -44,11 +48,29 @@ Todos os subcomandos FLOW também aceitam `--project <path>`, `--vault <path>` e
44
48
  Quando informado, `--session` restringe inclusive consultas e mutações por ID à sessão dona do
45
49
  FLOW; um ID de outra sessão falha sem mutação.
46
50
 
51
+ ## Ownership e superfície programática
52
+
53
+ O workspace privado `packages/harness` é o dono canônico da resolução/política dos Perfis de
54
+ Operação e da engine de sensores. Consumidores programáticos usam o subpath público do pacote raiz:
55
+
56
+ ```js
57
+ import {
58
+ resolveOperatingProfile,
59
+ runSensors,
60
+ evaluateGate,
61
+ } from 'wendkeep/harness';
62
+ ```
63
+
64
+ `src/operating-profile.mjs` e `hooks/sensors-core.mjs` são somente fachadas de compatibilidade. A
65
+ direção de dependências é `adapters (cli/mcp/integrations/pi) -> Harness -> Vault`; o Vault nunca
66
+ depende do Harness. Os workspaces continuam privados e não são publicados como pacotes npm
67
+ independentes.
68
+
47
69
  ## Opções e códigos de saída
48
70
 
49
71
  | Perfil | Rota | Contrato |
50
72
  |---|---|---|
51
- | `OFF` | harness nativo da LLM | Wend Runtime desligado; Keep Core integral. |
73
+ | `OFF` | harness nativo da LLM | Governança automática desligada; Keep Core e comandos explícitos disponíveis. |
52
74
  | `FLOW` | E → V | Microcontrato com baseline Git, allowlist, sensores e recibo, sem change. |
53
75
  | `GUIDE` | P → E → V | Change compacta; política reconhecida para evolução compatível. |
54
76
  | `GOVERN` | P → R → E → V | Loop a2 atual e fallback conservador. |
@@ -144,8 +166,9 @@ npx wendkeep flow promote $flowId --change-slug outro-slug
144
166
 
145
167
  Trocar o perfil não cria outra sessão nem interrompe o Vault. Em `OFF`, a memória e as lessons
146
168
  continuam injetadas e o Stop continua persistindo sessão/memória, mas router, skill gate,
147
- change context/warn/nag/guard e captura de plano ficam inativos. Um FLOW concluído deixa recibo
148
- durável e consultável; um FLOW promovido passa a seguir o lifecycle normal de change.
169
+ change context/warn/nag/guard e captura de plano automáticos ficam inativos. Os comandos explícitos
170
+ continuam disponíveis e executam seus próprios contratos. Um FLOW concluído deixa recibo durável e
171
+ consultável; um FLOW promovido passa a seguir o lifecycle normal de change.
149
172
 
150
173
  ## Erros comuns e diagnóstico
151
174
 
@@ -1,102 +1 @@
1
- // hooks/sensors-core.mjs — native sensor runner + evidence gate (Pilar C).
2
- // Pure-ish: `spawn` is injectable so runs are testable without a shell. Config lives
3
- // at the PROJECT ROOT (wendkeep.sensors.json); evidence lives per-change in the vault.
4
- import { spawnSync } from 'node:child_process';
5
- import { existsSync, readFileSync } from 'node:fs';
6
- import { dirname, join, resolve } from 'node:path';
7
-
8
- export const SENSOR_VAULT_ENV = 'WENDKEEP_SENSOR_VAULT';
9
-
10
- export function sensorProcessEnv(vaultBase, inherited = process.env) {
11
- return {
12
- ...inherited,
13
- OBSIDIAN_VAULT_PATH: vaultBase,
14
- [SENSOR_VAULT_ENV]: vaultBase,
15
- };
16
- }
17
-
18
- export function loadSensors(projectRoot, file = 'wendkeep.sensors.json') {
19
- return loadSensorsDetailed(projectRoot, file).sensors;
20
- }
21
-
22
- // Missing config and broken config are different failures: absent file usually means
23
- // wrong cwd (subdirectory), broken JSON means the config itself needs fixing. Collapsing
24
- // both into [] made every sensor report "sensor não definido" — a misleading diagnosis.
25
- export function loadSensorsDetailed(projectRoot, file = 'wendkeep.sensors.json') {
26
- const path = join(projectRoot, file);
27
- if (!existsSync(path)) return { sensors: [], missing: true, error: null, path };
28
- try {
29
- const data = JSON.parse(readFileSync(path, 'utf8'));
30
- return { sensors: Array.isArray(data.sensors) ? data.sensors : [], missing: false, error: null, path };
31
- } catch (e) {
32
- return { sensors: [], missing: false, error: e.message, path };
33
- }
34
- }
35
-
36
- // Climb the directory tree looking for a project marker (wendkeep.sensors.json or
37
- // .wendkeep.json), like git does with .git — shells in agent harnesses keep their cwd
38
- // across commands, so verify is often run from a subdirectory.
39
- export function findProjectRoot(startDir) {
40
- let dir = resolve(startDir);
41
- for (;;) {
42
- if (existsSync(join(dir, 'wendkeep.sensors.json')) || existsSync(join(dir, '.wendkeep.json'))) return dir;
43
- const parent = dirname(dir);
44
- if (parent === dir) return null;
45
- dir = parent;
46
- }
47
- }
48
-
49
- export function requiredSensors(tasks) {
50
- return [...new Set((tasks || []).flatMap((task) => (
51
- Array.isArray(task.sensors) && task.sensors.length ? task.sensors : [task.sensor]
52
- )).filter(Boolean))];
53
- }
54
-
55
- export function runSensors(sensors, ids, { spawn = spawnSync, cwd, env, now } = {}) {
56
- const byId = Object.fromEntries((sensors || []).map((s) => [s.id, s]));
57
- const ts = now || new Date().toISOString();
58
- const evidence = [];
59
- for (const id of ids) {
60
- const s = byId[id];
61
- if (!s) { evidence.push({ id, status: 'red', ts, severity: 'critical', note: 'sensor não definido' }); continue; }
62
- const r = spawn(s.command, [], { cwd, shell: true, stdio: 'ignore', ...(env ? { env } : {}) });
63
- const entry = { id, status: (r.status ?? 1) === 0 ? 'green' : 'red', ts, severity: s.severity || 'critical' };
64
- if (s.type === 'mutation' && s.report) {
65
- // Delegated mutation (Wave B): read the tool's mutation-testing-elements report and
66
- // attach surviving mutants so verify can turn them into fix tasks.
67
- try { entry.survivors = parseMutationReport(JSON.parse(readFileSync(join(cwd || '.', s.report), 'utf8'))); }
68
- catch { /* report ausente/ilegível — segue só com o exit code */ }
69
- }
70
- evidence.push(entry);
71
- }
72
- return evidence;
73
- }
74
-
75
- // Parse a mutation-testing-elements report (Stryker et al.): return surviving mutants
76
- // (Survived | NoCoverage) as {file, line, mutator}.
77
- export function parseMutationReport(json) {
78
- const out = [];
79
- const files = json && json.files ? json.files : {};
80
- for (const [file, data] of Object.entries(files)) {
81
- for (const m of (data && data.mutants) || []) {
82
- if (m.status === 'Survived' || m.status === 'NoCoverage') {
83
- out.push({ file, line: m.location && m.location.start ? m.location.start.line : null, mutator: m.mutatorName || 'unknown' });
84
- }
85
- }
86
- }
87
- return out;
88
- }
89
-
90
- // A required sensor blocks the gate when it is missing (never verified) or red at a
91
- // non-warning severity. Warnings are advisory: a red warning does not block archive.
92
- // Severity comes from the evidence entry (written by runSensors); absent -> critical.
93
- export function evaluateGate(evidence, requiredIds) {
94
- const byId = Object.fromEntries((evidence || []).map((e) => [e.id, e]));
95
- const failing = (requiredIds || []).filter((id) => {
96
- const e = byId[id];
97
- if (!e) return true; // never verified
98
- if (e.status === 'green') return false;
99
- return (e.severity || 'critical') !== 'warning';
100
- });
101
- return { ok: failing.length === 0, failing };
102
- }
1
+ export * from '../packages/harness/src/sensors-core.mjs';
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.61.0",
3
+ "version": "0.62.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": [
7
7
  "packages/*"
8
8
  ],
9
9
  "exports": {
10
+ "./harness": "./packages/harness/src/index.mjs",
10
11
  "./vault": "./packages/vault/src/index.mjs",
11
12
  "./*": "./*"
12
13
  },
@@ -30,7 +31,7 @@
30
31
  "node": ">=18"
31
32
  },
32
33
  "scripts": {
33
- "check": "node --check bin/wendkeep.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check hooks/operating-profile-runtime.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/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/memory-schema.mjs && node --check packages/vault/src/memory-mode.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",
34
+ "check": "node --check bin/wendkeep.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check hooks/operating-profile-runtime.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/operating-profile.mjs && node --check packages/harness/src/sensors-core.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/memory-schema.mjs && node --check packages/vault/src/memory-mode.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",
34
35
  "test": "node --test",
35
36
  "release": "node scripts/release.mjs",
36
37
  "release:dry": "node scripts/release.mjs --dry-run",
@@ -59,6 +60,7 @@
59
60
  "url": "https://github.com/rogersialves/wendkeep/issues"
60
61
  },
61
62
  "devDependencies": {
63
+ "acorn": "^8.18.0",
62
64
  "wendkeep": "^0.57.2"
63
65
  }
64
66
  }
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "name": "@wendkeep/harness",
3
3
  "private": true,
4
- "type": "module"
4
+ "type": "module",
5
+ "exports": "./src/index.mjs"
5
6
  }
@@ -0,0 +1,2 @@
1
+ export * from './operating-profile.mjs';
2
+ export * from './sensors-core.mjs';
@@ -0,0 +1,133 @@
1
+ export const OPERATING_PROFILES = Object.freeze([
2
+ 'OFF',
3
+ 'FLOW',
4
+ 'GUIDE',
5
+ 'GOVERN',
6
+ 'ASSURE',
7
+ ]);
8
+ export const DEFAULT_OPERATING_PROFILE = 'GOVERN';
9
+
10
+ const PROFILE_SET = new Set(OPERATING_PROFILES);
11
+
12
+ function policy(profile, route, options) {
13
+ return Object.freeze({
14
+ profile,
15
+ route: Object.freeze(route),
16
+ keepCore: true,
17
+ ...options,
18
+ });
19
+ }
20
+
21
+ export const OPERATING_PROFILE_POLICIES = Object.freeze({
22
+ OFF: policy('OFF', ['LLM'], {
23
+ harness: false,
24
+ contract: 'native',
25
+ requiresChange: false,
26
+ requiresReview: false,
27
+ requiresConfirmation: false,
28
+ }),
29
+ FLOW: policy('FLOW', ['E', 'V'], {
30
+ harness: true,
31
+ contract: 'flow',
32
+ requiresChange: false,
33
+ requiresReview: false,
34
+ requiresConfirmation: false,
35
+ }),
36
+ GUIDE: policy('GUIDE', ['P', 'E', 'V'], {
37
+ harness: true,
38
+ contract: 'simple-change',
39
+ requiresChange: true,
40
+ requiresReview: false,
41
+ requiresConfirmation: false,
42
+ }),
43
+ GOVERN: policy('GOVERN', ['P', 'R', 'E', 'V'], {
44
+ harness: true,
45
+ contract: 'change',
46
+ requiresChange: true,
47
+ requiresReview: true,
48
+ requiresConfirmation: false,
49
+ }),
50
+ ASSURE: policy('ASSURE', ['P', 'R', 'E', 'V', 'C'], {
51
+ harness: true,
52
+ contract: 'change',
53
+ requiresChange: true,
54
+ requiresReview: true,
55
+ requiresConfirmation: true,
56
+ }),
57
+ });
58
+
59
+ function invalidProfileError(value) {
60
+ const rendered = typeof value === 'string' ? `"${value}"` : String(value);
61
+ const error = new Error(
62
+ `Perfil de Operação inválido: ${rendered}. Use ${OPERATING_PROFILES.join(', ')}.`,
63
+ );
64
+ error.code = 'WENDKEEP_OPERATING_PROFILE_INVALID';
65
+ return error;
66
+ }
67
+
68
+ function canonicalProfile(value) {
69
+ if (typeof value !== 'string') return '';
70
+ return value.trim().toUpperCase();
71
+ }
72
+
73
+ export function normalizeOperatingProfile(value, { strict = false } = {}) {
74
+ const normalized = canonicalProfile(value);
75
+ if (PROFILE_SET.has(normalized)) return normalized;
76
+ if (strict) throw invalidProfileError(value);
77
+ return DEFAULT_OPERATING_PROFILE;
78
+ }
79
+
80
+ export function resolveOperatingProfile(config = {}) {
81
+ const harness = config && typeof config === 'object' && !Array.isArray(config)
82
+ && config.harness && typeof config.harness === 'object' && !Array.isArray(config.harness)
83
+ ? config.harness
84
+ : null;
85
+ const configured = !!harness && Object.prototype.hasOwnProperty.call(harness, 'profile');
86
+ if (!configured) {
87
+ return {
88
+ profile: DEFAULT_OPERATING_PROFILE,
89
+ source: 'default',
90
+ valid: true,
91
+ configured: false,
92
+ raw: null,
93
+ };
94
+ }
95
+
96
+ const raw = harness.profile;
97
+ const normalized = canonicalProfile(raw);
98
+ if (PROFILE_SET.has(normalized)) {
99
+ return {
100
+ profile: normalized,
101
+ source: 'project-binding',
102
+ valid: true,
103
+ configured: true,
104
+ raw,
105
+ };
106
+ }
107
+ return {
108
+ profile: DEFAULT_OPERATING_PROFILE,
109
+ source: 'default-invalid',
110
+ valid: false,
111
+ configured: true,
112
+ raw,
113
+ };
114
+ }
115
+
116
+ export function operatingProfilePolicy(value) {
117
+ return OPERATING_PROFILE_POLICIES[normalizeOperatingProfile(value)];
118
+ }
119
+
120
+ export function setOperatingProfile(config = {}, value) {
121
+ const profile = normalizeOperatingProfile(value, { strict: true });
122
+ const base = config && typeof config === 'object' && !Array.isArray(config) ? config : {};
123
+ const harness = base.harness && typeof base.harness === 'object' && !Array.isArray(base.harness)
124
+ ? base.harness
125
+ : {};
126
+ return {
127
+ ...base,
128
+ harness: {
129
+ ...harness,
130
+ profile,
131
+ },
132
+ };
133
+ }
@@ -0,0 +1,102 @@
1
+ // hooks/sensors-core.mjs — native sensor runner + evidence gate (Pilar C).
2
+ // Pure-ish: `spawn` is injectable so runs are testable without a shell. Config lives
3
+ // at the PROJECT ROOT (wendkeep.sensors.json); evidence lives per-change in the vault.
4
+ import { spawnSync } from 'node:child_process';
5
+ import { existsSync, readFileSync } from 'node:fs';
6
+ import { dirname, join, resolve } from 'node:path';
7
+
8
+ export const SENSOR_VAULT_ENV = 'WENDKEEP_SENSOR_VAULT';
9
+
10
+ export function sensorProcessEnv(vaultBase, inherited = process.env) {
11
+ return {
12
+ ...inherited,
13
+ OBSIDIAN_VAULT_PATH: vaultBase,
14
+ [SENSOR_VAULT_ENV]: vaultBase,
15
+ };
16
+ }
17
+
18
+ export function loadSensors(projectRoot, file = 'wendkeep.sensors.json') {
19
+ return loadSensorsDetailed(projectRoot, file).sensors;
20
+ }
21
+
22
+ // Missing config and broken config are different failures: absent file usually means
23
+ // wrong cwd (subdirectory), broken JSON means the config itself needs fixing. Collapsing
24
+ // both into [] made every sensor report "sensor não definido" — a misleading diagnosis.
25
+ export function loadSensorsDetailed(projectRoot, file = 'wendkeep.sensors.json') {
26
+ const path = join(projectRoot, file);
27
+ if (!existsSync(path)) return { sensors: [], missing: true, error: null, path };
28
+ try {
29
+ const data = JSON.parse(readFileSync(path, 'utf8'));
30
+ return { sensors: Array.isArray(data.sensors) ? data.sensors : [], missing: false, error: null, path };
31
+ } catch (e) {
32
+ return { sensors: [], missing: false, error: e.message, path };
33
+ }
34
+ }
35
+
36
+ // Climb the directory tree looking for a project marker (wendkeep.sensors.json or
37
+ // .wendkeep.json), like git does with .git — shells in agent harnesses keep their cwd
38
+ // across commands, so verify is often run from a subdirectory.
39
+ export function findProjectRoot(startDir) {
40
+ let dir = resolve(startDir);
41
+ for (;;) {
42
+ if (existsSync(join(dir, 'wendkeep.sensors.json')) || existsSync(join(dir, '.wendkeep.json'))) return dir;
43
+ const parent = dirname(dir);
44
+ if (parent === dir) return null;
45
+ dir = parent;
46
+ }
47
+ }
48
+
49
+ export function requiredSensors(tasks) {
50
+ return [...new Set((tasks || []).flatMap((task) => (
51
+ Array.isArray(task.sensors) && task.sensors.length ? task.sensors : [task.sensor]
52
+ )).filter(Boolean))];
53
+ }
54
+
55
+ export function runSensors(sensors, ids, { spawn = spawnSync, cwd, env, now } = {}) {
56
+ const byId = Object.fromEntries((sensors || []).map((s) => [s.id, s]));
57
+ const ts = now || new Date().toISOString();
58
+ const evidence = [];
59
+ for (const id of ids) {
60
+ const s = byId[id];
61
+ if (!s) { evidence.push({ id, status: 'red', ts, severity: 'critical', note: 'sensor não definido' }); continue; }
62
+ const r = spawn(s.command, [], { cwd, shell: true, stdio: 'ignore', ...(env ? { env } : {}) });
63
+ const entry = { id, status: (r.status ?? 1) === 0 ? 'green' : 'red', ts, severity: s.severity || 'critical' };
64
+ if (s.type === 'mutation' && s.report) {
65
+ // Delegated mutation (Wave B): read the tool's mutation-testing-elements report and
66
+ // attach surviving mutants so verify can turn them into fix tasks.
67
+ try { entry.survivors = parseMutationReport(JSON.parse(readFileSync(join(cwd || '.', s.report), 'utf8'))); }
68
+ catch { /* report ausente/ilegível — segue só com o exit code */ }
69
+ }
70
+ evidence.push(entry);
71
+ }
72
+ return evidence;
73
+ }
74
+
75
+ // Parse a mutation-testing-elements report (Stryker et al.): return surviving mutants
76
+ // (Survived | NoCoverage) as {file, line, mutator}.
77
+ export function parseMutationReport(json) {
78
+ const out = [];
79
+ const files = json && json.files ? json.files : {};
80
+ for (const [file, data] of Object.entries(files)) {
81
+ for (const m of (data && data.mutants) || []) {
82
+ if (m.status === 'Survived' || m.status === 'NoCoverage') {
83
+ out.push({ file, line: m.location && m.location.start ? m.location.start.line : null, mutator: m.mutatorName || 'unknown' });
84
+ }
85
+ }
86
+ }
87
+ return out;
88
+ }
89
+
90
+ // A required sensor blocks the gate when it is missing (never verified) or red at a
91
+ // non-warning severity. Warnings are advisory: a red warning does not block archive.
92
+ // Severity comes from the evidence entry (written by runSensors); absent -> critical.
93
+ export function evaluateGate(evidence, requiredIds) {
94
+ const byId = Object.fromEntries((evidence || []).map((e) => [e.id, e]));
95
+ const failing = (requiredIds || []).filter((id) => {
96
+ const e = byId[id];
97
+ if (!e) return true; // never verified
98
+ if (e.status === 'green') return false;
99
+ return (e.severity || 'critical') !== 'warning';
100
+ });
101
+ return { ok: failing.length === 0, failing };
102
+ }
@@ -1,133 +1 @@
1
- export const OPERATING_PROFILES = Object.freeze([
2
- 'OFF',
3
- 'FLOW',
4
- 'GUIDE',
5
- 'GOVERN',
6
- 'ASSURE',
7
- ]);
8
- export const DEFAULT_OPERATING_PROFILE = 'GOVERN';
9
-
10
- const PROFILE_SET = new Set(OPERATING_PROFILES);
11
-
12
- function policy(profile, route, options) {
13
- return Object.freeze({
14
- profile,
15
- route: Object.freeze(route),
16
- keepCore: true,
17
- ...options,
18
- });
19
- }
20
-
21
- export const OPERATING_PROFILE_POLICIES = Object.freeze({
22
- OFF: policy('OFF', ['LLM'], {
23
- harness: false,
24
- contract: 'native',
25
- requiresChange: false,
26
- requiresReview: false,
27
- requiresConfirmation: false,
28
- }),
29
- FLOW: policy('FLOW', ['E', 'V'], {
30
- harness: true,
31
- contract: 'flow',
32
- requiresChange: false,
33
- requiresReview: false,
34
- requiresConfirmation: false,
35
- }),
36
- GUIDE: policy('GUIDE', ['P', 'E', 'V'], {
37
- harness: true,
38
- contract: 'simple-change',
39
- requiresChange: true,
40
- requiresReview: false,
41
- requiresConfirmation: false,
42
- }),
43
- GOVERN: policy('GOVERN', ['P', 'R', 'E', 'V'], {
44
- harness: true,
45
- contract: 'change',
46
- requiresChange: true,
47
- requiresReview: true,
48
- requiresConfirmation: false,
49
- }),
50
- ASSURE: policy('ASSURE', ['P', 'R', 'E', 'V', 'C'], {
51
- harness: true,
52
- contract: 'change',
53
- requiresChange: true,
54
- requiresReview: true,
55
- requiresConfirmation: true,
56
- }),
57
- });
58
-
59
- function invalidProfileError(value) {
60
- const rendered = typeof value === 'string' ? `"${value}"` : String(value);
61
- const error = new Error(
62
- `Perfil de Operação inválido: ${rendered}. Use ${OPERATING_PROFILES.join(', ')}.`,
63
- );
64
- error.code = 'WENDKEEP_OPERATING_PROFILE_INVALID';
65
- return error;
66
- }
67
-
68
- function canonicalProfile(value) {
69
- if (typeof value !== 'string') return '';
70
- return value.trim().toUpperCase();
71
- }
72
-
73
- export function normalizeOperatingProfile(value, { strict = false } = {}) {
74
- const normalized = canonicalProfile(value);
75
- if (PROFILE_SET.has(normalized)) return normalized;
76
- if (strict) throw invalidProfileError(value);
77
- return DEFAULT_OPERATING_PROFILE;
78
- }
79
-
80
- export function resolveOperatingProfile(config = {}) {
81
- const harness = config && typeof config === 'object' && !Array.isArray(config)
82
- && config.harness && typeof config.harness === 'object' && !Array.isArray(config.harness)
83
- ? config.harness
84
- : null;
85
- const configured = !!harness && Object.prototype.hasOwnProperty.call(harness, 'profile');
86
- if (!configured) {
87
- return {
88
- profile: DEFAULT_OPERATING_PROFILE,
89
- source: 'default',
90
- valid: true,
91
- configured: false,
92
- raw: null,
93
- };
94
- }
95
-
96
- const raw = harness.profile;
97
- const normalized = canonicalProfile(raw);
98
- if (PROFILE_SET.has(normalized)) {
99
- return {
100
- profile: normalized,
101
- source: 'project-binding',
102
- valid: true,
103
- configured: true,
104
- raw,
105
- };
106
- }
107
- return {
108
- profile: DEFAULT_OPERATING_PROFILE,
109
- source: 'default-invalid',
110
- valid: false,
111
- configured: true,
112
- raw,
113
- };
114
- }
115
-
116
- export function operatingProfilePolicy(value) {
117
- return OPERATING_PROFILE_POLICIES[normalizeOperatingProfile(value)];
118
- }
119
-
120
- export function setOperatingProfile(config = {}, value) {
121
- const profile = normalizeOperatingProfile(value, { strict: true });
122
- const base = config && typeof config === 'object' && !Array.isArray(config) ? config : {};
123
- const harness = base.harness && typeof base.harness === 'object' && !Array.isArray(base.harness)
124
- ? base.harness
125
- : {};
126
- return {
127
- ...base,
128
- harness: {
129
- ...harness,
130
- profile,
131
- },
132
- };
133
- }
1
+ export * from '../packages/harness/src/operating-profile.mjs';