wendkeep 0.2.6 → 0.3.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/README.md +2 -2
- package/bin/wendkeep.mjs +17 -2
- package/hooks/brain-inject.mjs +4 -1
- package/hooks/change-core.mjs +178 -0
- package/hooks/sensors-core.mjs +38 -0
- package/hooks/session-stop.mjs +9 -0
- package/package.json +1 -1
- package/src/change.mjs +90 -0
- package/src/dotcontext-seed.mjs +83 -0
- package/src/init.mjs +57 -12
- package/src/skills-seed.mjs +163 -0
- package/src/taxonomy.mjs +54 -2
- package/src/vault-theme.mjs +4 -0
- package/src/verify.mjs +41 -0
package/README.md
CHANGED
|
@@ -38,13 +38,13 @@ npx wendkeep init
|
|
|
38
38
|
1. Create the vault folder taxonomy and a templated `README.md` (default vault: `<project>/.<project-name>-vault`, e.g. `.MyApp-vault`; override with `--vault`).
|
|
39
39
|
2. **Merge** the three session hooks and `OBSIDIAN_VAULT_PATH` into `.claude/settings.json` — without clobbering your existing settings (a `.bak` is saved; an unparseable file is left untouched and a `.new` is written for you to merge).
|
|
40
40
|
3. Add the `wendkeep-vault` MCP server to `.mcp.json` (skip with `--no-mcp`).
|
|
41
|
-
4. Offer to pin **companion** plugins/MCP (multi-choice; `context-mode` pre-checked). Each is wired through the most agent-agnostic path it supports — `context-mode` as
|
|
41
|
+
4. Offer to pin **companion** plugins/MCP (multi-choice; `context-mode` + `dotcontext` pre-checked). Each is wired through the most agent-agnostic path it supports — `context-mode` and `dotcontext` as `.mcp.json` MCP servers (any agent), `understand-anything` via a `understand-inject` SessionStart hook that injects its domain graph when generated, and the Claude Code plugin layer (`extraKnownMarketplaces` + `enabledPlugins`) as a bonus. **`dotcontext`** (execution/PREVC harness) also gets its lifecycle hooks wired (SessionStart-last + Stop + PostToolUse, pinned CLI) and a starter `.context/config/sensors.json` seeded with a `wendkeep validate-memory` sensor plus one per detected `package.json` script (test/typecheck/lint/build) — add the `.context/runtime|cache|logs|plans|agents|skills/` paths to `.gitignore` (wendkeep doesn't touch git). If dotcontext is already configured globally (`~/.claude.json`), the project MCP entry is **skipped automatically** to avoid a duplicate; tune with `--dotcontext-mcp <auto|project|none>` and `--dotcontext-hooks <full|light|none>` (light drops PostToolUse). Control selection with `--companions <csv>` or `--no-companions`. (`caveman` additionally runs its own cross-agent installer on non-Claude agents.)
|
|
42
42
|
5. Install a **color system** into the vault's `.obsidian/`: a CSS snippet that accents notes by type (session/decision/bug/learning, via the `cssclasses` the hooks emit) plus graph color groups by folder. Non-destructive merge into `appearance.json`/`graph.json`; skip with `--no-colors`.
|
|
43
43
|
6. Seed the **curated memory layer**: `.brain/CORE.md` (the hand-curated hot layer, with the 3 required sections) and `.brain/COMPACTION_PROTOCOL.md` (the protocol guide). The auto layers (`DIGEST.md`, `index.jsonl`) are generated by the hooks. Validate the curated layer with `wendkeep validate-memory` (cap 25 lines, 3 sections, no secrets/PII).
|
|
44
44
|
7. Seed the **definitions layer**: `.brain/agents/` + `.brain/skills/` (versioned source of truth for custom agents/skills, with a README + one example each). Run `wendkeep sync-defs` to copy them into the project's agent dirs (`.brain/agents/*.toml` → `.codex/agents/`, `.brain/skills/<name>/` → `.claude/skills/`).
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
|
-
npx wendkeep init --vault "~/vaults/work" --project . --yes # non-interactive (companions: context-mode)
|
|
47
|
+
npx wendkeep init --vault "~/vaults/work" --project . --yes # non-interactive (companions: context-mode,dotcontext)
|
|
48
48
|
npx wendkeep init --companions "context-mode,caveman" --yes
|
|
49
49
|
npx wendkeep init --no-companions --yes
|
|
50
50
|
```
|
package/bin/wendkeep.mjs
CHANGED
|
@@ -29,10 +29,13 @@ Usage:
|
|
|
29
29
|
--vault <path> Obsidian vault folder (default: <project>/.<project-name>-vault).
|
|
30
30
|
--project <path> Project root to wire (default: current directory).
|
|
31
31
|
--no-mcp Do not add the mcpvault MCP server to .mcp.json.
|
|
32
|
-
--companions <csv> Companion plugins/MCP to pin: context-mode,caveman,understand-anything
|
|
33
|
-
(default when interactive/--yes: context-mode).
|
|
32
|
+
--companions <csv> Companion plugins/MCP to pin: context-mode,dotcontext,caveman,understand-anything
|
|
33
|
+
(default when interactive/--yes: context-mode,dotcontext).
|
|
34
34
|
--no-companions Skip companion plugins/MCP entirely.
|
|
35
35
|
--no-colors Skip the Obsidian color system (.obsidian snippet + graph groups).
|
|
36
|
+
--dotcontext-mcp <v> dotcontext MCP placement: auto (default; skip project entry
|
|
37
|
+
if already global), project, or none.
|
|
38
|
+
--dotcontext-hooks <v> dotcontext hooks: full (default), light (no PostToolUse), none.
|
|
36
39
|
--yes, -y Non-interactive; accept defaults.
|
|
37
40
|
--force Overwrite existing wendkeep config blocks.
|
|
38
41
|
|
|
@@ -40,6 +43,8 @@ Usage:
|
|
|
40
43
|
agent's JSON on stdin. Names: ${RUNNABLE_HOOKS.join(', ')}.
|
|
41
44
|
|
|
42
45
|
wendkeep doctor [--vault P] Run a vault health check.
|
|
46
|
+
wendkeep change <sub> Change lifecycle: new <slug> | list | show <slug> | archive <slug>.
|
|
47
|
+
wendkeep verify [--change s] Run a change's task sensors + record evidence (the gate).
|
|
43
48
|
wendkeep validate-memory [path] Validate .brain/CORE.md against the compaction
|
|
44
49
|
protocol (cap 25, 3 sections, no secrets/PII). Uses
|
|
45
50
|
--vault <path> or OBSIDIAN_VAULT_PATH if no path given.
|
|
@@ -96,6 +101,16 @@ async function main() {
|
|
|
96
101
|
runSyncDefs(rest);
|
|
97
102
|
break;
|
|
98
103
|
}
|
|
104
|
+
case 'change': {
|
|
105
|
+
const { runChange } = await import('../src/change.mjs');
|
|
106
|
+
runChange(rest);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case 'verify': {
|
|
110
|
+
const { runVerify } = await import('../src/verify.mjs');
|
|
111
|
+
runVerify(rest);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
99
114
|
case '--version':
|
|
100
115
|
case '-v':
|
|
101
116
|
process.stdout.write(`${version()}\n`);
|
package/hooks/brain-inject.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { join } from 'node:path';
|
|
|
7
7
|
import { pathToFileURL } from 'node:url';
|
|
8
8
|
import { getVaultBase, readHookInput, writeHookOutput } from './obsidian-common.mjs';
|
|
9
9
|
import { brainDir } from './brain-core.mjs';
|
|
10
|
+
import { buildActiveChangeInjection } from './change-core.mjs';
|
|
10
11
|
|
|
11
12
|
const MAX_LINES = 45; // CORE ≤25 + DIGEST ≤15 + folga; salvaguarda se o CORE crescer à mão
|
|
12
13
|
|
|
@@ -23,7 +24,9 @@ export function buildInjection(vaultBase) {
|
|
|
23
24
|
lines = lines.slice(0, MAX_LINES);
|
|
24
25
|
lines.push('*…truncado pelo budget — fonte completa: .brain/CORE.md + .brain/DIGEST.md*');
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
+
const brain = ['<brain_memory>', ...lines, pointer, '</brain_memory>'].join('\n');
|
|
28
|
+
const change = buildActiveChangeInjection(vaultBase);
|
|
29
|
+
return change ? `${brain}\n${change}` : brain;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// hooks/change-core.mjs
|
|
2
|
+
// Native change/spec lifecycle in the vault (Pilar B). Vault-facing lib consumed by
|
|
3
|
+
// the `wendkeep change` CLI (src/change.mjs) and the brain-inject hook. No external deps.
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { ensureDir, wikilinkFromRel } from './obsidian-common.mjs';
|
|
7
|
+
|
|
8
|
+
export const CHANGES_DIR = '08-Mudanças';
|
|
9
|
+
export const SPECS_DIR = '07-Specs';
|
|
10
|
+
export const ARCHIVE_DIR = '_arquivo';
|
|
11
|
+
const POINTER = '.brain/CURRENT_CHANGE.md';
|
|
12
|
+
|
|
13
|
+
export function changeDirRel(slug) {
|
|
14
|
+
return join(CHANGES_DIR, slug);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function renderChangeScaffold({ slug, sessionRel, dateStr }) {
|
|
18
|
+
const source = sessionRel ? `\n - "${wikilinkFromRel(sessionRel)}"` : ' []';
|
|
19
|
+
const proposta = `---
|
|
20
|
+
type: change
|
|
21
|
+
status: active
|
|
22
|
+
date: ${dateStr}
|
|
23
|
+
cssclasses:
|
|
24
|
+
- topic-change
|
|
25
|
+
tags:
|
|
26
|
+
- mudanca
|
|
27
|
+
source:${source}
|
|
28
|
+
specs: []
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
# ${slug}
|
|
32
|
+
|
|
33
|
+
## Por quê
|
|
34
|
+
|
|
35
|
+
(motivo da mudança)
|
|
36
|
+
|
|
37
|
+
## O que muda
|
|
38
|
+
|
|
39
|
+
(escopo da mudança)
|
|
40
|
+
`;
|
|
41
|
+
const design = `# ${slug} — design
|
|
42
|
+
|
|
43
|
+
## Abordagem
|
|
44
|
+
|
|
45
|
+
(abordagem técnica)
|
|
46
|
+
`;
|
|
47
|
+
const tarefas = `# ${slug} — tarefas
|
|
48
|
+
|
|
49
|
+
- [ ] 1.1 (primeira tarefa)
|
|
50
|
+
`;
|
|
51
|
+
return { proposta, design, tarefas };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function activeChange(vaultBase) {
|
|
55
|
+
try {
|
|
56
|
+
const m = readFileSync(join(vaultBase, POINTER), 'utf8').match(/^change:\s*(.+)$/m);
|
|
57
|
+
return m ? m[1].trim() : '';
|
|
58
|
+
} catch {
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function setActiveChange(vaultBase, slug) {
|
|
64
|
+
mkdirSync(join(vaultBase, '.brain'), { recursive: true });
|
|
65
|
+
writeFileSync(join(vaultBase, POINTER), `change: ${slug}\n`, 'utf8');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function clearActiveChange(vaultBase) {
|
|
69
|
+
const p = join(vaultBase, POINTER);
|
|
70
|
+
if (existsSync(p)) writeFileSync(p, 'change:\n', 'utf8');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function newChange(vaultBase, slug, { sessionRel = '', dateStr }) {
|
|
74
|
+
const dir = join(vaultBase, CHANGES_DIR, slug);
|
|
75
|
+
const existed = existsSync(join(dir, 'proposta.md'));
|
|
76
|
+
mkdirSync(dir, { recursive: true });
|
|
77
|
+
const files = renderChangeScaffold({ slug, sessionRel, dateStr });
|
|
78
|
+
const write = (name, content) => {
|
|
79
|
+
const f = join(dir, name);
|
|
80
|
+
if (!existsSync(f)) writeFileSync(f, content, 'utf8');
|
|
81
|
+
};
|
|
82
|
+
write('proposta.md', files.proposta);
|
|
83
|
+
write('design.md', files.design);
|
|
84
|
+
write('tarefas.md', files.tarefas);
|
|
85
|
+
setActiveChange(vaultBase, slug);
|
|
86
|
+
return { rel: changeDirRel(slug), created: !existed };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function parseTasks(md) {
|
|
90
|
+
const tasks = [];
|
|
91
|
+
const re = /^-\s+\[( |x)\]\s+(\S+)\s+(.*)$/gm;
|
|
92
|
+
const sensorRe = /\[sensor:\s*([\w.-]+)\]/;
|
|
93
|
+
let m;
|
|
94
|
+
while ((m = re.exec(String(md))) !== null) {
|
|
95
|
+
let text = m[3].trim();
|
|
96
|
+
const sm = text.match(sensorRe);
|
|
97
|
+
const sensor = sm ? sm[1] : undefined;
|
|
98
|
+
if (sm) text = text.replace(sensorRe, '').replace(/\s+/g, ' ').trim();
|
|
99
|
+
tasks.push({ id: m[2], text, done: m[1] === 'x', ...(sensor ? { sensor } : {}) });
|
|
100
|
+
}
|
|
101
|
+
return tasks;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function listChanges(vaultBase) {
|
|
105
|
+
const base = join(vaultBase, CHANGES_DIR);
|
|
106
|
+
const active = [];
|
|
107
|
+
let archived = [];
|
|
108
|
+
try {
|
|
109
|
+
for (const name of readdirSync(base)) {
|
|
110
|
+
if (name === ARCHIVE_DIR) continue;
|
|
111
|
+
if (existsSync(join(base, name, 'proposta.md'))) active.push(name);
|
|
112
|
+
}
|
|
113
|
+
} catch { /* no changes dir yet */ }
|
|
114
|
+
try {
|
|
115
|
+
archived = readdirSync(join(base, ARCHIVE_DIR)).filter((n) => !n.startsWith('.'));
|
|
116
|
+
} catch { /* none */ }
|
|
117
|
+
return { active, archived };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function buildActiveChangeInjection(vaultBase, { maxTasks = 8 } = {}) {
|
|
121
|
+
const slug = activeChange(vaultBase);
|
|
122
|
+
if (!slug) return '';
|
|
123
|
+
let md = '';
|
|
124
|
+
try { md = readFileSync(join(vaultBase, CHANGES_DIR, slug, 'tarefas.md'), 'utf8'); } catch { return ''; }
|
|
125
|
+
const open = parseTasks(md).filter((t) => !t.done).slice(0, maxTasks);
|
|
126
|
+
const lines = open.map((t) => `- [ ] ${t.id} ${t.text}`);
|
|
127
|
+
const more = open.length === maxTasks ? '\n*…mais tarefas em tarefas.md*' : '';
|
|
128
|
+
return `<active_change>
|
|
129
|
+
Mudança ativa: ${slug} — [[${CHANGES_DIR}/${slug}/proposta]]. Tarefas abertas:
|
|
130
|
+
${lines.join('\n')}${more}
|
|
131
|
+
</active_change>`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function activeChangeLink(vaultBase) {
|
|
135
|
+
const slug = activeChange(vaultBase);
|
|
136
|
+
return slug ? `Change ativa: [[${CHANGES_DIR}/${slug}/proposta]]` : '';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Gate seam (Pilar B stub; Pilar C replaces with real sensor evidence).
|
|
140
|
+
export function gateGreen() {
|
|
141
|
+
return { ok: true, failing: [] };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function archiveChange(vaultBase, slug, { gate = gateGreen, dateStr, adrNum }) {
|
|
145
|
+
const src = join(vaultBase, CHANGES_DIR, slug);
|
|
146
|
+
const verdict = gate(src);
|
|
147
|
+
if (!verdict.ok) return { ok: false, failing: verdict.failing || [] };
|
|
148
|
+
|
|
149
|
+
const destRel = join(CHANGES_DIR, ARCHIVE_DIR, `${dateStr}-${slug}`);
|
|
150
|
+
ensureDir(join(vaultBase, CHANGES_DIR, ARCHIVE_DIR));
|
|
151
|
+
renameSync(src, join(vaultBase, destRel));
|
|
152
|
+
|
|
153
|
+
const [year] = String(dateStr).split('-');
|
|
154
|
+
const adrDirRel = join('04-Decisões', year);
|
|
155
|
+
ensureDir(join(vaultBase, adrDirRel));
|
|
156
|
+
const num = String(adrNum).padStart(3, '0');
|
|
157
|
+
const adrRel = join(adrDirRel, `ADR-${num}-${slug}.md`);
|
|
158
|
+
const changeWikilink = wikilinkFromRel(join(destRel, 'proposta'));
|
|
159
|
+
writeFileSync(join(vaultBase, adrRel), `---
|
|
160
|
+
type: decision
|
|
161
|
+
status: accepted
|
|
162
|
+
date: ${dateStr}
|
|
163
|
+
cssclasses:
|
|
164
|
+
- topic-decision
|
|
165
|
+
tags:
|
|
166
|
+
- decisao
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
# ADR-${num} — ${slug}
|
|
170
|
+
|
|
171
|
+
## Decisão
|
|
172
|
+
|
|
173
|
+
Mudança ${changeWikilink} concluída e arquivada.
|
|
174
|
+
`, 'utf8');
|
|
175
|
+
|
|
176
|
+
clearActiveChange(vaultBase);
|
|
177
|
+
return { ok: true, failing: [], archivedRel: destRel, adrRel };
|
|
178
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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 { readFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
export function loadSensors(projectRoot, file = 'wendkeep.sensors.json') {
|
|
9
|
+
try {
|
|
10
|
+
const data = JSON.parse(readFileSync(join(projectRoot, file), 'utf8'));
|
|
11
|
+
return Array.isArray(data.sensors) ? data.sensors : [];
|
|
12
|
+
} catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function requiredSensors(tasks) {
|
|
18
|
+
return [...new Set((tasks || []).map((t) => t.sensor).filter(Boolean))];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function runSensors(sensors, ids, { spawn = spawnSync, cwd, now } = {}) {
|
|
22
|
+
const byId = Object.fromEntries((sensors || []).map((s) => [s.id, s]));
|
|
23
|
+
const ts = now || new Date().toISOString();
|
|
24
|
+
const evidence = [];
|
|
25
|
+
for (const id of ids) {
|
|
26
|
+
const s = byId[id];
|
|
27
|
+
if (!s) { evidence.push({ id, status: 'red', ts, note: 'sensor não definido' }); continue; }
|
|
28
|
+
const r = spawn(s.command, [], { cwd, shell: true, stdio: 'ignore' });
|
|
29
|
+
evidence.push({ id, status: (r.status ?? 1) === 0 ? 'green' : 'red', ts });
|
|
30
|
+
}
|
|
31
|
+
return evidence;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function evaluateGate(evidence, requiredIds) {
|
|
35
|
+
const byId = Object.fromEntries((evidence || []).map((e) => [e.id, e.status]));
|
|
36
|
+
const failing = (requiredIds || []).filter((id) => byId[id] !== 'green');
|
|
37
|
+
return { ok: failing.length === 0, failing };
|
|
38
|
+
}
|
package/hooks/session-stop.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { pathToFileURL } from 'url';
|
|
|
6
6
|
import { createLinkedNotes } from './linked-notes.mjs';
|
|
7
7
|
import { addUsage, costBreakdown, emptyTokenUsage, normalizeClaudeUsage, normalizeCodexUsage, priceForModel, updateSessionUsage } from './token-usage.mjs';
|
|
8
8
|
import { buildBrainDigest, buildBrainIndex } from './brain-core.mjs';
|
|
9
|
+
import { activeChangeLink } from './change-core.mjs';
|
|
9
10
|
import {
|
|
10
11
|
ensureDir,
|
|
11
12
|
findActiveSessionByTranscript,
|
|
@@ -1072,6 +1073,14 @@ function main() {
|
|
|
1072
1073
|
findLinkedDerivedNotes(vaultBase, sessionRel),
|
|
1073
1074
|
);
|
|
1074
1075
|
finalizeSessionFile(sessionPath, tx, created, endedAt);
|
|
1076
|
+
// Link bidirecional sessão↔change ativa no grafo Obsidian. Fail-quiet: nunca derruba o Stop.
|
|
1077
|
+
try {
|
|
1078
|
+
const chgLink = activeChangeLink(vaultBase);
|
|
1079
|
+
if (chgLink) {
|
|
1080
|
+
const cur = readFileSync(sessionPath, 'utf8');
|
|
1081
|
+
if (!cur.includes(chgLink)) writeFileSync(sessionPath, `${cur.trimEnd()}\n\n${chgLink}\n`, 'utf8');
|
|
1082
|
+
}
|
|
1083
|
+
} catch { /* nunca derruba o Stop */ }
|
|
1075
1084
|
writeControl(vaultBase, {
|
|
1076
1085
|
status: 'inactive',
|
|
1077
1086
|
session_file: '',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Automatically capture AI coding agent sessions (Claude Code, Codex) as local Markdown in your Obsidian vault — turn-by-turn history, cost/token tracking, auto-extracted decisions/bugs/learnings, rendered in the graph. Local-first.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/change.mjs
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// `wendkeep change <sub>` — native change lifecycle CLI (Pilar B).
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
newChange,
|
|
6
|
+
activeChange,
|
|
7
|
+
listChanges,
|
|
8
|
+
parseTasks,
|
|
9
|
+
archiveChange,
|
|
10
|
+
} from '../hooks/change-core.mjs';
|
|
11
|
+
import { evaluateGate, requiredSensors } from '../hooks/sensors-core.mjs';
|
|
12
|
+
import { getNextAdrNumber } from '../hooks/obsidian-common.mjs';
|
|
13
|
+
|
|
14
|
+
function resolveVault(argv) {
|
|
15
|
+
let vault;
|
|
16
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
17
|
+
const a = argv[i];
|
|
18
|
+
if (a === '--vault') vault = argv[++i];
|
|
19
|
+
else if (a.startsWith('--vault=')) vault = a.slice(8);
|
|
20
|
+
}
|
|
21
|
+
const base = vault || process.env.OBSIDIAN_VAULT_PATH;
|
|
22
|
+
if (!base) {
|
|
23
|
+
process.stderr.write('wendkeep change: no vault. Pass --vault <path> or set OBSIDIAN_VAULT_PATH.\n');
|
|
24
|
+
process.exit(2);
|
|
25
|
+
}
|
|
26
|
+
return isAbsolute(base) ? base : resolve(process.cwd(), base);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function today() {
|
|
30
|
+
const d = new Date();
|
|
31
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function runChange(argv) {
|
|
35
|
+
const [sub, ...rest] = argv;
|
|
36
|
+
const vaultBase = resolveVault(rest);
|
|
37
|
+
const slugArg = () => rest.find((a) => !a.startsWith('-'));
|
|
38
|
+
|
|
39
|
+
if (sub === 'new') {
|
|
40
|
+
const slug = slugArg();
|
|
41
|
+
if (!slug) { process.stderr.write('wendkeep change new: missing <slug>\n'); process.exit(2); }
|
|
42
|
+
const r = newChange(vaultBase, slug, { dateStr: today() });
|
|
43
|
+
process.stdout.write(`change ${r.created ? 'created' : 'exists'}: ${r.rel} (active)\n`);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (sub === 'list') {
|
|
48
|
+
const { active, archived } = listChanges(vaultBase);
|
|
49
|
+
const cur = activeChange(vaultBase);
|
|
50
|
+
process.stdout.write(`active: ${active.map((s) => (s === cur ? `*${s}` : s)).join(', ') || '(none)'}\n`);
|
|
51
|
+
process.stdout.write(`archived: ${archived.join(', ') || '(none)'}\n`);
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (sub === 'show') {
|
|
56
|
+
const slug = slugArg();
|
|
57
|
+
if (!slug) { process.stderr.write('wendkeep change show: missing <slug>\n'); process.exit(2); }
|
|
58
|
+
let md;
|
|
59
|
+
try { md = readFileSync(join(vaultBase, '08-Mudanças', slug, 'tarefas.md'), 'utf8'); }
|
|
60
|
+
catch { process.stderr.write(`wendkeep change show: not found: ${slug}\n`); process.exit(2); }
|
|
61
|
+
const tasks = parseTasks(md);
|
|
62
|
+
const open = tasks.filter((t) => !t.done).length;
|
|
63
|
+
process.stdout.write(`${slug}: ${tasks.length} task(s), ${open} open\n`);
|
|
64
|
+
for (const t of tasks) process.stdout.write(` [${t.done ? 'x' : ' '}] ${t.id} ${t.text}\n`);
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (sub === 'archive') {
|
|
69
|
+
const slug = slugArg() || activeChange(vaultBase);
|
|
70
|
+
if (!slug) { process.stderr.write('wendkeep change archive: missing <slug> and no active change\n'); process.exit(2); }
|
|
71
|
+
// Real gate (Pilar C): every sensor a task declared must be green in evidencia.json.
|
|
72
|
+
const gate = (dir) => {
|
|
73
|
+
let required = [];
|
|
74
|
+
try { required = requiredSensors(parseTasks(readFileSync(join(dir, 'tarefas.md'), 'utf8'))); } catch { /* no tasks */ }
|
|
75
|
+
let evidence = [];
|
|
76
|
+
try { evidence = JSON.parse(readFileSync(join(dir, 'evidencia.json'), 'utf8')); } catch { /* no evidence */ }
|
|
77
|
+
return evaluateGate(evidence, required);
|
|
78
|
+
};
|
|
79
|
+
const r = archiveChange(vaultBase, slug, { dateStr: today(), adrNum: getNextAdrNumber(vaultBase), gate });
|
|
80
|
+
if (!r.ok) {
|
|
81
|
+
process.stderr.write(`change archive BLOCKED (gate): failing sensors: ${r.failing.join(', ')} — run \`wendkeep verify\`.\n`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
process.stdout.write(`archived: ${r.archivedRel}; ADR: ${r.adrRel}\n`);
|
|
85
|
+
process.exit(0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
process.stderr.write(`wendkeep change: unknown subcommand "${sub}". Known: new, list, show, archive.\n`);
|
|
89
|
+
process.exit(2);
|
|
90
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// dotcontext .context/config seeding + MCP-placement resolution.
|
|
2
|
+
// Seeds `memory-validation` (`wendkeep validate-memory`, valid in any wendkeep
|
|
3
|
+
// project) plus a sensor for each detected package.json script (test/typecheck/
|
|
4
|
+
// lint/build). Project-specific commands beyond that are left to `context init`.
|
|
5
|
+
// Non-destructive. Also resolves whether to add the project MCP entry (skip it
|
|
6
|
+
// when dotcontext is already configured globally in ~/.claude.json).
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
// package.json script -> sensor (only seeded if the script exists in the project).
|
|
12
|
+
const SCRIPT_SENSORS = [
|
|
13
|
+
{ script: 'typecheck', id: 'typecheck', name: 'Typecheck', severity: 'critical' },
|
|
14
|
+
{ script: 'test', id: 'tests', name: 'Tests', severity: 'critical' },
|
|
15
|
+
{ script: 'lint', id: 'lint', name: 'Lint', severity: 'warning' },
|
|
16
|
+
{ script: 'build', id: 'build', name: 'Build', severity: 'warning' },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export function renderSensorsJson(scripts = {}) {
|
|
20
|
+
const sensors = [
|
|
21
|
+
{
|
|
22
|
+
id: 'memory-validation',
|
|
23
|
+
name: 'CORE validation',
|
|
24
|
+
description: 'valida .brain/CORE.md (cap 25 + 3 seções, sem segredos) — wendkeep',
|
|
25
|
+
severity: 'critical',
|
|
26
|
+
command: 'npx wendkeep validate-memory',
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
for (const m of SCRIPT_SENSORS) {
|
|
30
|
+
if (scripts && scripts[m.script]) {
|
|
31
|
+
sensors.push({
|
|
32
|
+
id: m.id,
|
|
33
|
+
name: m.name,
|
|
34
|
+
description: `npm run ${m.script}`,
|
|
35
|
+
severity: m.severity,
|
|
36
|
+
command: `npm run ${m.script}`,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return `${JSON.stringify({ version: 1, source: 'manual', sensors }, null, 2)}\n`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readProjectScripts(projectPath) {
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(readFileSync(join(projectPath, 'package.json'), 'utf8')).scripts || {};
|
|
46
|
+
} catch {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Create <project>/.context/config/sensors.json if absent (sensors auto-detected
|
|
52
|
+
// from the project's package.json scripts). Returns paths created.
|
|
53
|
+
export function seedDotcontext(projectPath) {
|
|
54
|
+
const created = [];
|
|
55
|
+
const configDir = join(projectPath, '.context', 'config');
|
|
56
|
+
mkdirSync(configDir, { recursive: true });
|
|
57
|
+
const sensorsPath = join(configDir, 'sensors.json');
|
|
58
|
+
if (!existsSync(sensorsPath)) {
|
|
59
|
+
writeFileSync(sensorsPath, renderSensorsJson(readProjectScripts(projectPath)), 'utf8');
|
|
60
|
+
created.push(sensorsPath);
|
|
61
|
+
}
|
|
62
|
+
return created;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// True if ~/.claude.json already declares a global dotcontext MCP server. Best-effort.
|
|
66
|
+
export function globalHasDotcontext(claudeJsonPath = join(homedir(), '.claude.json')) {
|
|
67
|
+
try {
|
|
68
|
+
const data = JSON.parse(readFileSync(claudeJsonPath, 'utf8'));
|
|
69
|
+
return !!data?.mcpServers?.dotcontext;
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Whether to SKIP the project-scoped dotcontext MCP entry.
|
|
76
|
+
// 'none' -> always skip
|
|
77
|
+
// 'project' -> never skip
|
|
78
|
+
// 'auto'/-- -> skip iff dotcontext is already global (avoid a duplicate server)
|
|
79
|
+
export function resolveDotcontextSkipMcp(flag, globalHas) {
|
|
80
|
+
if (flag === 'none') return true;
|
|
81
|
+
if (flag === 'project') return false;
|
|
82
|
+
return !!globalHas;
|
|
83
|
+
}
|
package/src/init.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
companionMcpPatch,
|
|
20
20
|
companionHookSpecs,
|
|
21
21
|
cavemanInstallCommand,
|
|
22
|
+
DOTCONTEXT_GITIGNORE,
|
|
22
23
|
} from './taxonomy.mjs';
|
|
23
24
|
import { renderVaultReadme } from './vault-readme.mjs';
|
|
24
25
|
import { canInteractiveSelect, selectCompanionsInteractive } from './companion-select.mjs';
|
|
@@ -31,6 +32,8 @@ import {
|
|
|
31
32
|
} from './vault-theme.mjs';
|
|
32
33
|
import { renderCoreSkeleton, renderCompactionProtocol } from './validate-core.mjs';
|
|
33
34
|
import { seedDefinitions } from './sync-defs.mjs';
|
|
35
|
+
import { seedWkSkills } from './skills-seed.mjs';
|
|
36
|
+
import { seedDotcontext, globalHasDotcontext, resolveDotcontextSkipMcp, renderSensorsJson } from './dotcontext-seed.mjs';
|
|
34
37
|
|
|
35
38
|
function parseArgs(argv) {
|
|
36
39
|
const args = { mcp: true, yes: false, force: false };
|
|
@@ -43,6 +46,10 @@ function parseArgs(argv) {
|
|
|
43
46
|
else if (a === '--force') args.force = true;
|
|
44
47
|
else if (a === '--no-companions') args.noCompanions = true;
|
|
45
48
|
else if (a === '--no-colors') args.noColors = true;
|
|
49
|
+
else if (a === '--dotcontext-mcp') args.dotcontextMcp = argv[++i];
|
|
50
|
+
else if (a.startsWith('--dotcontext-mcp=')) args.dotcontextMcp = a.slice(17);
|
|
51
|
+
else if (a === '--dotcontext-hooks') args.dotcontextHooks = argv[++i];
|
|
52
|
+
else if (a.startsWith('--dotcontext-hooks=')) args.dotcontextHooks = a.slice(19);
|
|
46
53
|
else if (a === '--companions') args.companions = argv[++i];
|
|
47
54
|
else if (a.startsWith('--companions=')) args.companions = a.slice(13);
|
|
48
55
|
else if (a.startsWith('--vault=')) args.vault = a.slice(8);
|
|
@@ -80,13 +87,19 @@ function backup(path) {
|
|
|
80
87
|
|
|
81
88
|
// --- merges -----------------------------------------------------------------
|
|
82
89
|
|
|
83
|
-
export function mergeSettings(existing, { vaultPath, withMcp, force, companions = [] }) {
|
|
90
|
+
export function mergeSettings(existing, { vaultPath, withMcp, force, companions = [], skipMcp = [], dotcontextHookLevel = 'full' }) {
|
|
84
91
|
const s = existing && typeof existing === 'object' ? { ...existing } : {};
|
|
85
92
|
s.hooks = { ...(s.hooks || {}) };
|
|
86
93
|
let added = 0;
|
|
87
|
-
// wendkeep's own session hooks + any companion-authored hooks
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
// wendkeep's own session hooks + any companion-authored hooks. Stable-sort by
|
|
95
|
+
// `order` (default 0) so higher-order companion hooks (e.g. dotcontext's order 100)
|
|
96
|
+
// fold AFTER wendkeep's own within each event. A spec may carry its own `command`
|
|
97
|
+
// (dotcontext dispatch) instead of a wendkeep hook `name`.
|
|
98
|
+
const allSpecs = [...SESSION_HOOKS, ...companionHookSpecs(companions, { dotcontextHookLevel })].sort(
|
|
99
|
+
(a, b) => (a.order ?? 0) - (b.order ?? 0),
|
|
100
|
+
);
|
|
101
|
+
for (const h of allSpecs) {
|
|
102
|
+
const command = h.command ?? hookCommand(h.name);
|
|
90
103
|
const groups = Array.isArray(s.hooks[h.event]) ? [...s.hooks[h.event]] : [];
|
|
91
104
|
const present = groups.some((g) => (g.hooks || []).some((x) => x.command === command));
|
|
92
105
|
if (present && !force) {
|
|
@@ -110,7 +123,7 @@ export function mergeSettings(existing, { vaultPath, withMcp, force, companions
|
|
|
110
123
|
s.enabledPlugins = { ...(s.enabledPlugins || {}), ...cp.enabledPlugins };
|
|
111
124
|
}
|
|
112
125
|
|
|
113
|
-
const companionMcp = Object.keys(companionMcpPatch(companions));
|
|
126
|
+
const companionMcp = Object.keys(companionMcpPatch(companions, skipMcp));
|
|
114
127
|
if (withMcp || companionMcp.length) {
|
|
115
128
|
const set = new Set(Array.isArray(s.enabledMcpjsonServers) ? s.enabledMcpjsonServers : []);
|
|
116
129
|
if (withMcp) set.add(MCP_SERVER_KEY);
|
|
@@ -120,11 +133,11 @@ export function mergeSettings(existing, { vaultPath, withMcp, force, companions
|
|
|
120
133
|
return { settings: s, added };
|
|
121
134
|
}
|
|
122
135
|
|
|
123
|
-
export function mergeMcp(existing, { vaultPath, withVault = true, companions = [] }) {
|
|
136
|
+
export function mergeMcp(existing, { vaultPath, withVault = true, companions = [], skipMcp = [] }) {
|
|
124
137
|
const m = existing && typeof existing === 'object' ? { ...existing } : {};
|
|
125
138
|
m.mcpServers = { ...(m.mcpServers || {}) };
|
|
126
139
|
if (withVault) m.mcpServers[MCP_SERVER_KEY] = mcpServerEntry(vaultPath);
|
|
127
|
-
Object.assign(m.mcpServers, companionMcpPatch(companions));
|
|
140
|
+
Object.assign(m.mcpServers, companionMcpPatch(companions, skipMcp));
|
|
128
141
|
return m;
|
|
129
142
|
}
|
|
130
143
|
|
|
@@ -229,6 +242,16 @@ export async function runInit(argv) {
|
|
|
229
242
|
log(` companions : ${companions.length ? companions.join(', ') : 'none'}`);
|
|
230
243
|
log(` colors : ${args.noColors ? 'skipped' : 'wendkeep-colors (snippet + graph groups)'}\n`);
|
|
231
244
|
|
|
245
|
+
// dotcontext controls (only relevant when selected): hook level + MCP placement.
|
|
246
|
+
// Skip the project MCP entry when dotcontext is already global (avoids a duplicate
|
|
247
|
+
// server / version clash); --dotcontext-mcp project|none and --dotcontext-hooks
|
|
248
|
+
// full|light|none override.
|
|
249
|
+
const dotcontextHookLevel = args.dotcontextHooks || 'full';
|
|
250
|
+
const skipMcp = [];
|
|
251
|
+
if (companions.includes('dotcontext') && resolveDotcontextSkipMcp(args.dotcontextMcp, globalHasDotcontext())) {
|
|
252
|
+
skipMcp.push('dotcontext');
|
|
253
|
+
}
|
|
254
|
+
|
|
232
255
|
// 1. Vault taxonomy ---------------------------------------------------------
|
|
233
256
|
if (!existsSync(vaultPath)) mkdirSync(vaultPath, { recursive: true });
|
|
234
257
|
let created = 0;
|
|
@@ -258,19 +281,32 @@ export async function runInit(argv) {
|
|
|
258
281
|
// Seed the definitions layer (.brain/agents + .brain/skills): versioned source of
|
|
259
282
|
// truth for custom agents/skills. `wendkeep sync-defs` copies them to the agent dirs.
|
|
260
283
|
seedDefinitions(brainDir);
|
|
261
|
-
|
|
284
|
+
seedWkSkills(brainDir); // Pilar A: native process skills (wk-workflow/tdd/debugging/...).
|
|
285
|
+
// Seed the change/spec layer starters (Pilar B) — non-destructive.
|
|
286
|
+
const specsReadme = join(vaultPath, '07-Specs', 'README.md');
|
|
287
|
+
if (!existsSync(specsReadme)) writeFileSync(specsReadme, '# Specs — contrato vivo\n\nCapacidades do projeto (requisitos/cenários). Changes em `08-Mudanças/` promovem deltas aqui no `wendkeep change archive`.\n', 'utf8');
|
|
288
|
+
const changeTpl = join(vaultPath, 'Templates', 'Change.md');
|
|
289
|
+
if (!existsSync(changeTpl)) writeFileSync(changeTpl, '---\ntype: change\nstatus: active\ncssclasses:\n - topic-change\n---\n\n# <slug>\n\n## Por quê\n\n## O que muda\n', 'utf8');
|
|
290
|
+
// Seed the native sensor config (Pilar C) at project root — non-destructive.
|
|
291
|
+
const sensorsFile = join(projectPath, 'wendkeep.sensors.json');
|
|
292
|
+
if (!existsSync(sensorsFile)) {
|
|
293
|
+
let scripts = {};
|
|
294
|
+
try { scripts = JSON.parse(readFileSync(join(projectPath, 'package.json'), 'utf8')).scripts || {}; } catch { /* no package.json */ }
|
|
295
|
+
writeFileSync(sensorsFile, renderSensorsJson(scripts), 'utf8');
|
|
296
|
+
}
|
|
297
|
+
log(` [1/4] vault taxonomy: ${VAULT_FOLDERS.length} folders (${created} created)${readmeNote}, .brain + change/spec + sensors seeded`);
|
|
262
298
|
|
|
263
299
|
// 2. .claude/settings.json --------------------------------------------------
|
|
264
300
|
const settingsPath = join(projectPath, '.claude', 'settings.json');
|
|
265
301
|
const settingsRead = readJsonSafe(settingsPath);
|
|
266
302
|
if (!settingsRead.ok) {
|
|
267
303
|
// Unparseable existing file — never clobber. Drop a .new for manual merge.
|
|
268
|
-
const fresh = mergeSettings(null, { vaultPath, withMcp: args.mcp, force: true, companions }).settings;
|
|
304
|
+
const fresh = mergeSettings(null, { vaultPath, withMcp: args.mcp, force: true, companions, skipMcp, dotcontextHookLevel }).settings;
|
|
269
305
|
writeJson(`${settingsPath}.new`, fresh);
|
|
270
306
|
log(` [2/4] settings.json exists but is not valid JSON -> wrote ${settingsPath}.new (merge by hand)`);
|
|
271
307
|
} else {
|
|
272
308
|
const hadFile = settingsRead.data !== null;
|
|
273
|
-
const { settings, added } = mergeSettings(settingsRead.data, { vaultPath, withMcp: args.mcp, force: args.force, companions });
|
|
309
|
+
const { settings, added } = mergeSettings(settingsRead.data, { vaultPath, withMcp: args.mcp, force: args.force, companions, skipMcp, dotcontextHookLevel });
|
|
274
310
|
if (hadFile) backup(settingsPath);
|
|
275
311
|
writeJson(settingsPath, settings);
|
|
276
312
|
log(` [2/4] settings.json ${hadFile ? 'merged' : 'created'} (${added} hook(s) wired, OBSIDIAN_VAULT_PATH set${hadFile ? ', .bak saved' : ''})`);
|
|
@@ -278,11 +314,11 @@ export async function runInit(argv) {
|
|
|
278
314
|
|
|
279
315
|
// 3. .mcp.json --------------------------------------------------------------
|
|
280
316
|
// Written when mcpvault is wanted OR a selected companion ships an MCP server.
|
|
281
|
-
const companionMcp = companionMcpPatch(companions);
|
|
317
|
+
const companionMcp = companionMcpPatch(companions, skipMcp);
|
|
282
318
|
if (args.mcp || Object.keys(companionMcp).length) {
|
|
283
319
|
const mcpPath = join(projectPath, '.mcp.json');
|
|
284
320
|
const mcpRead = readJsonSafe(mcpPath);
|
|
285
|
-
const mcpOpts = { vaultPath, withVault: args.mcp, companions };
|
|
321
|
+
const mcpOpts = { vaultPath, withVault: args.mcp, companions, skipMcp };
|
|
286
322
|
const names = [args.mcp && MCP_SERVER_KEY, ...Object.keys(companionMcp)].filter(Boolean).join(', ');
|
|
287
323
|
if (!mcpRead.ok) {
|
|
288
324
|
writeJson(`${mcpPath}.new`, mergeMcp(null, mcpOpts));
|
|
@@ -309,6 +345,15 @@ export async function runInit(argv) {
|
|
|
309
345
|
// plugin entry is already wired in settings.json regardless.
|
|
310
346
|
if (companions.includes('caveman')) runCavemanInstaller(log);
|
|
311
347
|
|
|
348
|
+
// dotcontext: MCP + lifecycle hooks are wired by mergeSettings/mergeMcp above;
|
|
349
|
+
// here we seed the versioned .context/config starter and print the gitignore note.
|
|
350
|
+
if (companions.includes('dotcontext')) {
|
|
351
|
+
const seeded = seedDotcontext(projectPath);
|
|
352
|
+
const mcpNote = skipMcp.includes('dotcontext') ? 'MCP global (project entry skipped)' : 'MCP (project)';
|
|
353
|
+
log(`\n dotcontext: ${mcpNote} + hooks(${dotcontextHookLevel}); .context/config seeded (${seeded.length} file(s)).`);
|
|
354
|
+
log(` [!] add to .gitignore (wendkeep não toca git): ${DOTCONTEXT_GITIGNORE.join(' ')}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
312
357
|
log('\nNext steps:');
|
|
313
358
|
log(` 1. Open the vault in Obsidian: "Open folder as vault" -> ${vaultPath}`);
|
|
314
359
|
log(' 2. Make sure wendkeep is installed where the agent runs (npm i -D wendkeep, or -g).');
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// src/skills-seed.mjs — native, zero-dep process skills (Pilar A: the HOW layer).
|
|
2
|
+
// Seeded into the vault's .brain/skills; distributed by `wendkeep sync-defs` to
|
|
3
|
+
// .claude/skills. Wendkeep-flavored (reference change/verify), concise native prose.
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
function skill(name, description, body) {
|
|
8
|
+
return { name, description, body: `---\nname: ${name}\ndescription: ${description}\n---\n${body}` };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const WORKFLOW = `# Loop a2 — o ciclo de trabalho do wendkeep
|
|
12
|
+
|
|
13
|
+
Use ao começar qualquer mudança não-trivial. O loop mantém memória (vault) e prova
|
|
14
|
+
(sensores) juntas, tudo linkado no grafo do Obsidian.
|
|
15
|
+
|
|
16
|
+
## Os passos
|
|
17
|
+
|
|
18
|
+
1. **Explore** — entenda o problema antes de propor. Leia o código/contexto relevante.
|
|
19
|
+
2. **Propose** — \`wendkeep change new <slug>\`. Isso cria \`08-Mudanças/<slug>/\` com:
|
|
20
|
+
- \`proposta.md\` — *por quê* e *o que muda* (o WHAT).
|
|
21
|
+
- \`design.md\` — a abordagem técnica.
|
|
22
|
+
- \`tarefas.md\` — a lista de tarefas \`- [ ] N.N descrição\`.
|
|
23
|
+
A mudança vira a *ativa* (ponteiro \`.brain/CURRENT_CHANGE.md\`) e é injetada no
|
|
24
|
+
próximo SessionStart, então você retoma o trabalho em curso automaticamente.
|
|
25
|
+
3. **Apply** — implemente cada tarefa de \`tarefas.md\` com disciplina **wk-tdd**
|
|
26
|
+
(teste vermelho antes do código). Marque \`- [x]\` ao concluir cada uma.
|
|
27
|
+
Em tarefas cujo resultado precisa de *prova*, declare o sensor no fim da linha:
|
|
28
|
+
\`- [ ] 2.1 valida CORE [sensor:memory-validation]\`.
|
|
29
|
+
4. **Verify** — \`wendkeep verify\`. Roda os sensores declarados pelas tarefas e grava
|
|
30
|
+
\`evidencia.json\`. Verde = prova registrada.
|
|
31
|
+
5. **Archive** — \`wendkeep change archive <slug>\`. O *gate* trava se algum sensor
|
|
32
|
+
declarado não estiver verde. Passando, move a mudança pro \`_arquivo\` e gera um ADR
|
|
33
|
+
em \`04-Decisões/\`.
|
|
34
|
+
|
|
35
|
+
## Regras
|
|
36
|
+
|
|
37
|
+
- Uma mudança ativa por vez. Termine (archive) antes de abrir outra.
|
|
38
|
+
- Se uma tarefa não precisa de prova automatizada, não declare sensor — o gate só exige
|
|
39
|
+
o que você declarou. Sem \`[sensor:]\`, o archive não trava.
|
|
40
|
+
- A proposta linka a sessão de origem; a sessão linka a mudança ativa. É de propósito:
|
|
41
|
+
o grafo do Obsidian mostra plano↔sessão↔decisão.
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
const TDD = `# TDD — Red, Green, Refactor
|
|
45
|
+
|
|
46
|
+
Use ao implementar qualquer comportamento. Nunca escreva código de produção sem um
|
|
47
|
+
teste vermelho pedindo por ele.
|
|
48
|
+
|
|
49
|
+
## O ciclo
|
|
50
|
+
|
|
51
|
+
1. **Red** — escreva o menor teste que falha por faltar o comportamento desejado.
|
|
52
|
+
2. **Rode e veja falhar** — confirme que falha *pelo motivo certo* (não por erro de
|
|
53
|
+
import/typo). Um teste que nunca falhou não prova nada.
|
|
54
|
+
3. **Green** — o código mínimo que faz o teste passar. Nada além disso.
|
|
55
|
+
4. **Refactor** — limpe com os testes verdes te protegendo. Rode de novo.
|
|
56
|
+
|
|
57
|
+
## Regras
|
|
58
|
+
|
|
59
|
+
- Um comportamento por teste. Nome do teste descreve o comportamento, não a implementação.
|
|
60
|
+
- Passos pequenos: um ciclo red→green leva minutos, não horas.
|
|
61
|
+
- Não teste detalhe interno que você vai refatorar — teste a interface observável.
|
|
62
|
+
- Bug encontrado = primeiro um teste que o reproduz (vermelho), depois a correção (verde).
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
const DEBUGGING = `# Depuração sistemática
|
|
66
|
+
|
|
67
|
+
Use quando algo falha, quebra ou dá resultado errado. Uma hipótese por vez — não saia
|
|
68
|
+
mudando coisas no escuro.
|
|
69
|
+
|
|
70
|
+
## O método
|
|
71
|
+
|
|
72
|
+
1. **Reproduza** — um caso mínimo e determinístico que dispara a falha. Sem repro
|
|
73
|
+
confiável, você não sabe se corrigiu.
|
|
74
|
+
2. **Uma hipótese** — a causa mais provável, específica o suficiente pra testar.
|
|
75
|
+
3. **Isole** — confirme ou descarte a hipótese ANTES de corrigir: bisect, log no ponto
|
|
76
|
+
suspeito, comente metade. Prove onde está, não onde você acha que está.
|
|
77
|
+
4. **Corrija a raiz** — o defeito real, não o sintoma. Se só some o sintoma, você não
|
|
78
|
+
entendeu a causa.
|
|
79
|
+
5. **Verifique** — o repro do passo 1 sumiu E a suíte segue verde. Sem regressão.
|
|
80
|
+
|
|
81
|
+
## Regras
|
|
82
|
+
|
|
83
|
+
- Mudou várias coisas de uma vez e "consertou"? Você não sabe o que consertou. Reverta,
|
|
84
|
+
isole uma variável por vez.
|
|
85
|
+
- Leia a mensagem de erro inteira e a stack — a linha decisiva costuma estar ali.
|
|
86
|
+
- "Não faz sentido" = uma suposição sua está errada. Cheque as suposições, não o improvável.
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
const BRAINSTORMING = `# Brainstorming — ideia vira design
|
|
90
|
+
|
|
91
|
+
Use quando a ideia ainda é vaga. Transforme-a num design aprovado ANTES de escrever
|
|
92
|
+
qualquer código.
|
|
93
|
+
|
|
94
|
+
## Como
|
|
95
|
+
|
|
96
|
+
1. **Explore o contexto** — arquivos, docs, histórico relevante.
|
|
97
|
+
2. **Pergunte uma coisa por vez** — propósito, restrições, critério de sucesso. Prefira
|
|
98
|
+
múltipla escolha. Não despeje dez perguntas juntas.
|
|
99
|
+
3. **Proponha 2–3 abordagens** — com trade-offs e sua recomendação primeiro.
|
|
100
|
+
4. **Apresente o design em seções** — escale ao tamanho do problema; confirme cada seção.
|
|
101
|
+
|
|
102
|
+
## Gate rígido
|
|
103
|
+
|
|
104
|
+
NÃO escreva código, scaffold nem tome ação de implementação até apresentar um design e o
|
|
105
|
+
usuário aprovar. Vale pra TODO projeto, por mais simples que pareça — "simples demais pra
|
|
106
|
+
precisar de design" é onde suposições não-checadas mais custam. O design pode ser curto,
|
|
107
|
+
mas tem que existir e ser aprovado.
|
|
108
|
+
|
|
109
|
+
Ao aprovar, o próximo passo é **wk-planning** (design → plano). Não pule pra implementação.
|
|
110
|
+
`;
|
|
111
|
+
|
|
112
|
+
const PLANNING = `# Planejamento — design vira plano de tarefas
|
|
113
|
+
|
|
114
|
+
Use depois de um design aprovado. Produza um plano que um dev sem contexto do projeto
|
|
115
|
+
consiga executar.
|
|
116
|
+
|
|
117
|
+
## Antes das tarefas
|
|
118
|
+
|
|
119
|
+
Mapeie os arquivos: quais criar/modificar e a responsabilidade de cada um. Arquivos que
|
|
120
|
+
mudam juntos ficam juntos; um arquivo, uma responsabilidade. É aqui que a decomposição
|
|
121
|
+
trava.
|
|
122
|
+
|
|
123
|
+
## Tarefas bite-sized (TDD)
|
|
124
|
+
|
|
125
|
+
Cada tarefa termina num entregável testável de forma independente. Cada passo é uma ação
|
|
126
|
+
de 2–5 min:
|
|
127
|
+
|
|
128
|
+
- Escreva o teste que falha (mostre o código do teste).
|
|
129
|
+
- Rode e veja falhar (comando exato + saída esperada).
|
|
130
|
+
- Implementação mínima (mostre o código).
|
|
131
|
+
- Rode e veja passar.
|
|
132
|
+
- Checkpoint: suíte verde.
|
|
133
|
+
|
|
134
|
+
## Regras
|
|
135
|
+
|
|
136
|
+
- Caminhos de arquivo exatos, sempre. Código real em cada passo — nada de "TODO",
|
|
137
|
+
"tratar erros apropriadamente", "similar à Task N".
|
|
138
|
+
- DRY, YAGNI. Corte features que o design não pediu.
|
|
139
|
+
- Nomes/assinaturas consistentes entre tarefas (uma função é \`x()\` em toda parte).
|
|
140
|
+
`;
|
|
141
|
+
|
|
142
|
+
export const WK_SKILLS = [
|
|
143
|
+
skill('wk-workflow', 'Use ao começar qualquer mudança não-trivial — orquestra o loop a2 (explore, propose, apply, verify, archive) nos comandos wendkeep.', WORKFLOW),
|
|
144
|
+
skill('wk-tdd', 'Use ao implementar qualquer comportamento — disciplina Red, Green, Refactor.', TDD),
|
|
145
|
+
skill('wk-debugging', 'Use quando algo falha ou quebra — depuração sistemática por hipótese antes de corrigir.', DEBUGGING),
|
|
146
|
+
skill('wk-brainstorming', 'Use quando a ideia ainda é vaga — transforma ideia em design aprovado antes de código.', BRAINSTORMING),
|
|
147
|
+
skill('wk-planning', 'Use após um design aprovado — decompõe em plano de tarefas TDD bite-sized.', PLANNING),
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
// Seed each skill into <brainDir>/skills/<name>/SKILL.md if absent (non-destructive).
|
|
151
|
+
export function seedWkSkills(brainDir) {
|
|
152
|
+
const created = [];
|
|
153
|
+
for (const s of WK_SKILLS) {
|
|
154
|
+
const dir = join(brainDir, 'skills', s.name);
|
|
155
|
+
mkdirSync(dir, { recursive: true });
|
|
156
|
+
const f = join(dir, 'SKILL.md');
|
|
157
|
+
if (!existsSync(f)) {
|
|
158
|
+
writeFileSync(f, s.body, 'utf8');
|
|
159
|
+
created.push(f);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return created;
|
|
163
|
+
}
|
package/src/taxonomy.mjs
CHANGED
|
@@ -15,6 +15,8 @@ export const VAULT_FOLDERS = [
|
|
|
15
15
|
'04-Decisões',
|
|
16
16
|
'05-Bugs',
|
|
17
17
|
'06-Aprendizados',
|
|
18
|
+
'07-Specs',
|
|
19
|
+
'08-Mudanças',
|
|
18
20
|
'Templates',
|
|
19
21
|
'.brain',
|
|
20
22
|
];
|
|
@@ -30,6 +32,8 @@ export const HOOK_FILES = [
|
|
|
30
32
|
'token-usage.mjs',
|
|
31
33
|
'pricing.json',
|
|
32
34
|
'brain-core.mjs',
|
|
35
|
+
'change-core.mjs',
|
|
36
|
+
'sensors-core.mjs',
|
|
33
37
|
'brain-inject.mjs',
|
|
34
38
|
'brain-recall.mjs',
|
|
35
39
|
'brain-reindex.mjs',
|
|
@@ -113,6 +117,17 @@ export const COMPANIONS = [
|
|
|
113
117
|
// No MCP, no agnostic hook: on non-Claude agents, run its own installer
|
|
114
118
|
// (see cavemanInstallCommand — npx, non-interactive, Gemini excluded).
|
|
115
119
|
},
|
|
120
|
+
{
|
|
121
|
+
id: 'dotcontext',
|
|
122
|
+
label: 'dotcontext — orquestração de execução (PREVC + sensores + gate E→V)',
|
|
123
|
+
default: true,
|
|
124
|
+
// MCP-only (no Claude Code plugin). Agent-agnostic server, @latest surface.
|
|
125
|
+
mcp: { key: 'dotcontext', entry: { type: 'stdio', command: 'npx', args: ['-y', '@dotcontext/mcp@latest'], env: {} } },
|
|
126
|
+
// Lifecycle hooks via the pinned CLI; wired by wendkeep (single writer).
|
|
127
|
+
dotcontextHooks: { cliVersion: '1.1.1', source: 'claude-code' },
|
|
128
|
+
// Seed a starter .context/config (one neutral sensor).
|
|
129
|
+
contextSeed: true,
|
|
130
|
+
},
|
|
116
131
|
];
|
|
117
132
|
|
|
118
133
|
const COMPANION_BY_ID = Object.fromEntries(COMPANIONS.map((c) => [c.id, c]));
|
|
@@ -139,6 +154,7 @@ export function companionSettingsPatch(ids) {
|
|
|
139
154
|
for (const id of ids) {
|
|
140
155
|
const c = COMPANION_BY_ID[id];
|
|
141
156
|
if (!c) continue;
|
|
157
|
+
if (!c.marketplace || !c.plugin) continue; // MCP-only companions (e.g. dotcontext) have no plugin layer
|
|
142
158
|
extraKnownMarketplaces[c.id] = { source: c.marketplace };
|
|
143
159
|
enabledPlugins[`${c.plugin}@${c.id}`] = true;
|
|
144
160
|
}
|
|
@@ -146,9 +162,13 @@ export function companionSettingsPatch(ids) {
|
|
|
146
162
|
}
|
|
147
163
|
|
|
148
164
|
// The .mcp.json fragment (agent-agnostic layer) for MCP-capable companions only.
|
|
149
|
-
|
|
165
|
+
// `skip` omits ids whose MCP is already configured elsewhere (e.g. dotcontext set
|
|
166
|
+
// globally in ~/.claude.json — avoids a duplicate project-scoped server).
|
|
167
|
+
export function companionMcpPatch(ids, skip = []) {
|
|
168
|
+
const skipSet = new Set(skip);
|
|
150
169
|
const servers = {};
|
|
151
170
|
for (const id of ids) {
|
|
171
|
+
if (skipSet.has(id)) continue;
|
|
152
172
|
const c = COMPANION_BY_ID[id];
|
|
153
173
|
if (c?.mcp) servers[c.mcp.key] = c.mcp.entry;
|
|
154
174
|
}
|
|
@@ -158,7 +178,10 @@ export function companionMcpPatch(ids) {
|
|
|
158
178
|
// SessionStart hook specs wendkeep must author for companions that lack a native
|
|
159
179
|
// one (only Understand-Anything's domain-graph injector today). Same shape as
|
|
160
180
|
// SESSION_HOOKS so the same merge logic wires them.
|
|
161
|
-
|
|
181
|
+
// `dotcontextHookLevel`: 'full' (SessionStart+Stop+PostToolUse), 'light' (drops the
|
|
182
|
+
// per-tool PostToolUse trace hook — cuts latency when other PostToolUse hooks exist),
|
|
183
|
+
// or 'none' (MCP only, no lifecycle hooks).
|
|
184
|
+
export function companionHookSpecs(ids, { dotcontextHookLevel = 'full' } = {}) {
|
|
162
185
|
const specs = [];
|
|
163
186
|
for (const id of ids) {
|
|
164
187
|
const c = COMPANION_BY_ID[id];
|
|
@@ -168,13 +191,42 @@ export function companionHookSpecs(ids) {
|
|
|
168
191
|
matcher: null,
|
|
169
192
|
name: c.wendkeepHook,
|
|
170
193
|
timeout: 15,
|
|
194
|
+
order: 0,
|
|
171
195
|
statusMessage: `wendkeep: ${c.id} domain graph`,
|
|
172
196
|
});
|
|
173
197
|
}
|
|
198
|
+
// dotcontext: lifecycle hooks (SessionStart last + Stop + PostToolUse) that
|
|
199
|
+
// dispatch to its pinned CLI. They carry an explicit `command` (not a wendkeep
|
|
200
|
+
// hook name) and order 100 so they fold AFTER wendkeep's own session hooks.
|
|
201
|
+
if (c?.dotcontextHooks && dotcontextHookLevel !== 'none') {
|
|
202
|
+
const command = dotcontextHookCommand(c.dotcontextHooks.cliVersion, c.dotcontextHooks.source);
|
|
203
|
+
const base = { command, timeout: 8, order: 100, statusMessage: `${c.id}: harness hook` };
|
|
204
|
+
specs.push({ event: 'SessionStart', matcher: null, ...base });
|
|
205
|
+
specs.push({ event: 'Stop', matcher: null, ...base });
|
|
206
|
+
if (dotcontextHookLevel === 'full') {
|
|
207
|
+
specs.push({ event: 'PostToolUse', matcher: 'Write|Edit|Bash', ...base });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
174
210
|
}
|
|
175
211
|
return specs;
|
|
176
212
|
}
|
|
177
213
|
|
|
214
|
+
// The pinned dotcontext CLI hook-dispatch command (runs on each lifecycle event).
|
|
215
|
+
export function dotcontextHookCommand(cliVersion, source) {
|
|
216
|
+
return `npx -y @dotcontext/cli@${cliVersion} hook dispatch --source ${source}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// .context/ subpaths that should be gitignored (runtime/cache/regenerable). wendkeep
|
|
220
|
+
// does not touch git — init prints these as a note for the user to add.
|
|
221
|
+
export const DOTCONTEXT_GITIGNORE = [
|
|
222
|
+
'.context/runtime/',
|
|
223
|
+
'.context/cache/',
|
|
224
|
+
'.context/logs/',
|
|
225
|
+
'.context/plans/',
|
|
226
|
+
'.context/agents/',
|
|
227
|
+
'.context/skills/',
|
|
228
|
+
];
|
|
229
|
+
|
|
178
230
|
// Agents wendkeep installs caveman into via its cross-agent installer. Gemini is
|
|
179
231
|
// excluded on purpose: its CLI rejects caveman's agent tool names and crashes
|
|
180
232
|
// (libuv assertion) mid-install. caveman has no --exclude flag — only an --only
|
package/src/vault-theme.mjs
CHANGED
|
@@ -29,6 +29,8 @@ const FOLDER_PALETTE = [
|
|
|
29
29
|
['04-Decisões', 'violet'],
|
|
30
30
|
['05-Bugs', 'red'],
|
|
31
31
|
['06-Aprendizados', 'green'],
|
|
32
|
+
['07-Specs', 'teal'],
|
|
33
|
+
['08-Mudanças', 'amber'],
|
|
32
34
|
['Templates', 'slate'],
|
|
33
35
|
];
|
|
34
36
|
|
|
@@ -38,6 +40,8 @@ export const NOTE_COLORS = {
|
|
|
38
40
|
decision: { cssClass: 'topic-decision', folder: '04-Decisões', palette: 'violet' },
|
|
39
41
|
bug: { cssClass: 'topic-bug', folder: '05-Bugs', palette: 'red' },
|
|
40
42
|
learning: { cssClass: 'topic-learning', folder: '06-Aprendizados', palette: 'green' },
|
|
43
|
+
spec: { cssClass: 'topic-spec', folder: '07-Specs', palette: 'teal' },
|
|
44
|
+
change: { cssClass: 'topic-change', folder: '08-Mudanças', palette: 'amber' },
|
|
41
45
|
};
|
|
42
46
|
|
|
43
47
|
// cssclasses that get the note-accent treatment (the four note types + a home note).
|
package/src/verify.mjs
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// `wendkeep verify [--change <slug>]` — run a change's task sensors, record evidence.
|
|
2
|
+
// Sensors run at the PROJECT root (--project or cwd); the change + evidence live in
|
|
3
|
+
// the VAULT. Writes 08-Mudanças/<slug>/evidencia.json; exit 1 if a critical sensor is red.
|
|
4
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
6
|
+
import { parseTasks, activeChange } from '../hooks/change-core.mjs';
|
|
7
|
+
import { loadSensors, requiredSensors, runSensors, evaluateGate } from '../hooks/sensors-core.mjs';
|
|
8
|
+
|
|
9
|
+
function opt(argv, name) {
|
|
10
|
+
const i = argv.indexOf(name);
|
|
11
|
+
if (i >= 0) return argv[i + 1];
|
|
12
|
+
const eq = argv.find((a) => a.startsWith(`${name}=`));
|
|
13
|
+
return eq ? eq.slice(name.length + 1) : undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function runVerify(argv) {
|
|
17
|
+
const vaultRaw = opt(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
|
|
18
|
+
if (!vaultRaw) { process.stderr.write('wendkeep verify: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
|
|
19
|
+
const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
|
|
20
|
+
const projectRoot = resolve(opt(argv, '--project') || process.cwd());
|
|
21
|
+
const slug = opt(argv, '--change') || activeChange(vaultBase);
|
|
22
|
+
if (!slug) { process.stderr.write('wendkeep verify: no change (--change or active).\n'); process.exit(2); }
|
|
23
|
+
|
|
24
|
+
const changeDir = join(vaultBase, '08-Mudanças', slug);
|
|
25
|
+
let tarefas = '';
|
|
26
|
+
try { tarefas = readFileSync(join(changeDir, 'tarefas.md'), 'utf8'); }
|
|
27
|
+
catch { process.stderr.write(`wendkeep verify: change not found: ${slug}\n`); process.exit(2); }
|
|
28
|
+
|
|
29
|
+
const ids = requiredSensors(parseTasks(tarefas));
|
|
30
|
+
const sensors = loadSensors(projectRoot);
|
|
31
|
+
const evidence = runSensors(sensors, ids, { cwd: projectRoot });
|
|
32
|
+
writeFileSync(join(changeDir, 'evidencia.json'), `${JSON.stringify(evidence, null, 2)}\n`, 'utf8');
|
|
33
|
+
|
|
34
|
+
const severity = Object.fromEntries(sensors.map((s) => [s.id, s.severity || 'critical']));
|
|
35
|
+
const critical = ids.filter((id) => severity[id] !== 'warning');
|
|
36
|
+
const { ok, failing } = evaluateGate(evidence, critical);
|
|
37
|
+
for (const e of evidence) process.stdout.write(` ${e.status === 'green' ? '✓' : '✗'} ${e.id}\n`);
|
|
38
|
+
if (!ok) { process.stderr.write(`verify: critical sensors red: ${failing.join(', ')}\n`); process.exit(1); }
|
|
39
|
+
process.stdout.write(`verify OK (${ids.length} sensor(s))\n`);
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|