wendkeep 0.86.0 → 0.87.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.
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from 'node:crypto';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { mkdtempSync, rmSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import { join, resolve } from 'node:path';
7
+
8
+ import {
9
+ messageEvidence, messageScope, messageTasks, messageTests, nativeDesignReference, validateCommitMessage,
10
+ } from '../packages/commit/src/index.mjs';
11
+ import {
12
+ collectCommitSensorProof,
13
+ commitTaskSensorIds,
14
+ parseSignedEvidenceRef,
15
+ validateCommitProofSet,
16
+ } from '../packages/commit/src/proof-validation.mjs';
17
+
18
+ function option(argv, name) {
19
+ const index = argv.indexOf(name);
20
+ if (index >= 0) return argv[index + 1] || '';
21
+ return argv.find((item) => item.startsWith(`${name}=`))?.slice(name.length + 1) || '';
22
+ }
23
+
24
+ function git(args, { binary = false, allowFailure = false } = {}) {
25
+ const result = spawnSync('git', args, {
26
+ encoding: binary ? null : 'utf8', windowsHide: true, maxBuffer: 64 * 1024 * 1024,
27
+ });
28
+ if (!allowFailure && result.status !== 0) {
29
+ process.stderr.write((binary ? result.stderr?.toString('utf8') : result.stderr) || `git ${args.join(' ')} failed\n`);
30
+ process.exit(2);
31
+ }
32
+ return result;
33
+ }
34
+ const text = (args, options) => String(git(args, options).stdout || '');
35
+ const objectExists = (specifier) => git(['cat-file', '-e', specifier], { allowFailure: true }).status === 0;
36
+ const parents = (sha) => text(['show', '-s', '--format=%P', sha]).trim().split(/\s+/).filter(Boolean);
37
+
38
+ function commitDiff(sha) {
39
+ const parent = parents(sha)[0];
40
+ const args = parent
41
+ ? ['diff', '--binary', '--no-ext-diff', '--no-color', parent, sha]
42
+ : ['show', '--format=', '--binary', '--no-ext-diff', '--no-color', sha];
43
+ return git(args, { binary: true }).stdout;
44
+ }
45
+
46
+ function changedFiles(sha) {
47
+ const parent = parents(sha)[0];
48
+ const args = parent
49
+ ? ['diff', '--name-only', '-z', '--no-renames', parent, sha]
50
+ : ['diff-tree', '--root', '--no-commit-id', '--name-only', '-z', '-r', sha];
51
+ return text(args).split('\0').filter(Boolean).map((path) => path.replaceAll('\\', '/'))
52
+ .sort((left, right) => left.localeCompare(right, 'en'));
53
+ }
54
+
55
+ function scopeFor(sha) {
56
+ return { sha256: createHash('sha256').update(commitDiff(sha)).digest('hex'), files: changedFiles(sha) };
57
+ }
58
+
59
+ const docsPath = (path) => /^(?:docs\/|README(?:\.en)?\.md$|[^/]+\.md$)/.test(path);
60
+ const testPath = (path) => /^(?:tests?\/|fixtures?\/)/.test(path) || /(?:^|\/)__tests__\//.test(path);
61
+ function trivialCommit(subject, files) {
62
+ if (!files.length) return false;
63
+ if (/^docs(?:\([^)]*\))?:/.test(subject)) return files.every(docsPath);
64
+ if (/^test(?:\([^)]*\))?:/.test(subject)) return files.every(testPath);
65
+ if (/^chore(?:\([^)]*\))?:/.test(subject)) return files.every((path) => docsPath(path) || testPath(path));
66
+ return false;
67
+ }
68
+
69
+ function mergeErrors(sha, message) {
70
+ const commitParents = parents(sha);
71
+ if (commitParents.length < 2 || !/^Merge\b/.test(message.split(/\r?\n/, 1)[0])) {
72
+ return ['merge commit must have multiple parents and a canonical Merge subject'];
73
+ }
74
+ const errors = [];
75
+ for (const path of changedFiles(sha)) {
76
+ const mergeObject = text(['rev-parse', `${sha}:${path}`], { allowFailure: true }).trim();
77
+ if (!mergeObject) continue;
78
+ const inherited = commitParents.some((parent) => (
79
+ text(['rev-parse', `${parent}:${path}`], { allowFailure: true }).trim() === mergeObject
80
+ ));
81
+ if (!inherited) errors.push(`WENDKEEP_COMMIT_MERGE_RESOLUTION_UNGOVERNED: ${path}`);
82
+ }
83
+ return errors;
84
+ }
85
+
86
+ function commitJson(sha, path) {
87
+ try { return JSON.parse(text(['show', `${sha}:${path}`])); } catch { return null; }
88
+ }
89
+
90
+ function collectSensorsAtCommit(sha, entries) {
91
+ const ids = commitTaskSensorIds(entries);
92
+ if (!ids.length) return null;
93
+ const config = commitJson(sha, 'wendkeep.sensors.json');
94
+ if (!config || !Array.isArray(config.sensors)) {
95
+ throw Object.assign(new Error('versioned wendkeep.sensors.json is required'), {
96
+ code: 'WENDKEEP_COMMIT_SENSOR_CONFIG_MISSING',
97
+ });
98
+ }
99
+ const parent = mkdtempSync(join(tmpdir(), 'wendkeep-commit-range-'));
100
+ const checkout = join(parent, 'checkout');
101
+ const added = git(['worktree', 'add', '--detach', '--force', checkout, sha], { allowFailure: true });
102
+ if (added.status !== 0) {
103
+ rmSync(parent, { recursive: true, force: true });
104
+ throw Object.assign(new Error(String(added.stderr || 'temporary commit checkout failed').trim()), {
105
+ code: 'WENDKEEP_COMMIT_CHECKOUT_FAILED',
106
+ });
107
+ }
108
+ try {
109
+ return collectCommitSensorProof({ sensors: config.sensors, ids, cwd: checkout });
110
+ } finally {
111
+ git(['worktree', 'remove', '--force', checkout], { allowFailure: true });
112
+ const bounded = resolve(parent);
113
+ if (bounded.startsWith(resolve(tmpdir()))) rmSync(bounded, { recursive: true, force: true });
114
+ }
115
+ }
116
+
117
+ function evidenceErrors(sha, message, scope) {
118
+ const errors = [];
119
+ const evidence = messageEvidence(message);
120
+ const entries = [];
121
+ for (const item of evidence) {
122
+ if (item.status !== 'verified' || ['evidence', 'receipt', 'verdict'].includes(item.kind)) {
123
+ errors.push('WENDKEEP_COMMIT_REMOTE_PROOF_UNAVAILABLE');
124
+ continue;
125
+ }
126
+ let signed;
127
+ try { signed = parseSignedEvidenceRef(item.ref); }
128
+ catch (error) { errors.push(error.code || error.message); continue; }
129
+ if (!objectExists(`${sha}:${signed.path}`)) {
130
+ errors.push(`WENDKEEP_COMMIT_EVIDENCE_UNVERSIONED: ${signed.path}`);
131
+ continue;
132
+ }
133
+ entries.push({ kind: item.kind, path: signed.path, sha256: signed.sha256, content: text(['show', `${sha}:${signed.path}`]) });
134
+ if (item.status !== 'verified') errors.push(`WENDKEEP_COMMIT_EVIDENCE_STATUS_INVALID: ${signed.path}`);
135
+ }
136
+ const adr = message.match(/^ADR:\s*(ADR-\d{4,})$/m)?.[1] || '';
137
+ const design = nativeDesignReference(message);
138
+ const authority = adr
139
+ ? { kind: 'adr', adr, ref: entries.find((entry) => entry.kind === 'adr')?.path || '', issue: message.match(/^Refs:\s*(#\d+)$/m)?.[1] || '' }
140
+ : { kind: 'native', issue: message.match(/^Issue:\s*(#\d+)$/m)?.[1] || '', design };
141
+ try {
142
+ const config = commitJson(sha, '.wendkeep.json') || {};
143
+ const executionProof = collectSensorsAtCommit(sha, entries);
144
+ const snapshot = {
145
+ head_sha: parents(sha)[0] || sha,
146
+ index_tree_sha: text(['show', '-s', '--format=%T', sha]).trim(),
147
+ };
148
+ const resolved = validateCommitProofSet({
149
+ entries, authority, stagedHash: scope.sha256,
150
+ context: {
151
+ projectId: config.projectId || '',
152
+ changeSlug: adr ? adr.toLowerCase() : `issue-${authority.issue.slice(1)}`,
153
+ baseSha: snapshot.base_sha,
154
+ headSha: snapshot.head_sha,
155
+ indexTreeSha: snapshot.index_tree_sha,
156
+ worktreeDigest: snapshot.worktree_digest,
157
+ dirty: snapshot.dirty,
158
+ sensorConfigSha256: executionProof?.configSha256,
159
+ executionProof,
160
+ profile: String(config?.harness?.profile || 'OFF').toUpperCase(),
161
+ },
162
+ });
163
+ if (JSON.stringify(messageTasks(message)) !== JSON.stringify(resolved.tasks)) errors.push('WENDKEEP_COMMIT_TASKS_MISMATCH');
164
+ if (JSON.stringify(messageTests(message)) !== JSON.stringify(resolved.tests)) errors.push('WENDKEEP_COMMIT_TESTS_MISMATCH');
165
+ } catch (error) {
166
+ errors.push(`${error.code || 'WENDKEEP_COMMIT_EVIDENCE_UNVERIFIED'}: ${error.message}`);
167
+ }
168
+ return errors;
169
+ }
170
+
171
+ function authorityErrors(sha, message) {
172
+ const errors = [];
173
+ const design = nativeDesignReference(message);
174
+ if (!design) return errors;
175
+ if (!objectExists(`${sha}:${design}`)) return [`WENDKEEP_COMMIT_DESIGN_UNVERSIONED: ${design}`];
176
+ const issue = message.match(/^Issue:\s*(#\d+)$/m)?.[1] || '';
177
+ const content = text(['show', `${sha}:${design}`]);
178
+ if (!content.includes(issue)) errors.push('WENDKEEP_COMMIT_NATIVE_ISSUE_UNVERIFIED');
179
+ const config = commitJson(sha, '.wendkeep.json');
180
+ if (String(config?.harness?.profile || '').toUpperCase() !== 'OFF') errors.push('WENDKEEP_COMMIT_NATIVE_PROFILE_REQUIRED');
181
+ const adrPaths = text(['ls-tree', '-r', '--name-only', sha]).split(/\r?\n/)
182
+ .filter((path) => /ADR-\d+.*\.md$/i.test(path));
183
+ if (adrPaths.some((path) => {
184
+ const adr = text(['show', `${sha}:${path}`]);
185
+ return adr.includes(issue) && adr.includes(design.split('/').at(-1));
186
+ })) errors.push('WENDKEEP_COMMIT_CAUSAL_AUTHORITY_EXISTS');
187
+ return errors;
188
+ }
189
+
190
+ function configuredPrivacyErrors(sha, message) {
191
+ const config = commitJson(sha, '.wendkeep.json');
192
+ const vault = typeof config?.vault === 'string' ? config.vault.replaceAll('\\', '/').trim() : '';
193
+ if (!vault) return [];
194
+ const name = vault.split('/').filter(Boolean).at(-1) || '';
195
+ const lower = message.toLowerCase();
196
+ return [vault, name].some((marker) => marker.length >= 3 && lower.includes(marker.toLowerCase()))
197
+ ? ['WENDKEEP_COMMIT_PRIVATE_PATH: message references the configured project Vault'] : [];
198
+ }
199
+
200
+ const base = option(process.argv.slice(2), '--base') || process.env.WENDKEEP_COMMIT_BASE || '';
201
+ const head = option(process.argv.slice(2), '--head') || process.env.WENDKEEP_COMMIT_HEAD || 'HEAD';
202
+ if (!base) {
203
+ process.stderr.write('WENDKEEP_COMMIT_ARGUMENT: --base is required\n');
204
+ process.exit(2);
205
+ }
206
+
207
+ const commits = text(['rev-list', '--reverse', `${base}..${head}`]).trim().split(/\r?\n/).filter(Boolean);
208
+ const failures = [];
209
+ for (const sha of commits) {
210
+ const message = text(['show', '-s', '--format=%B', sha]);
211
+ const subject = message.split(/\r?\n/, 1)[0];
212
+ const commitParents = parents(sha);
213
+ const errors = [...configuredPrivacyErrors(sha, message)];
214
+ if (commitParents.length > 1) {
215
+ errors.push(...mergeErrors(sha, message));
216
+ } else {
217
+ const result = validateCommitMessage(message);
218
+ errors.push(...result.errors);
219
+ if (messageEvidence(message).some((item) => (
220
+ item.status !== 'verified' || ['evidence', 'receipt', 'verdict'].includes(item.kind)
221
+ ))) errors.push('WENDKEEP_COMMIT_REMOTE_PROOF_UNAVAILABLE');
222
+ const files = changedFiles(sha);
223
+ if (!result.governed && !trivialCommit(subject, files)) errors.push('WENDKEEP_COMMIT_PRODUCT_CHANGE_UNGOVERNED');
224
+ if (result.governed && result.ok) {
225
+ const expected = scopeFor(sha);
226
+ const observed = messageScope(message);
227
+ if (expected.sha256 !== observed.sha256 || JSON.stringify(expected.files) !== JSON.stringify(observed.files)) {
228
+ errors.push('WENDKEEP_COMMIT_SCOPE_MISMATCH');
229
+ }
230
+ errors.push(...evidenceErrors(sha, message, expected), ...authorityErrors(sha, message));
231
+ }
232
+ }
233
+ if (errors.length) failures.push({ sha, subject, errors: [...new Set(errors)] });
234
+ }
235
+
236
+ if (failures.length) {
237
+ process.stderr.write(`WENDKEEP_COMMIT_RANGE_INVALID: ${failures.length} invalid commit(s)\n`);
238
+ for (const failure of failures) {
239
+ process.stderr.write(`${failure.sha.slice(0, 12)} ${failure.subject}\n`);
240
+ for (const error of failure.errors) process.stderr.write(` - ${error}\n`);
241
+ }
242
+ process.exit(1);
243
+ }
244
+ process.stdout.write(`${commits.length} commit(s) valid\n`);
package/src/doctor.mjs CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  import { inspectPortableState } from './portable.mjs';
20
20
  import { inspectSyncOutbox, readLocalSyncState } from './sync-outbox.mjs';
21
21
  import { readProjectForValidation } from '../packages/vault/src/validate-memory.mjs';
22
+ import { inspectGitCommitHooks } from './git-commit-hooks.mjs';
22
23
 
23
24
  const healthStatusLabel = (status) => ({
24
25
  healthy: 'saudável', warning: 'atenção', degraded: 'degradada', blocked: 'bloqueada', legacy: 'legado',
@@ -182,6 +183,11 @@ export function runDoctor(argv) {
182
183
  process.stdout.write(` → ${issue.slug}: ${issue.errorCode} — ${issue.repair}\n`);
183
184
  }
184
185
 
186
+ const commitHooks = inspectGitCommitHooks({ projectRoot });
187
+ process.stdout.write(`\n[commit-hooks] ${commitHooks.status}\n`);
188
+ for (const issue of commitHooks.issues) process.stdout.write(` ! ${issue}\n`);
189
+ if (commitHooks.repair) process.stdout.write(` → ${commitHooks.repair}\n`);
190
+
185
191
  const activeContexts = inspectActiveContextHealth({ vaultBase, projectRoot });
186
192
  process.stdout.write(`\n${renderActiveContextHealthLines(activeContexts).join('\n')}\n`);
187
193
 
@@ -257,6 +263,7 @@ export function runDoctor(argv) {
257
263
  || repairable.length
258
264
  || warnings.length
259
265
  || worktrees.issues.length
266
+ || (commitHooks.configured && commitHooks.status !== 'healthy')
260
267
  || activeContexts.issues.length
261
268
  || ['diverged', 'invalid'].includes(portable.status)
262
269
  || sync.status === 'corrupt'
@@ -0,0 +1,112 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ export const GIT_COMMIT_HOOKS = ['prepare-commit-msg', 'commit-msg'];
7
+ export const GIT_COMMIT_HOOKS_PATH = '.githooks';
8
+
9
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
10
+ const packagedHooks = join(packageRoot, '.githooks');
11
+
12
+ function git(projectRoot, args, { allowFailure = false } = {}) {
13
+ const result = spawnSync('git', args, {
14
+ cwd: projectRoot,
15
+ encoding: 'utf8',
16
+ windowsHide: true,
17
+ });
18
+ if (!allowFailure && result.status !== 0) {
19
+ const error = new Error((result.stderr || `git ${args.join(' ')} failed`).trim());
20
+ error.code = 'WENDKEEP_COMMIT_GIT_FAILED';
21
+ throw error;
22
+ }
23
+ return result;
24
+ }
25
+
26
+ function hookState(projectRoot, name) {
27
+ const source = join(packagedHooks, name);
28
+ const target = join(projectRoot, GIT_COMMIT_HOOKS_PATH, name);
29
+ if (!existsSync(target)) return { name, source, target, state: 'missing' };
30
+ return {
31
+ name,
32
+ source,
33
+ target,
34
+ state: readFileSync(target).equals(readFileSync(source)) ? 'current' : 'drift',
35
+ };
36
+ }
37
+
38
+ export function inspectGitCommitHooks({ projectRoot = process.cwd() } = {}) {
39
+ const root = resolve(projectRoot);
40
+ const repository = git(root, ['rev-parse', '--show-toplevel'], { allowFailure: true });
41
+ if (repository.status !== 0) {
42
+ return { status: 'unavailable', configured: false, issues: ['not a Git repository'], repair: '' };
43
+ }
44
+ const configured = git(root, ['config', '--local', '--get', 'core.hooksPath'], { allowFailure: true });
45
+ const configuredPath = configured.status === 0 ? configured.stdout.trim().replaceAll('\\', '/') : '';
46
+ if (!configuredPath) {
47
+ return { status: 'disabled', configured: false, issues: [], repair: 'wendkeep init --git-commit-hooks --yes' };
48
+ }
49
+ if (!['.githooks', './.githooks'].includes(configuredPath)) {
50
+ return {
51
+ status: 'drift',
52
+ configured: true,
53
+ configuredPath,
54
+ issues: [`core.hooksPath points to ${configuredPath}, not .githooks`],
55
+ repair: 'wendkeep init --git-commit-hooks --force --yes',
56
+ };
57
+ }
58
+ const states = GIT_COMMIT_HOOKS.map((name) => hookState(root, name));
59
+ const issues = states.filter((item) => item.state !== 'current').map((item) => (
60
+ item.state === 'missing'
61
+ ? `${item.name}: missing`
62
+ : `${item.name}: content differs from the installed WendKeep version`
63
+ ));
64
+ return {
65
+ status: issues.length ? (states.some((item) => item.state === 'missing') ? 'missing' : 'drift') : 'healthy',
66
+ configured: true,
67
+ configuredPath,
68
+ issues,
69
+ repair: issues.length ? 'wendkeep init --git-commit-hooks --force --yes' : '',
70
+ };
71
+ }
72
+
73
+ export function installGitCommitHooks({ projectRoot = process.cwd(), force = false } = {}) {
74
+ const root = resolve(projectRoot);
75
+ git(root, ['rev-parse', '--show-toplevel']);
76
+ const configured = git(root, ['config', '--local', '--get', 'core.hooksPath'], { allowFailure: true });
77
+ const configuredPath = configured.status === 0 ? configured.stdout.trim().replaceAll('\\', '/') : '';
78
+ if (configuredPath && !['.githooks', './.githooks'].includes(configuredPath) && !force) {
79
+ return {
80
+ status: 'conflict',
81
+ conflicts: ['core.hooksPath'],
82
+ configuredPath,
83
+ repair: 'wendkeep init --git-commit-hooks --force --yes',
84
+ };
85
+ }
86
+ const states = GIT_COMMIT_HOOKS.map((name) => hookState(root, name));
87
+ const conflicts = states.filter((item) => item.state === 'drift').map((item) => item.name);
88
+ if (conflicts.length && !force) {
89
+ return { status: 'conflict', conflicts, repair: 'wendkeep init --git-commit-hooks --force --yes' };
90
+ }
91
+ mkdirSync(join(root, GIT_COMMIT_HOOKS_PATH), { recursive: true });
92
+ let changed = false;
93
+ for (const item of states) {
94
+ if (item.state === 'current') continue;
95
+ if (item.state === 'drift' && force && !existsSync(`${item.target}.bak`)) {
96
+ copyFileSync(item.target, `${item.target}.bak`);
97
+ }
98
+ copyFileSync(item.source, item.target);
99
+ try { chmodSync(item.target, 0o755); } catch { /* Git for Windows uses its executable shim. */ }
100
+ changed = true;
101
+ }
102
+ if (configured.status !== 0 || configured.stdout.trim().replaceAll('\\', '/') !== GIT_COMMIT_HOOKS_PATH) {
103
+ git(root, ['config', '--local', 'core.hooksPath', GIT_COMMIT_HOOKS_PATH]);
104
+ changed = true;
105
+ }
106
+ return {
107
+ status: changed ? 'installed' : 'unchanged',
108
+ conflicts: [],
109
+ hooks: GIT_COMMIT_HOOKS,
110
+ path: join(root, GIT_COMMIT_HOOKS_PATH),
111
+ };
112
+ }
package/src/init.mjs CHANGED
@@ -41,6 +41,7 @@ import { adoptSpecsState, ensureSpecsReadme, SPECS_STATE_FILE } from '../hooks/s
41
41
  import { bindProjectVault, readProjectBinding } from './project-vault.mjs';
42
42
  import { seedMemoryV2 } from './memory.mjs';
43
43
  import { installVscodeWorktreeTasks } from './worktree.mjs';
44
+ import { installGitCommitHooks } from './git-commit-hooks.mjs';
44
45
  import {
45
46
  DEFAULT_OPERATING_PROFILE,
46
47
  normalizeOperatingProfile,
@@ -69,6 +70,7 @@ function parseArgs(argv) {
69
70
  else if (a === '--no-companions') args.noCompanions = true;
70
71
  else if (a === '--no-colors') args.noColors = true;
71
72
  else if (a === '--vscode-worktree-tasks') args.vscodeWorktreeTasks = true;
73
+ else if (a === '--git-commit-hooks') args.gitCommitHooks = true;
72
74
  else if (a === '--dotcontext-mcp') args.dotcontextMcp = argv[++i];
73
75
  else if (a.startsWith('--dotcontext-mcp=')) args.dotcontextMcp = a.slice(17);
74
76
  else if (a === '--dotcontext-hooks') args.dotcontextHooks = argv[++i];
@@ -639,6 +641,17 @@ export async function runInit(argv) {
639
641
  }
640
642
  log(M.codexTrust);
641
643
 
644
+ // Git commit policy is opt-in because this is the only init surface that writes
645
+ // repository-local Git configuration. Existing custom hooks are never overwritten silently.
646
+ if (args.gitCommitHooks) {
647
+ const hooks = installGitCommitHooks({ projectRoot: projectPath, force: args.force });
648
+ if (hooks.status === 'conflict') {
649
+ log(` [!] Git commit hooks preserved (${hooks.conflicts.join(', ')}); review and rerun ${hooks.repair}`);
650
+ } else {
651
+ log(` Git commit hooks: ${hooks.status} (${hooks.path})`);
652
+ }
653
+ }
654
+
642
655
  // 3. .mcp.json --------------------------------------------------------------
643
656
  // Written when the native WendKeep MCP is wanted OR a selected companion ships an MCP server.
644
657
  const companionMcp = companionMcpPatch(companions, skipMcp);
@@ -524,6 +524,83 @@ the author — even if you wrote the code, enter as if you'd never seen it. Fres
524
524
  - \`verdict-template.json\` — the exact shape of the \`verdict.json\` to write.
525
525
  `;
526
526
 
527
+ const WK_COMMIT = `# Commit universal baseado em evidências
528
+
529
+ Use para preparar commits de implementação que precisem do contrato WendKeep. O gerador recebe
530
+ somente referências sanitizadas e o diff staged resumido; nunca leia, copie ou publique o Vault,
531
+ \`.brain\`, registros de sessão, tokens ou segredos na mensagem.
532
+
533
+ ## Fluxo
534
+
535
+ 1. Confirme que autoridade e referências de evidência estão atuais. O draft **não aceita** texto
536
+ caller-controlled em \`tasks\` ou \`tests\`: o runtime deriva tasks de Task Contracts concluídos;
537
+ Tests vêm somente de \`[sensor:<id>]\` executado pelo coletor.
538
+ Sensors do Envelope devem corresponder exatamente à reexecução; só a reexecução gera Tests.
539
+ \`[phase:verify]\` sozinho nunca prova execução, e o range reexecuta o sensor no SHA exato,
540
+ rederiva authority/artefatos/task/spec/Scope/config no SHA. Envelope/Verdict/receipt/TDD ficam
541
+ somente na validação local e são omitidos da Evidence remota; nunca publique seus IDs locais.
542
+ Tasks com \`[req:]\` exigem \`spec\` versionada e sanitizada. Use
543
+ \`authority.kind: adr\` sempre que existe uma change ou ADR causal. Somente sob perfil observado
544
+ \`OFF\`, sem context/change/lease ou ADR causal real, use \`authority.kind: native\`, com
545
+ \`Issue #NNN\` e design versionado sob \`docs/superpowers/specs/\` ou \`plans/\`.
546
+ 2. Crie um JSON conforme \`schema/commit-message-v1.schema.json\`. Não declare \`fresh\`,
547
+ \`verified\`, \`tasks\` ou \`tests\`: o runtime rederiva prova, digest SHA-256 do artefato,
548
+ binding completo de identidade/snapshot/tasks/spec/config e Scope do diff staged. Expected
549
+ ausente, envelope vazio, task pendente,
550
+ verdict sem selos/cobertura/independência ou
551
+ receipt sem chain/observation válidos falham fechados. \`Co-Authored-By\` é omitido
552
+ enquanto não houver identidade registrada confiável.
553
+ 3. Com os arquivos já staged, rode
554
+ \`wendkeep commit context --input <arquivo.json>\`. O CLI calcula o hash do index e grava o
555
+ contexto sanitizado dentro de \`.git\`, fora do working tree.
556
+ 4. Rode \`git commit\` normalmente. Os hooks opt-in revalidam contexto, hash, privacidade e
557
+ trivialidade antes de consumir o contexto. Instale-os
558
+ explicitamente com \`wendkeep init --git-commit-hooks --yes\`.
559
+ 5. Para diagnóstico, rode \`wendkeep doctor\`; para limpar contexto abandonado, use
560
+ \`wendkeep commit context --clear\`.
561
+
562
+ Não use \`--no-verify\` para implementação. Amend, merge e squash preservam a mensagem existente e
563
+ não recebem conteúdo inventado. A validação remota do PR continua sendo a defesa contra bypass.
564
+ `;
565
+
566
+ const WK_COMMIT_EN = `# Evidence-based universal commit
567
+
568
+ Use this skill to prepare implementation commits governed by the WendKeep message contract. The
569
+ generator receives sanitized references and a staged-diff summary only; never read, copy, or publish
570
+ the Vault, \`.brain\`, session registries, tokens, or secrets into a commit message.
571
+
572
+ ## Flow
573
+
574
+ 1. Confirm that authority and evidence references are current. The draft **does not accept**
575
+ caller-controlled \`tasks\` or \`tests\`; runtime derives tasks from completed Task Contracts.
576
+ Tests come only from a collector-executed \`[sensor:<id>]\`.
577
+ Envelope sensors must exactly match reexecution; only reexecution emits Tests.
578
+ \`[phase:verify]\` alone never proves execution, and range re-executes the sensor at the exact
579
+ SHA and re-derives authority/artifacts/task/spec/Scope/config there. Envelope/Verdict/receipt/TDD
580
+ stay local-only and are omitted from remote Evidence; never publish their local IDs. Tasks with
581
+ \`[req:]\` require a versioned sanitized \`spec\`. Use \`authority.kind: adr\`
582
+ whenever a causal change or ADR exists. Only under observed profile \`OFF\`, with no real causal
583
+ context/change/lease or ADR, use \`authority.kind: native\` with \`Issue #NNN\` and a versioned
584
+ design under \`docs/superpowers/specs/\` or \`plans/\`.
585
+ 2. Create JSON matching \`schema/commit-message-v1.schema.json\`. Do not claim \`fresh\`,
586
+ \`verified\`, \`tasks\`, or \`tests\`: runtime re-derives proof, artifact SHA-256, canonical
587
+ full identity/snapshot/tasks/spec/config binding, and staged-diff Scope. Missing expected fields,
588
+ empty envelopes, pending tasks, verdicts without
589
+ seals/coverage/independence, and receipts
590
+ without a valid chain/observation fail closed. \`Co-Authored-By\` is omitted
591
+ until a trusted identity registry can resolve it.
592
+ 3. After staging the files, run \`wendkeep commit context --input <file.json>\`. The CLI hashes the
593
+ Git index and stores the sanitized context inside \`.git\`, outside the working tree.
594
+ 4. Run \`git commit\` normally. The opt-in hooks revalidate context, hash, privacy, and triviality
595
+ before consuming context. Install them
596
+ explicitly with \`wendkeep init --git-commit-hooks --yes\`.
597
+ 5. Run \`wendkeep doctor\` for diagnostics; abandon a context with
598
+ \`wendkeep commit context --clear\`.
599
+
600
+ Do not use \`--no-verify\` for implementation work. Amend, merge, and squash preserve the existing
601
+ message and never receive invented content. The PR range check remains the remote bypass defense.
602
+ `;
603
+
527
604
  // --- bundled templates (shipped alongside the relevant SKILL.md) -------------
528
605
 
529
606
  // Shared, language-neutral verdict skeleton for the independent verify pass.
@@ -709,6 +786,7 @@ const WK_SKILLS_PT = [
709
786
  skill('wk-brainstorming', 'Use quando a ideia ainda é vaga ou o usuário quer discutir/planejar uma feature (inclusive em plan mode) — vira design aprovado, com closure gate e tabela out-of-scope, antes de código.', BRAINSTORMING, [{ name: 'design-template.md', content: DESIGN_TEMPLATE_PT }]),
710
787
  skill('wk-planning', 'Use após um design aprovado ou um plano aceito (inclusive plan mode) — decompõe em plano de tarefas TDD bite-sized e registra na change ativa.', PLANNING, [{ name: 'plan-template.md', content: PLAN_TEMPLATE_PT }]),
711
788
  skill('wk-verify', 'Use no verify deep — passe independente read-only (autor≠verificador) que re-deriva a cobertura do spec e grava verdict.json.', VERIFY, [{ name: 'spec-reviewer-prompt.md', content: REVIEWER_PROMPT_PT }, { name: 'verdict-template.json', content: VERDICT_TEMPLATE }]),
789
+ skill('wk-commit', 'Use ao preparar commits de implementação baseados em autoridade causal, tarefas, testes e evidências verificadas, com hooks Git opt-in e privacidade local.', WK_COMMIT),
712
790
  ];
713
791
 
714
792
  const WK_SKILLS_EN = [
@@ -718,6 +796,7 @@ const WK_SKILLS_EN = [
718
796
  skill('wk-brainstorming', 'Use when the idea is still vague or the user wants to discuss/plan a feature (plan mode included) — turns it into an approved design, with a closure gate and out-of-scope table, before code.', BRAINSTORMING_EN, [{ name: 'design-template.md', content: DESIGN_TEMPLATE_EN }]),
719
797
  skill('wk-planning', 'Use after an approved design or an accepted plan (plan mode included) — decomposes it into a bite-sized TDD task plan recorded in the active change.', PLANNING_EN, [{ name: 'plan-template.md', content: PLAN_TEMPLATE_EN }]),
720
798
  skill('wk-verify', 'Use in verify deep — an independent read-only pass (author≠verifier) that re-derives spec coverage and writes verdict.json.', VERIFY_EN, [{ name: 'spec-reviewer-prompt.md', content: REVIEWER_PROMPT_EN }, { name: 'verdict-template.json', content: VERDICT_TEMPLATE }]),
799
+ skill('wk-commit', 'Use when preparing implementation commits from causal authority, tasks, tests, and verified evidence, with opt-in Git hooks and local privacy.', WK_COMMIT_EN),
721
800
  ];
722
801
 
723
802
  // Skill set for a locale. WK_SKILLS stays the pt-BR set for back-compat.