wendkeep 0.61.0 → 0.63.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 +48 -0
- package/README.en.md +18 -7
- package/README.md +18 -7
- package/docs/en/commands/operating-profiles.md +26 -4
- package/docs/pt-BR/commands/operating-profiles.md +26 -3
- package/hooks/locale.mjs +1 -73
- package/hooks/sensors-core.mjs +1 -102
- package/hooks/vault-runtime-store.mjs +1 -558
- package/package.json +4 -2
- package/packages/harness/package.json +2 -1
- package/packages/harness/src/flow-store.mjs +558 -0
- package/packages/harness/src/index.mjs +3 -0
- package/packages/harness/src/operating-profile.mjs +133 -0
- package/packages/harness/src/sensors-core.mjs +102 -0
- package/packages/vault/src/index.mjs +1 -0
- package/packages/vault/src/locale.mjs +74 -0
- package/packages/vault/src/vault-path-safety.mjs +321 -152
- package/src/operating-profile.mjs +1 -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
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// packages/vault/src/locale.mjs — canonical vault locale and folder taxonomy.
|
|
2
|
+
// The locale is a property of the VAULT,
|
|
3
|
+
// stored at <vault>/.brain/config.json ({ "locale": "en" }); absent = pt-BR (full backward
|
|
4
|
+
// compat). Parsers stay bilingual everywhere; only RENDERING follows the locale.
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
export const LOCALES = {
|
|
9
|
+
'pt-BR': {
|
|
10
|
+
id: 'pt-BR',
|
|
11
|
+
folders: {
|
|
12
|
+
inbox: '00-Inbox',
|
|
13
|
+
project: '01-Projeto',
|
|
14
|
+
sessions: '02-Sessões',
|
|
15
|
+
linear: '03-Linear',
|
|
16
|
+
decisions: '04-Decisões',
|
|
17
|
+
bugs: '05-Bugs',
|
|
18
|
+
learnings: '06-Aprendizados',
|
|
19
|
+
specs: '07-Specs',
|
|
20
|
+
changes: '08-Mudanças',
|
|
21
|
+
},
|
|
22
|
+
months: ['01-JAN', '02-FEV', '03-MAR', '04-ABR', '05-MAI', '06-JUN', '07-JUL', '08-AGO', '09-SET', '10-OUT', '11-NOV', '12-DEZ'],
|
|
23
|
+
reqHeading: 'Requisito',
|
|
24
|
+
fixTaskVerb: 'mata mutante',
|
|
25
|
+
coreSections: ['Preferências do Usuário', 'Padrões Ativos', 'Pendências Abertas'],
|
|
26
|
+
},
|
|
27
|
+
en: {
|
|
28
|
+
id: 'en',
|
|
29
|
+
folders: {
|
|
30
|
+
inbox: '00-Inbox',
|
|
31
|
+
project: '01-Project',
|
|
32
|
+
sessions: '02-Sessions',
|
|
33
|
+
linear: '03-Linear',
|
|
34
|
+
decisions: '04-Decisions',
|
|
35
|
+
bugs: '05-Bugs',
|
|
36
|
+
learnings: '06-Learnings',
|
|
37
|
+
specs: '07-Specs',
|
|
38
|
+
changes: '08-Changes',
|
|
39
|
+
},
|
|
40
|
+
months: ['01-JAN', '02-FEB', '03-MAR', '04-APR', '05-MAY', '06-JUN', '07-JUL', '08-AUG', '09-SEP', '10-OCT', '11-NOV', '12-DEC'],
|
|
41
|
+
reqHeading: 'Requirement',
|
|
42
|
+
fixTaskVerb: 'kill mutant',
|
|
43
|
+
coreSections: ['User Preferences', 'Active Patterns', 'Open Items'],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const DEFAULT_LOCALE = 'pt-BR';
|
|
48
|
+
|
|
49
|
+
// Per-process cache: one vault per process (hooks + CLI), reads are hot paths.
|
|
50
|
+
const cache = new Map();
|
|
51
|
+
|
|
52
|
+
export function getLocale(vaultBase) {
|
|
53
|
+
if (!vaultBase) return LOCALES[DEFAULT_LOCALE];
|
|
54
|
+
const key = String(vaultBase);
|
|
55
|
+
if (cache.has(key)) return cache.get(key);
|
|
56
|
+
let id = DEFAULT_LOCALE;
|
|
57
|
+
try {
|
|
58
|
+
const data = JSON.parse(readFileSync(join(key, '.brain', 'config.json'), 'utf8'));
|
|
59
|
+
if (data.locale && LOCALES[data.locale]) id = data.locale;
|
|
60
|
+
} catch { /* sem config = pt-BR */ }
|
|
61
|
+
const loc = LOCALES[id];
|
|
62
|
+
cache.set(key, loc);
|
|
63
|
+
return loc;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Test hook: drop the memoized entry (tests rewrite config.json under one tmpdir).
|
|
67
|
+
export function clearLocaleCache() {
|
|
68
|
+
cache.clear();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The full vault taxonomy for a locale (folders + fixed entries), in creation order.
|
|
72
|
+
export function vaultFolders(loc) {
|
|
73
|
+
return [...Object.values(loc.folders), 'Templates', '.brain'];
|
|
74
|
+
}
|