wendkeep 0.76.9 → 0.78.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/README.en.md +3 -3
- package/README.md +3 -3
- package/docs/en/commands/changes-and-verification.md +8 -1
- package/docs/en/commands/verify.md +22 -5
- package/docs/en/commands/worktrees.md +80 -32
- package/docs/pt-BR/commands/changes-and-verification.md +8 -1
- package/docs/pt-BR/commands/verify.md +22 -6
- package/docs/pt-BR/commands/worktrees.md +80 -31
- package/hooks/change-core.mjs +2 -1
- package/hooks/harness-doctor.mjs +51 -1
- package/hooks/spec-core.mjs +26 -8
- package/package.json +2 -2
- package/packages/cli/src/index.mjs +1 -1
- package/packages/harness/src/sensors-core.mjs +57 -3
- package/packages/vault/src/evidence-envelope.mjs +73 -0
- package/packages/vault/src/index.mjs +1 -0
- package/packages/vault/src/memory-handoff.mjs +46 -5
- package/packages/vault/src/vault-path-safety.mjs +11 -0
- package/schema/wendkeep.evidence-envelope-v2.schema.json +92 -0
- package/src/change.mjs +93 -10
- package/src/evidence-envelope.mjs +288 -0
- package/src/skills-seed.mjs +11 -5
- package/src/verify.mjs +85 -22
- package/src/worktree-cleanup.mjs +712 -0
- package/src/worktree.mjs +130 -19
package/src/change.mjs
CHANGED
|
@@ -19,13 +19,24 @@ import {
|
|
|
19
19
|
isGuideCompactChange,
|
|
20
20
|
setActiveChange,
|
|
21
21
|
} from '../hooks/change-core.mjs';
|
|
22
|
-
import { evaluateGate, requiredSensors } from '../hooks/sensors-core.mjs';
|
|
22
|
+
import { evaluateGate, loadSensorsDetailed, requiredSensors } from '../hooks/sensors-core.mjs';
|
|
23
23
|
import { buildEffectiveRequirementPackage, evaluateVerdict, formatOrphanReqs, tasksHashOf, parseSpecsList, parseDelta, parseRequirements, applyDelta, validateSpecImpact } from '../hooks/spec-core.mjs';
|
|
24
24
|
import { getNextAdrNumber, readControl, readSessionRegistry, upsertSessionRegistry } from '../hooks/obsidian-common.mjs';
|
|
25
25
|
import { getLocale } from '../hooks/locale.mjs';
|
|
26
26
|
import { enqueueObserverDocumentChange } from './observer-sql-publish.mjs';
|
|
27
27
|
import { readProjectForValidation } from '../packages/vault/src/validate-memory.mjs';
|
|
28
28
|
import { resolveCommandActiveContext } from './active-context-runtime.mjs';
|
|
29
|
+
import {
|
|
30
|
+
captureGitSnapshot,
|
|
31
|
+
resolveEvidenceIdentity,
|
|
32
|
+
sensorConfigSha256,
|
|
33
|
+
} from './evidence-envelope.mjs';
|
|
34
|
+
import {
|
|
35
|
+
evaluateEvidenceBinding,
|
|
36
|
+
evidenceCheckoutBinding,
|
|
37
|
+
evidenceCheckoutBindingMatches,
|
|
38
|
+
evidenceSensors,
|
|
39
|
+
} from '../packages/vault/src/evidence-envelope.mjs';
|
|
29
40
|
|
|
30
41
|
function observerMarkdownUnder(vaultBase, relativeRoot) {
|
|
31
42
|
const output = [];
|
|
@@ -74,15 +85,15 @@ export function runChange(argv) {
|
|
|
74
85
|
const VALUE_FLAGS = new Set(['--vault', '--change', '--project', '--session']);
|
|
75
86
|
const slugArg = () => rest.find((a, i) => !a.startsWith('-') && !VALUE_FLAGS.has(rest[i - 1]));
|
|
76
87
|
const projectRoot = resolve(opt(rest, '--project') || process.cwd());
|
|
88
|
+
const sessionId = opt(rest, '--session')
|
|
89
|
+
|| process.env.CODEX_THREAD_ID
|
|
90
|
+
|| process.env.CLAUDE_SESSION_ID
|
|
91
|
+
|| '';
|
|
77
92
|
let contextResolved = false;
|
|
78
93
|
let resolvedContext = null;
|
|
79
94
|
const context = () => {
|
|
80
95
|
if (contextResolved) return resolvedContext;
|
|
81
96
|
contextResolved = true;
|
|
82
|
-
const sessionId = opt(rest, '--session')
|
|
83
|
-
|| process.env.CODEX_THREAD_ID
|
|
84
|
-
|| process.env.CLAUDE_SESSION_ID
|
|
85
|
-
|| '';
|
|
86
97
|
try {
|
|
87
98
|
resolvedContext = resolveCommandActiveContext({ vaultBase, projectRoot, sessionId });
|
|
88
99
|
return resolvedContext;
|
|
@@ -196,19 +207,48 @@ export function runChange(argv) {
|
|
|
196
207
|
}
|
|
197
208
|
let evidence = null;
|
|
198
209
|
try { evidence = JSON.parse(readFileSync(join(dir, 'evidencia.json'), 'utf8')); } catch { /* sem evidência */ }
|
|
199
|
-
if (evidence) for (const e of evidence) process.stdout.write(` ${e.status === 'green' ? '✓' : '✗'} ${e.id} (${e.severity || 'critical'})\n`);
|
|
210
|
+
if (evidence) for (const e of evidenceSensors(evidence)) process.stdout.write(` ${e.status === 'green' ? '✓' : '✗'} ${e.id} (${e.severity || 'critical'})\n`);
|
|
200
211
|
else process.stdout.write('evidencia: ausente\n');
|
|
201
212
|
const reqIds = [...new Set(tasks.flatMap((t) => t.reqs ?? []))];
|
|
202
213
|
const effective = buildEffectiveRequirementPackage(vaultBase, dir, reqIds);
|
|
203
214
|
if (effective.errors.length || effective.missing.length) {
|
|
204
215
|
process.stdout.write(`spec efetiva: inválida (${[...effective.errors, ...effective.missing.map((id) => `req órfão ${id}`)].join('; ')})\n`);
|
|
205
216
|
}
|
|
217
|
+
if (evidence) {
|
|
218
|
+
let expected = {
|
|
219
|
+
change_slug: slug,
|
|
220
|
+
tasks_sha256: tasksHashOf(tarefasMd),
|
|
221
|
+
effective_spec_sha256: `sha256:${effective.hash}`,
|
|
222
|
+
};
|
|
223
|
+
let unavailable = '';
|
|
224
|
+
try {
|
|
225
|
+
const ids = requiredSensors(tasks);
|
|
226
|
+
const loaded = loadSensorsDetailed(projectRoot);
|
|
227
|
+
expected = {
|
|
228
|
+
...expected,
|
|
229
|
+
identity: resolveEvidenceIdentity({
|
|
230
|
+
vaultBase, projectRoot, changeSlug: slug, sessionId, context: context(),
|
|
231
|
+
}),
|
|
232
|
+
snapshot: captureGitSnapshot(projectRoot),
|
|
233
|
+
sensor_config_sha256: sensorConfigSha256(loaded.sensors, ids),
|
|
234
|
+
};
|
|
235
|
+
} catch (error) {
|
|
236
|
+
unavailable = error.code || error.message;
|
|
237
|
+
}
|
|
238
|
+
const binding = evaluateEvidenceBinding(evidence, expected);
|
|
239
|
+
process.stdout.write(`evidence-binding: ${binding.state}${binding.reasons.length ? ` (${binding.reasons.join('; ')})` : ''}${unavailable ? ` [current snapshot unavailable: ${unavailable}]` : ''}\n`);
|
|
240
|
+
}
|
|
206
241
|
let verdict = null;
|
|
207
242
|
try { verdict = JSON.parse(readFileSync(join(dir, 'verdict.json'), 'utf8')); } catch { /* sem verdict */ }
|
|
208
243
|
if (!verdict) process.stdout.write(`verdict: ausente — rode \`wendkeep verify --deep\`${reqIds.length ? ' + wk-verify' : ' (verdict trivial automático)'}\n`);
|
|
209
244
|
else if (!reqIds.length) process.stdout.write(`verdict: ${verdict.ok === true ? 'ok (trivial)' : 'não-ok — re-verifique'}\n`);
|
|
210
245
|
else {
|
|
211
|
-
const v = evaluateVerdict(verdict, reqIds, {
|
|
246
|
+
const v = evaluateVerdict(verdict, reqIds, {
|
|
247
|
+
tasksHash: tasksHashOf(tarefasMd),
|
|
248
|
+
effectiveSpecHash: effective.hash,
|
|
249
|
+
evidenceEnvelopeId: evidence?.schema_version === 2 ? evidence.envelope_id : undefined,
|
|
250
|
+
evidenceBinding: evidence?.schema_version === 2 ? evidenceCheckoutBinding(evidence) : undefined,
|
|
251
|
+
});
|
|
212
252
|
process.stdout.write(`verdict: ${v.ok ? 'ok' : v.stale ? 'stale — re-verifique' : `incompleto: falta ${v.missing.join(', ')}`}\n`);
|
|
213
253
|
}
|
|
214
254
|
try { process.stdout.write(`mutation-round: ${readFileSync(join(dir, '.mutation-round'), 'utf8').trim()}/3\n`); } catch { /* sem rodadas */ }
|
|
@@ -289,9 +329,34 @@ export function runChange(argv) {
|
|
|
289
329
|
const effective = buildEffectiveRequirementPackage(vaultBase, dir, reqIds);
|
|
290
330
|
if (effective.errors.length) return { ok: false, failing: [`spec efetiva inválida: ${effective.errors.join('; ')}`] };
|
|
291
331
|
if (effective.missing.length) return { ok: false, failing: [formatOrphanReqs(effective.missing)] };
|
|
292
|
-
let evidence =
|
|
332
|
+
let evidence = null;
|
|
293
333
|
try { evidence = JSON.parse(readFileSync(join(dir, 'evidencia.json'), 'utf8')); } catch { /* no evidence */ }
|
|
294
|
-
const
|
|
334
|
+
const sensorEvidence = evidenceSensors(evidence);
|
|
335
|
+
if (required.length && (!evidence || evidence.schema_version !== 2)) {
|
|
336
|
+
return { ok: false, failing: ['evidência legacy-unbound não satisfaz autoridade v2 — rode `wendkeep verify` novamente'] };
|
|
337
|
+
}
|
|
338
|
+
if (evidence?.schema_version === 2) {
|
|
339
|
+
let currentBinding;
|
|
340
|
+
try {
|
|
341
|
+
const loaded = loadSensorsDetailed(projectRoot);
|
|
342
|
+
currentBinding = evaluateEvidenceBinding(evidence, {
|
|
343
|
+
change_slug: slug,
|
|
344
|
+
identity: resolveEvidenceIdentity({
|
|
345
|
+
vaultBase, projectRoot, changeSlug: slug, sessionId, context: selectedContext,
|
|
346
|
+
}),
|
|
347
|
+
snapshot: captureGitSnapshot(projectRoot),
|
|
348
|
+
tasks_sha256: tasksHashOf(tarefasMd),
|
|
349
|
+
effective_spec_sha256: `sha256:${effective.hash}`,
|
|
350
|
+
sensor_config_sha256: sensorConfigSha256(loaded.sensors, required),
|
|
351
|
+
});
|
|
352
|
+
} catch (error) {
|
|
353
|
+
return { ok: false, failing: [`binding atual indisponível (${error.code || error.message}) — recupere o contexto e rode \`wendkeep verify\` novamente`] };
|
|
354
|
+
}
|
|
355
|
+
if (currentBinding.state !== 'bound') {
|
|
356
|
+
return { ok: false, failing: [`evidência ${currentBinding.state} (${currentBinding.reasons.join('; ')}) — rode \`wendkeep verify\` novamente`] };
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const s = evaluateGate(sensorEvidence, required);
|
|
295
360
|
if (!s.ok) return s;
|
|
296
361
|
// Verdict SEMPRE exigido (0.31.0) — a exigência universal vive AQUI no gate; a semântica
|
|
297
362
|
// reqless→ok de evaluateVerdict (spec-core) não muda porque `verify --deep` e `change
|
|
@@ -310,6 +375,19 @@ export function runChange(argv) {
|
|
|
310
375
|
}
|
|
311
376
|
let verification = null;
|
|
312
377
|
try { verification = JSON.parse(readFileSync(join(dir, 'verificacao.json'), 'utf8')); } catch { /* none */ }
|
|
378
|
+
const checkoutBinding = evidence?.schema_version === 2 ? evidenceCheckoutBinding(evidence) : null;
|
|
379
|
+
if (evidence?.schema_version === 2 && verification?.evidenceEnvelopeId !== evidence.envelope_id) {
|
|
380
|
+
return { ok: false, failing: ['pacote de verificação não está ligado ao envelope atual — rode `wendkeep verify --deep` novamente'] };
|
|
381
|
+
}
|
|
382
|
+
if (checkoutBinding && !evidenceCheckoutBindingMatches(verification?.evidenceBinding, checkoutBinding)) {
|
|
383
|
+
return { ok: false, failing: ['binding do pacote de verificação diverge do checkout provado — rode `wendkeep verify --deep` novamente'] };
|
|
384
|
+
}
|
|
385
|
+
if (evidence?.schema_version === 2 && verdict.evidenceEnvelopeId !== evidence.envelope_id) {
|
|
386
|
+
return { ok: false, failing: [`verdict não está ligado ao envelope atual — rode \`wendkeep verify --deep\`${reqIds.length ? ' + wk-verify' : ''}`] };
|
|
387
|
+
}
|
|
388
|
+
if (checkoutBinding && !evidenceCheckoutBindingMatches(verdict.evidenceBinding, checkoutBinding)) {
|
|
389
|
+
return { ok: false, failing: [`binding do verdict diverge do checkout provado — rode \`wendkeep verify --deep\`${reqIds.length ? ' + wk-verify' : ''}`] };
|
|
390
|
+
}
|
|
313
391
|
if (verification?.effectiveSpecHash && verification.effectiveSpecHash !== effective.hash) {
|
|
314
392
|
return { ok: false, failing: ['pacote de verificação stale (spec efetiva mudou) — rode `wendkeep verify --deep` novamente'] };
|
|
315
393
|
}
|
|
@@ -317,7 +395,12 @@ export function runChange(argv) {
|
|
|
317
395
|
return { ok: false, failing: ['verdict sem effectiveSpecHash — rode a skill wk-verify novamente'] };
|
|
318
396
|
}
|
|
319
397
|
if (reqIds.length) {
|
|
320
|
-
const v = evaluateVerdict(verdict, reqIds, {
|
|
398
|
+
const v = evaluateVerdict(verdict, reqIds, {
|
|
399
|
+
tasksHash: hash,
|
|
400
|
+
effectiveSpecHash: effective.hash,
|
|
401
|
+
evidenceEnvelopeId: evidence?.schema_version === 2 ? evidence.envelope_id : undefined,
|
|
402
|
+
evidenceBinding: checkoutBinding || undefined,
|
|
403
|
+
});
|
|
321
404
|
if (!v.ok) {
|
|
322
405
|
if (v.stale) return { ok: false, failing: ['verdict stale (tarefas.md mudou depois da verificação) — re-verifique: `wendkeep verify --deep` + wk-verify'] };
|
|
323
406
|
return { ok: false, failing: [`verdict incompleto: falta ${v.missing.join(', ')}`] };
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { isUtf8 } from 'node:buffer';
|
|
4
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { readProjectForValidation } from '../packages/vault/src/validate-memory.mjs';
|
|
7
|
+
import {
|
|
8
|
+
discoverWorktreeRepository,
|
|
9
|
+
readWorktreeRegistry,
|
|
10
|
+
worktreeIdentity,
|
|
11
|
+
} from '../packages/vault/src/worktree-metadata.mjs';
|
|
12
|
+
import {
|
|
13
|
+
canonicalSha256,
|
|
14
|
+
evaluateEvidenceBinding,
|
|
15
|
+
evidenceSensors,
|
|
16
|
+
} from '../packages/vault/src/evidence-envelope.mjs';
|
|
17
|
+
|
|
18
|
+
export { canonicalSha256, evaluateEvidenceBinding, evidenceSensors };
|
|
19
|
+
|
|
20
|
+
const PACKAGE_VERSION = JSON.parse(readFileSync(
|
|
21
|
+
join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'),
|
|
22
|
+
'utf8',
|
|
23
|
+
)).version;
|
|
24
|
+
|
|
25
|
+
export function sensorConfigSha256(sensors, ids) {
|
|
26
|
+
const selected = new Set(ids || []);
|
|
27
|
+
const canonical = (sensors || [])
|
|
28
|
+
.filter((sensor) => selected.has(sensor.id))
|
|
29
|
+
.map((sensor) => sensor)
|
|
30
|
+
.sort((left, right) => String(left.id || '').localeCompare(String(right.id || '')));
|
|
31
|
+
return canonicalSha256(canonical);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizedPath(value) {
|
|
35
|
+
return String(value || '').replaceAll('\\', '/');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const BINARY_EXTENSIONS = new Set([
|
|
39
|
+
'.7z', '.a', '.avi', '.bin', '.bmp', '.class', '.dll', '.doc', '.docx', '.dylib', '.eot',
|
|
40
|
+
'.exe', '.gif', '.gz', '.ico', '.jar', '.jpeg', '.jpg', '.mov', '.mp3', '.mp4', '.o',
|
|
41
|
+
'.ogg', '.otf', '.pdf', '.png', '.so', '.tar', '.tif', '.tiff', '.ttf', '.wav', '.webm',
|
|
42
|
+
'.webp', '.woff', '.woff2', '.xls', '.xlsx', '.zip',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
function normalizedContent(content, binary = false) {
|
|
46
|
+
const bytes = Buffer.isBuffer(content) ? content : Buffer.from(content || '');
|
|
47
|
+
if (binary || bytes.includes(0) || !isUtf8(bytes)) return bytes;
|
|
48
|
+
return Buffer.from(bytes.toString('utf8').replace(/\r\n?/g, '\n'), 'utf8');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function digestWorktreeEntries(entries) {
|
|
52
|
+
const canonical = (entries || []).map((entry) => ({
|
|
53
|
+
layer: String(entry.layer || ''),
|
|
54
|
+
status: String(entry.status || ''),
|
|
55
|
+
path: normalizedPath(entry.path),
|
|
56
|
+
...(entry.oldPath ? { old_path: normalizedPath(entry.oldPath) } : {}),
|
|
57
|
+
content_mode: entry.binary ? 'binary' : 'text',
|
|
58
|
+
content_sha256: entry.content == null ? null : canonicalSha256(normalizedContent(entry.content, entry.binary)),
|
|
59
|
+
})).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
|
60
|
+
return canonicalSha256(canonical);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function gitResult(projectRoot, args, { spawn = spawnSync, allowFailure = false } = {}) {
|
|
64
|
+
const result = spawn('git', args, {
|
|
65
|
+
cwd: projectRoot,
|
|
66
|
+
encoding: null,
|
|
67
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
68
|
+
windowsHide: true,
|
|
69
|
+
});
|
|
70
|
+
if (!allowFailure && (result.error || result.status !== 0)) {
|
|
71
|
+
const detail = Buffer.from(result.stderr || '').toString('utf8').trim();
|
|
72
|
+
const error = new Error(`git ${args.join(' ')} falhou${detail ? `: ${detail}` : ''}`);
|
|
73
|
+
error.code = 'WENDKEEP_EVIDENCE_GIT_FAILED';
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function gitText(projectRoot, args, options) {
|
|
80
|
+
const result = gitResult(projectRoot, args, options);
|
|
81
|
+
if (result.status !== 0) return '';
|
|
82
|
+
return Buffer.from(result.stdout || '').toString('utf8').trim();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function gitBuffer(projectRoot, args, options) {
|
|
86
|
+
const result = gitResult(projectRoot, args, options);
|
|
87
|
+
return result.status === 0 ? Buffer.from(result.stdout || '') : Buffer.alloc(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parseNameStatus(buffer, layer) {
|
|
91
|
+
const tokens = buffer.toString('utf8').split('\0');
|
|
92
|
+
if (tokens.at(-1) === '') tokens.pop();
|
|
93
|
+
const entries = [];
|
|
94
|
+
for (let index = 0; index < tokens.length;) {
|
|
95
|
+
const status = tokens[index++];
|
|
96
|
+
const renamed = /^[RC]/.test(status);
|
|
97
|
+
const oldPath = renamed ? tokens[index++] : '';
|
|
98
|
+
const path = tokens[index++];
|
|
99
|
+
if (!path) continue;
|
|
100
|
+
entries.push({ layer, status, path, ...(oldPath ? { oldPath } : {}) });
|
|
101
|
+
}
|
|
102
|
+
return entries;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function binaryAttributes(projectRoot, paths, options) {
|
|
106
|
+
const unique = [...new Set(paths.filter(Boolean))];
|
|
107
|
+
if (!unique.length) return new Map();
|
|
108
|
+
const tokens = gitBuffer(projectRoot, ['check-attr', '-z', 'binary', 'text', '--', ...unique], {
|
|
109
|
+
...options, allowFailure: true,
|
|
110
|
+
}).toString('utf8').split('\0');
|
|
111
|
+
if (tokens.at(-1) === '') tokens.pop();
|
|
112
|
+
const attributes = new Map();
|
|
113
|
+
for (let index = 0; index + 2 < tokens.length; index += 3) {
|
|
114
|
+
const [path, attribute, value] = tokens.slice(index, index + 3);
|
|
115
|
+
const entry = attributes.get(path) || {};
|
|
116
|
+
entry[attribute] = value;
|
|
117
|
+
attributes.set(path, entry);
|
|
118
|
+
}
|
|
119
|
+
return attributes;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function pathIsBinary(path, attributes) {
|
|
123
|
+
const values = attributes.get(path) || {};
|
|
124
|
+
if (values.binary === 'set' || values.text === 'unset') return true;
|
|
125
|
+
if (values.binary === 'unset' || values.text === 'set' || values.text === 'auto') return false;
|
|
126
|
+
const normalized = normalizedPath(path).toLowerCase();
|
|
127
|
+
const dot = normalized.lastIndexOf('.');
|
|
128
|
+
return dot >= 0 && BINARY_EXTENSIONS.has(normalized.slice(dot));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function readWorkingPath(projectRoot, path) {
|
|
132
|
+
const root = resolve(projectRoot);
|
|
133
|
+
const absolute = resolve(root, ...normalizedPath(path).split('/'));
|
|
134
|
+
const scoped = relative(root, absolute);
|
|
135
|
+
if (scoped === '..' || scoped.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) || isAbsolute(scoped)) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
if (!existsSync(absolute) || !statSync(absolute).isFile()) return null;
|
|
140
|
+
return readFileSync(absolute);
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function changedEntries(projectRoot, options) {
|
|
147
|
+
const staged = parseNameStatus(gitBuffer(projectRoot, [
|
|
148
|
+
'diff', '--cached', '--name-status', '-z', '--find-renames', '--no-ext-diff',
|
|
149
|
+
], options), 'index');
|
|
150
|
+
const unstaged = parseNameStatus(gitBuffer(projectRoot, [
|
|
151
|
+
'diff', '--name-status', '-z', '--find-renames', '--no-ext-diff',
|
|
152
|
+
], options), 'worktree');
|
|
153
|
+
const untracked = gitBuffer(projectRoot, [
|
|
154
|
+
'ls-files', '--others', '--exclude-standard', '-z',
|
|
155
|
+
], options).toString('utf8').split('\0').filter(Boolean).map((path) => ({
|
|
156
|
+
layer: 'untracked', status: '?', path,
|
|
157
|
+
}));
|
|
158
|
+
const attributes = binaryAttributes(projectRoot, [
|
|
159
|
+
...staged.map((entry) => entry.path),
|
|
160
|
+
...unstaged.map((entry) => entry.path),
|
|
161
|
+
...untracked.map((entry) => entry.path),
|
|
162
|
+
], options);
|
|
163
|
+
|
|
164
|
+
for (const entry of staged) {
|
|
165
|
+
entry.binary = pathIsBinary(entry.path, attributes);
|
|
166
|
+
entry.content = /^D/.test(entry.status)
|
|
167
|
+
? null
|
|
168
|
+
: gitBuffer(projectRoot, ['show', `:${entry.path}`], { ...options, allowFailure: true });
|
|
169
|
+
}
|
|
170
|
+
for (const entry of [...unstaged, ...untracked]) {
|
|
171
|
+
entry.binary = pathIsBinary(entry.path, attributes);
|
|
172
|
+
entry.content = /^D/.test(entry.status) ? null : readWorkingPath(projectRoot, entry.path);
|
|
173
|
+
}
|
|
174
|
+
return [...staged, ...unstaged, ...untracked];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function resolveBaseSha(projectRoot, headSha, options) {
|
|
178
|
+
const upstream = gitText(projectRoot, ['rev-parse', '--verify', '@{upstream}'], {
|
|
179
|
+
...options, allowFailure: true,
|
|
180
|
+
});
|
|
181
|
+
let candidate = upstream;
|
|
182
|
+
if (!candidate) {
|
|
183
|
+
candidate = gitText(projectRoot, ['rev-parse', '--verify', 'refs/heads/main'], {
|
|
184
|
+
...options, allowFailure: true,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (!candidate) return headSha;
|
|
188
|
+
return gitText(projectRoot, ['merge-base', headSha, candidate], {
|
|
189
|
+
...options, allowFailure: true,
|
|
190
|
+
}) || headSha;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function captureGitSnapshot(projectRoot, { spawn = spawnSync } = {}) {
|
|
194
|
+
const options = { spawn };
|
|
195
|
+
const headSha = gitText(projectRoot, ['rev-parse', 'HEAD'], options);
|
|
196
|
+
const branch = gitText(projectRoot, ['symbolic-ref', '--short', '-q', 'HEAD'], {
|
|
197
|
+
...options, allowFailure: true,
|
|
198
|
+
}) || 'HEAD';
|
|
199
|
+
const indexTreeSha = gitText(projectRoot, ['write-tree'], options);
|
|
200
|
+
const entries = changedEntries(projectRoot, options);
|
|
201
|
+
return {
|
|
202
|
+
branch,
|
|
203
|
+
base_sha: resolveBaseSha(projectRoot, headSha, options),
|
|
204
|
+
head_sha: headSha,
|
|
205
|
+
index_tree_sha: indexTreeSha,
|
|
206
|
+
worktree_digest: digestWorktreeEntries(entries),
|
|
207
|
+
dirty: entries.length > 0,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function resolveEvidenceIdentity({
|
|
212
|
+
vaultBase,
|
|
213
|
+
projectRoot,
|
|
214
|
+
changeSlug,
|
|
215
|
+
sessionId = '',
|
|
216
|
+
context = null,
|
|
217
|
+
spawn = spawnSync,
|
|
218
|
+
} = {}) {
|
|
219
|
+
const project = readProjectForValidation(vaultBase);
|
|
220
|
+
const repository = discoverWorktreeRepository({ startDir: projectRoot, spawn });
|
|
221
|
+
const { registry } = readWorktreeRegistry(repository);
|
|
222
|
+
if (registry && project.ok && registry.projectId !== project.projectId) {
|
|
223
|
+
const error = new Error('PROJECT.json e registry de worktrees pertencem a projetos diferentes');
|
|
224
|
+
error.code = 'WENDKEEP_EVIDENCE_IDENTITY_MISMATCH';
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
const repositoryId = context?.repositoryId
|
|
228
|
+
|| registry?.repositoryId
|
|
229
|
+
|| canonicalSha256({ git_common_dir: normalizedPath(repository.commonDir).toLowerCase() });
|
|
230
|
+
const projectId = context?.projectId
|
|
231
|
+
|| (project.ok ? project.projectId : canonicalSha256({ repository_id: repositoryId }));
|
|
232
|
+
const worktreeId = context?.worktreeId || worktreeIdentity(repositoryId, repository.gitDir);
|
|
233
|
+
const requestedSession = String(sessionId || '').trim();
|
|
234
|
+
const workSessionId = context?.workSessionId
|
|
235
|
+
|| requestedSession
|
|
236
|
+
|| canonicalSha256({ project_id: projectId, worktree_id: worktreeId, change_slug: changeSlug });
|
|
237
|
+
return {
|
|
238
|
+
project_id: projectId,
|
|
239
|
+
repository_id: repositoryId,
|
|
240
|
+
worktree_id: worktreeId,
|
|
241
|
+
work_session_id: workSessionId,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function assertStableHead(startSnapshot, finishSnapshot) {
|
|
246
|
+
if (startSnapshot?.head_sha !== finishSnapshot?.head_sha) {
|
|
247
|
+
const error = new Error(
|
|
248
|
+
`HEAD mudou durante verify (${startSnapshot?.head_sha || 'ausente'} -> ${finishSnapshot?.head_sha || 'ausente'}); rode novamente no commit estável`,
|
|
249
|
+
);
|
|
250
|
+
error.code = 'WENDKEEP_EVIDENCE_HEAD_CHANGED';
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function buildEvidenceEnvelope({
|
|
256
|
+
identity,
|
|
257
|
+
changeSlug,
|
|
258
|
+
snapshot,
|
|
259
|
+
tasksSha256,
|
|
260
|
+
effectiveSpecSha256,
|
|
261
|
+
sensorConfigSha256: configSha256,
|
|
262
|
+
sensors,
|
|
263
|
+
startedAt,
|
|
264
|
+
finishedAt,
|
|
265
|
+
version = PACKAGE_VERSION,
|
|
266
|
+
runtimePlatform = `${process.platform}-${process.arch}`,
|
|
267
|
+
} = {}) {
|
|
268
|
+
const envelope = {
|
|
269
|
+
schema_version: 2,
|
|
270
|
+
...identity,
|
|
271
|
+
change_slug: changeSlug,
|
|
272
|
+
branch: snapshot.branch,
|
|
273
|
+
base_sha: snapshot.base_sha,
|
|
274
|
+
head_sha: snapshot.head_sha,
|
|
275
|
+
index_tree_sha: snapshot.index_tree_sha,
|
|
276
|
+
worktree_digest: snapshot.worktree_digest,
|
|
277
|
+
dirty: snapshot.dirty,
|
|
278
|
+
tasks_sha256: tasksSha256,
|
|
279
|
+
effective_spec_sha256: effectiveSpecSha256,
|
|
280
|
+
sensor_config_sha256: configSha256,
|
|
281
|
+
wendkeep_version: version,
|
|
282
|
+
platform: runtimePlatform,
|
|
283
|
+
started_at: startedAt,
|
|
284
|
+
finished_at: finishedAt,
|
|
285
|
+
sensors,
|
|
286
|
+
};
|
|
287
|
+
return { ...envelope, envelope_id: canonicalSha256(envelope) };
|
|
288
|
+
}
|
package/src/skills-seed.mjs
CHANGED
|
@@ -281,7 +281,8 @@ nunca tivesse visto a implementação. Contexto fresco, read-only.
|
|
|
281
281
|
(isolamento real). Nos outros, entre num contexto limpo e re-derive do spec, não da memória.
|
|
282
282
|
- \`ok: false\` se algum requisito não tem cobertura que discrimina. Gap não é "quase lá" — é vermelho.
|
|
283
283
|
- Não conserte aqui. Gap vira tarefa de correção na change; re-verifica depois.
|
|
284
|
-
- O gate do \`archive\` **exige** \`verdict.json\` com \`ok\` cobrindo todo \`[req:]
|
|
284
|
+
- O gate do \`archive\` **exige** \`verdict.json\` com \`ok\` cobrindo todo \`[req:]\` e copiando
|
|
285
|
+
\`evidenceEnvelopeId\` + \`evidenceBinding\` de \`verificacao.json\`. Sem isso, não arquiva.
|
|
285
286
|
|
|
286
287
|
## Templates (nesta pasta)
|
|
287
288
|
- \`spec-reviewer-prompt.md\` — cole ao spawnar o subagente verificador (read-only, autor≠verificador).
|
|
@@ -489,7 +490,8 @@ the author — even if you wrote the code, enter as if you'd never seen it. Fres
|
|
|
489
490
|
- **Author ≠ verifier.** On Claude, spawn a read-only sub-agent for real isolation.
|
|
490
491
|
- \`ok: false\` if any requirement lacks discriminating coverage. A gap is red, not "almost".
|
|
491
492
|
- Don't fix here — a gap becomes a fix task; re-verify after.
|
|
492
|
-
- The archive gate **requires** a fresh \`verdict.json\`
|
|
493
|
+
- The archive gate **requires** a fresh \`verdict.json\` matching \`tasksHash\`,
|
|
494
|
+
\`effectiveSpecHash\`, \`evidenceEnvelopeId\`, and \`evidenceBinding\`, covering every \`[req:]\`.
|
|
493
495
|
|
|
494
496
|
## Templates (in this folder)
|
|
495
497
|
- \`spec-reviewer-prompt.md\` — hand it to the verifier sub-agent you spawn (read-only, author≠verifier).
|
|
@@ -507,6 +509,8 @@ const VERDICT_TEMPLATE = `{
|
|
|
507
509
|
],
|
|
508
510
|
"tasksHash": "<copie de verificacao.json — selo de frescor / copy from verificacao.json — freshness seal>",
|
|
509
511
|
"effectiveSpecHash": "<copie de verificacao.json / copy from verificacao.json>",
|
|
512
|
+
"evidenceEnvelopeId": "<copie de verificacao.json / copy from verificacao.json>",
|
|
513
|
+
"evidenceBinding": "<copie o objeto de verificacao.json / copy the object from verificacao.json>",
|
|
510
514
|
"notes": []
|
|
511
515
|
}
|
|
512
516
|
`;
|
|
@@ -532,7 +536,8 @@ Para cada \`[req:ID]\` da mudança:
|
|
|
532
536
|
|
|
533
537
|
Grave \`08-Mudanças/<slug>/verdict.json\` no formato de \`verdict-template.json\`. \`ok: false\` se
|
|
534
538
|
qualquer \`[req:]\` não tem cobertura que discrimina. Não conserte aqui — gap vira tarefa de
|
|
535
|
-
correção. \`tasksHash\` e \`
|
|
539
|
+
correção. \`tasksHash\`, \`effectiveSpecHash\`, \`evidenceEnvelopeId\` e \`evidenceBinding\` vêm do
|
|
540
|
+
pacote; alterações posteriores deixam o verdict stale e o binding nunca deve ser reconstruído à mão.
|
|
536
541
|
---
|
|
537
542
|
`;
|
|
538
543
|
|
|
@@ -556,8 +561,9 @@ For each \`[req:ID]\`:
|
|
|
556
561
|
4. Check the observable result against the criterion — not the code.
|
|
557
562
|
|
|
558
563
|
Write \`08-Changes/<slug>/verdict.json\` in the shape of \`verdict-template.json\`. \`ok: false\` if any
|
|
559
|
-
\`[req:]\` lacks discriminating coverage. Don't fix here — a gap becomes a fix task. \`tasksHash
|
|
560
|
-
and \`
|
|
564
|
+
\`[req:]\` lacks discriminating coverage. Don't fix here — a gap becomes a fix task. \`tasksHash\`,
|
|
565
|
+
\`effectiveSpecHash\`, \`evidenceEnvelopeId\`, and \`evidenceBinding\` are copied from the package;
|
|
566
|
+
never reconstruct the binding by hand (later checkout/task/spec edits make the verdict stale).
|
|
561
567
|
---
|
|
562
568
|
`;
|
|
563
569
|
|