wendkeep 0.32.0 → 0.34.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/src/spec.mjs CHANGED
@@ -1,7 +1,15 @@
1
1
  // `wendkeep spec <sub>` — read-only views over the living specs in 07-Specs (0.7.0).
2
2
  import { readFileSync, readdirSync } from 'node:fs';
3
3
  import { isAbsolute, join, resolve } from 'node:path';
4
- import { parseRequirements } from '../hooks/spec-core.mjs';
4
+ import {
5
+ adoptSpecsState,
6
+ buildEffectiveRequirementPackage,
7
+ captureSpecBaseline,
8
+ discoverSpecDeltas,
9
+ parseRequirements,
10
+ specConflicts,
11
+ } from '../hooks/spec-core.mjs';
12
+ import { activeChange, parseTasks } from '../hooks/change-core.mjs';
5
13
  import { getLocale } from '../hooks/locale.mjs';
6
14
 
7
15
  function resolveVault(argv) {
@@ -24,6 +32,64 @@ export function runSpec(argv) {
24
32
  const vaultBase = resolveVault(rest);
25
33
  const specsDir = join(vaultBase, getLocale(vaultBase).folders.specs);
26
34
 
35
+ const option = (name) => {
36
+ const index = rest.indexOf(name);
37
+ if (index >= 0) return rest[index + 1];
38
+ const entry = rest.find((a) => a.startsWith(`${name}=`));
39
+ return entry ? entry.slice(name.length + 1) : undefined;
40
+ };
41
+
42
+ if (sub === 'effective') {
43
+ const slug = option('--change') || activeChange(vaultBase);
44
+ if (!slug) { process.stderr.write('wendkeep spec effective: no change (--change or current)\n'); process.exit(2); }
45
+ const changeDir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
46
+ let tasks = [];
47
+ try {
48
+ readFileSync(join(changeDir, 'proposta.md'), 'utf8');
49
+ tasks = parseTasks(readFileSync(join(changeDir, 'tarefas.md'), 'utf8'));
50
+ }
51
+ catch { process.stderr.write(`wendkeep spec effective: change not found: ${slug}\n`); process.exit(2); }
52
+ const reqIds = [...new Set(tasks.map((task) => task.req).filter(Boolean))];
53
+ const effective = buildEffectiveRequirementPackage(vaultBase, changeDir, reqIds);
54
+ if (effective.errors.length) {
55
+ process.stderr.write(`wendkeep spec effective: invalid delta: ${effective.errors.join('; ')}\n`);
56
+ process.exit(1);
57
+ }
58
+ if (rest.includes('--json')) {
59
+ process.stdout.write(`${JSON.stringify({ slug, effectiveSpecHash: effective.hash, specs: effective.specs }, null, 2)}\n`);
60
+ } else {
61
+ process.stdout.write(`change: ${slug}\neffective-spec-hash: ${effective.hash}\n`);
62
+ for (const spec of effective.specs) {
63
+ process.stdout.write(`spec: ${spec.capability}\n`);
64
+ for (const req of spec.requirements) process.stdout.write(` ${req.operation === 'BASE' ? '=' : req.operation === 'ADDED' ? '+' : '~'} ${req.id || req.name} [${req.source}]\n`);
65
+ }
66
+ }
67
+ process.exit(0);
68
+ }
69
+
70
+ if (sub === 'migrate') {
71
+ const state = adoptSpecsState(vaultBase);
72
+ process.stdout.write(`spec state adopted: ${Object.keys(state.specs).length} living spec(s); 07-Specs is generated/read-only\n`);
73
+ process.exit(0);
74
+ }
75
+
76
+ if (sub === 'rebase') {
77
+ const slug = option('--change') || activeChange(vaultBase);
78
+ if (!slug) { process.stderr.write('wendkeep spec rebase: no change (--change or current)\n'); process.exit(2); }
79
+ const changeDir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
80
+ try { readFileSync(join(changeDir, 'proposta.md'), 'utf8'); }
81
+ catch { process.stderr.write(`wendkeep spec rebase: change not found: ${slug}\n`); process.exit(2); }
82
+ const capabilities = discoverSpecDeltas(changeDir);
83
+ const conflicts = specConflicts(vaultBase, changeDir, capabilities);
84
+ if (conflicts.length && !rest.includes('--accept-current')) {
85
+ process.stderr.write(`wendkeep spec rebase: conflicts: ${conflicts.join('; ')} — reconcile delta, then rerun with --accept-current\n`);
86
+ process.exit(1);
87
+ }
88
+ captureSpecBaseline(vaultBase, changeDir, { refresh: true });
89
+ process.stdout.write(`spec baseline rebased: ${slug}${conflicts.length ? ` (${conflicts.length} conflict(s) accepted)` : ''}\n`);
90
+ process.exit(0);
91
+ }
92
+
27
93
  if (sub === 'list') {
28
94
  let files = [];
29
95
  try { files = readdirSync(specsDir).filter((f) => f.endsWith('.md') && f !== 'README.md'); } catch { /* sem specs */ }
@@ -49,6 +115,6 @@ export function runSpec(argv) {
49
115
  process.exit(0);
50
116
  }
51
117
 
52
- process.stderr.write(`wendkeep spec: unknown subcommand "${sub}". Known: list, show.\n`);
118
+ process.stderr.write(`wendkeep spec: unknown subcommand "${sub}". Known: list, show, effective, migrate, rebase.\n`);
53
119
  process.exit(2);
54
120
  }
package/src/sync-defs.mjs CHANGED
@@ -2,11 +2,13 @@
2
2
  // they travel with the project in git. They have no automatic consumer — agents
3
3
  // read them from their own dirs — so `wendkeep sync-defs` copies them there:
4
4
  // .brain/agents/*.toml -> <project>/.codex/agents/ (Codex agent format)
5
- // .brain/skills/<name>/ -> <project>/.claude/skills/ (skill format)
5
+ // .brain/skills/<name>/ -> <project>/.claude/skills/ + .agents/skills/ (skill format)
6
6
  // .brain is the source of truth; re-run sync after editing. Copy (not symlink) for
7
7
  // cross-platform robustness.
8
- import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
9
- import { isAbsolute, join, resolve } from 'node:path';
8
+ import { createHash } from 'node:crypto';
9
+ import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
10
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
10
12
  import { seedWkSkills } from './skills-seed.mjs';
11
13
  import { getLocale } from '../hooks/locale.mjs';
12
14
 
@@ -15,6 +17,27 @@ import { getLocale } from '../hooks/locale.mjs';
15
17
  // the markers is ours; user content around it is never touched.
16
18
  const AG_START = '<!-- wendkeep:skills:start -->';
17
19
  const AG_END = '<!-- wendkeep:skills:end -->';
20
+ const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
21
+ const WENDKEEP_VERSION = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8')).version;
22
+ const META_FILE = '.wendkeep-meta.json';
23
+
24
+ function directoryHash(root) {
25
+ const hash = createHash('sha256');
26
+ const visit = (dir) => {
27
+ let names = [];
28
+ try { names = readdirSync(dir).sort(); } catch { return; }
29
+ for (const name of names) {
30
+ if (name === META_FILE) continue;
31
+ const path = join(dir, name);
32
+ const rel = relative(root, path).replaceAll('\\', '/');
33
+ const stat = statSync(path);
34
+ if (stat.isDirectory()) visit(path);
35
+ else { hash.update(rel); hash.update('\0'); hash.update(readFileSync(path)); hash.update('\0'); }
36
+ }
37
+ };
38
+ visit(root);
39
+ return hash.digest('hex');
40
+ }
18
41
 
19
42
  function skillInventory(skillsSrc) {
20
43
  const out = [];
@@ -31,9 +54,10 @@ function skillInventory(skillsSrc) {
31
54
  return out;
32
55
  }
33
56
 
34
- function renderAgentsSection(skills) {
57
+ function renderAgentsSection(skills, sourceHash = '') {
35
58
  const list = skills.map((s) => `- **${s.name}** — ${s.description}`).join('\n');
36
59
  return `${AG_START}
60
+ <!-- wendkeep-version: ${WENDKEEP_VERSION}; skills-sha256: ${sourceHash} -->
37
61
  ## wendkeep — process skills & loop
38
62
 
39
63
  This project uses the [wendkeep](https://github.com/rogersialves/wendkeep) harness. Work
@@ -41,9 +65,10 @@ through its change loop: \`wendkeep change new <slug>\` → implement tasks test
41
65
  (tag proof \`[sensor:id]\` and requirement \`[req:ID]\`) → \`wendkeep verify\` →
42
66
  \`wendkeep verify --deep\` + an independent read-only verification pass writing
43
67
  \`verdict.json\` → \`wendkeep change archive\` (gated). Inspect with \`wendkeep change
44
- status\` / \`spec list\` / \`sensors list\`.
68
+ status\` / \`spec effective --change <slug>\` / \`sensors list\`. Author specs only in
69
+ \`08-Mudanças/<slug>/specs/\`; \`07-Specs\` is generated and must not be edited directly.
45
70
 
46
- Process skills (full text in \`.claude/skills/\` and the vault's \`.brain/skills/\`):
71
+ Process skills (full text in \`.claude/skills/\`, \`.agents/skills/\`, and the vault's \`.brain/skills/\`):
47
72
  ${list}
48
73
  ${AG_END}`;
49
74
  }
@@ -52,7 +77,7 @@ function upsertAgentsMd(projectPath, skillsSrc) {
52
77
  const skills = skillInventory(skillsSrc);
53
78
  if (!skills.length) return false;
54
79
  const path = join(projectPath, 'AGENTS.md');
55
- const section = renderAgentsSection(skills);
80
+ const section = renderAgentsSection(skills, directoryHash(skillsSrc));
56
81
  let content = '';
57
82
  try { content = readFileSync(path, 'utf8'); } catch { /* novo */ }
58
83
  if (content.includes(AG_START) && content.includes(AG_END)) {
@@ -67,7 +92,7 @@ function upsertAgentsMd(projectPath, skillsSrc) {
67
92
  }
68
93
 
69
94
  export function syncDefs(vaultBase, projectPath) {
70
- const out = { agents: [], skills: [], agentsMd: false };
95
+ const out = { agents: [], skills: [], codexSkills: [], agentsMd: false };
71
96
 
72
97
  const agentsSrc = join(vaultBase, '.brain', 'agents');
73
98
  if (existsSync(agentsSrc)) {
@@ -85,10 +110,19 @@ export function syncDefs(vaultBase, projectPath) {
85
110
  for (const name of readdirSync(skillsSrc)) {
86
111
  const dir = join(skillsSrc, name);
87
112
  if (!statSync(dir).isDirectory()) continue; // skip skills/README.md
88
- const dest = join(projectPath, '.claude', 'skills', name);
89
- mkdirSync(dest, { recursive: true });
90
- cpSync(dir, dest, { recursive: true });
113
+ const destinations = [
114
+ join(projectPath, '.claude', 'skills', name),
115
+ join(projectPath, '.agents', 'skills', name),
116
+ ];
117
+ const sourceHash = directoryHash(dir);
118
+ for (const dest of destinations) {
119
+ rmSync(dest, { recursive: true, force: true });
120
+ mkdirSync(dest, { recursive: true });
121
+ cpSync(dir, dest, { recursive: true });
122
+ writeFileSync(join(dest, META_FILE), `${JSON.stringify({ wendkeepVersion: WENDKEEP_VERSION, sourceHash }, null, 2)}\n`, 'utf8');
123
+ }
91
124
  out.skills.push(name);
125
+ out.codexSkills.push(name);
92
126
  }
93
127
  }
94
128
 
@@ -98,6 +132,33 @@ export function syncDefs(vaultBase, projectPath) {
98
132
  return out;
99
133
  }
100
134
 
135
+ export function checkSyncDefs(vaultBase, projectPath) {
136
+ const issues = [];
137
+ const skillsSrc = join(vaultBase, '.brain', 'skills');
138
+ if (!existsSync(skillsSrc)) return { ok: false, issues: ['fonte .brain/skills ausente'] };
139
+ const names = readdirSync(skillsSrc).filter((name) => {
140
+ try { return statSync(join(skillsSrc, name)).isDirectory(); } catch { return false; }
141
+ });
142
+ for (const name of names) {
143
+ const expected = directoryHash(join(skillsSrc, name));
144
+ for (const relDest of [join('.claude', 'skills', name), join('.agents', 'skills', name)]) {
145
+ const dest = join(projectPath, relDest);
146
+ if (!existsSync(join(dest, 'SKILL.md'))) { issues.push(`${relDest}: ausente`); continue; }
147
+ if (directoryHash(dest) !== expected) issues.push(`${relDest}: conteúdo divergiu da fonte`);
148
+ let meta = null;
149
+ try { meta = JSON.parse(readFileSync(join(dest, META_FILE), 'utf8')); } catch { /* missing */ }
150
+ if (meta?.wendkeepVersion !== WENDKEEP_VERSION || meta?.sourceHash !== expected) {
151
+ issues.push(`${relDest}: metadata stale/ausente (esperado WendKeep ${WENDKEEP_VERSION})`);
152
+ }
153
+ }
154
+ }
155
+ const expectedSection = renderAgentsSection(skillInventory(skillsSrc), directoryHash(skillsSrc));
156
+ let agentsMd = '';
157
+ try { agentsMd = readFileSync(join(projectPath, 'AGENTS.md'), 'utf8'); } catch { /* missing */ }
158
+ if (!agentsMd.includes(expectedSection)) issues.push('AGENTS.md: bloco gerenciado stale/ausente');
159
+ return { ok: issues.length === 0, issues, version: WENDKEEP_VERSION, skills: names.length };
160
+ }
161
+
101
162
  // CLI entry for `wendkeep sync-defs`.
102
163
  export function runSyncDefs(argv) {
103
164
  let vault;
@@ -116,6 +177,15 @@ export function runSyncDefs(argv) {
116
177
  }
117
178
  const vaultBase = isAbsolute(base) ? base : resolve(process.cwd(), base);
118
179
  const projectPath = resolve(project || process.cwd());
180
+ if (argv.includes('--check')) {
181
+ const r = checkSyncDefs(vaultBase, projectPath);
182
+ if (r.ok) process.stdout.write(`wendkeep sync-defs --check: ok (${r.skills} skill(s), ${r.version})\n`);
183
+ else {
184
+ process.stderr.write(`wendkeep sync-defs --check: drift detectado\n - ${r.issues.join('\n - ')}\n`);
185
+ process.stderr.write('rode `wendkeep sync-defs --reseed` e reinicie Claude Code/Codex\n');
186
+ }
187
+ process.exit(r.ok ? 0 : 1);
188
+ }
119
189
  // --reseed (0.31.0): sobrescreve as wk-* de .brain/skills com os seeds da versão instalada
120
190
  // ANTES de copiar — é como um vault existente recebe descriptions/HARD-GATE novos.
121
191
  if (argv.includes('--reseed')) {
@@ -124,7 +194,7 @@ export function runSyncDefs(argv) {
124
194
  }
125
195
  const r = syncDefs(vaultBase, projectPath);
126
196
  process.stdout.write(
127
- `wendkeep sync-defs: ${r.agents.length} agent(s) -> .codex/agents, ${r.skills.length} skill(s) -> .claude/skills\n`,
197
+ `wendkeep sync-defs: ${r.agents.length} agent(s) -> .codex/agents, ${r.skills.length} skill(s) -> .claude/skills + .agents/skills\n`,
128
198
  );
129
199
  if (r.agents.length) process.stdout.write(` agents: ${r.agents.join(', ')}\n`);
130
200
  if (r.skills.length) process.stdout.write(` skills: ${r.skills.join(', ')}\n`);
@@ -151,8 +221,8 @@ model = "gpt-5.5"
151
221
  const SKILLS_README = `# .brain/skills — versioned custom skill definitions
152
222
 
153
223
  Canonical, versioned source for your project's custom skills (\`<name>/SKILL.md\`).
154
- \`wendkeep sync-defs\` copies each skill folder here into \`<project>/.claude/skills/\` so
155
- the agent loads them. Edit here (source of truth); re-run \`wendkeep sync-defs\` after changes.
224
+ \`wendkeep sync-defs\` copies each skill folder here into \`<project>/.claude/skills/\` and
225
+ \`<project>/.agents/skills/\`. Edit here (source of truth); re-run sync after changes.
156
226
  `;
157
227
 
158
228
  const EXAMPLE_SKILL = `---
package/src/verify.mjs CHANGED
@@ -5,7 +5,11 @@ import { readFileSync, unlinkSync, writeFileSync } from 'node:fs';
5
5
  import { isAbsolute, join, resolve } from 'node:path';
6
6
  import { parseTasks, activeChange, appendFixTasks } from '../hooks/change-core.mjs';
7
7
  import { loadSensors, requiredSensors, runSensors, evaluateGate } from '../hooks/sensors-core.mjs';
8
- import { tasksHashOf } from '../hooks/spec-core.mjs';
8
+ import {
9
+ buildEffectiveRequirementPackage,
10
+ captureSpecBaseline,
11
+ tasksHashOf,
12
+ } from '../hooks/spec-core.mjs';
9
13
  import { addLesson } from '../hooks/lessons-core.mjs';
10
14
  import { getLocale } from '../hooks/locale.mjs';
11
15
 
@@ -88,16 +92,36 @@ export function runVerify(argv) {
88
92
  const tasks = parseTasks(tarefas);
89
93
  const reqIds = [...new Set(tasks.map((t) => t.req).filter(Boolean))];
90
94
  const tasksHash = tasksHashOf(tarefas);
95
+ captureSpecBaseline(vaultBase, changeDir);
96
+ const effective = buildEffectiveRequirementPackage(vaultBase, changeDir, reqIds);
97
+ if (effective.errors.length) {
98
+ process.stderr.write(`verify --deep: spec efetiva inválida: ${effective.errors.join('; ')}\n`);
99
+ process.exit(1);
100
+ }
101
+ if (effective.missing.length) {
102
+ process.stderr.write(`verify --deep: requisito(s) órfão(s) na spec efetiva: ${effective.missing.join(', ')}\n`);
103
+ process.exit(1);
104
+ }
91
105
  const pkg = {
92
106
  slug,
93
107
  tasksHash,
94
- requirements: reqIds.map((id) => ({ id })),
108
+ effectiveSpecHash: effective.hash,
109
+ requirements: effective.requirements.map((req) => {
110
+ return {
111
+ id: req.id,
112
+ name: req.name,
113
+ capability: req.capability,
114
+ operation: req.operation,
115
+ source: req.source,
116
+ body: req.body,
117
+ };
118
+ }),
95
119
  tasks: tasks.map((t) => ({ id: t.id, text: t.text, req: t.req || null, done: t.done })),
96
120
  sensors: evidence,
97
121
  };
98
122
  writeFileSync(join(changeDir, 'verificacao.json'), `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
99
123
  if (reqIds.length === 0) {
100
- writeFileSync(join(changeDir, 'verdict.json'), `${JSON.stringify({ slug, ok: true, coverage: [], tasksHash, notes: ['trivial: sem requisito'] }, null, 2)}\n`, 'utf8');
124
+ writeFileSync(join(changeDir, 'verdict.json'), `${JSON.stringify({ slug, ok: true, coverage: [], tasksHash, effectiveSpecHash: effective.hash, notes: ['trivial: sem requisito'] }, null, 2)}\n`, 'utf8');
101
125
  process.stdout.write('verify --deep: pacote + verdict trivial escritos\n');
102
126
  } else {
103
127
  process.stdout.write('verify --deep: pacote escrito — rode a skill wk-verify pra gravar verdict.json\n');