wendkeep 0.77.0 → 0.79.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 +64 -0
- package/README.en.md +58 -3
- package/README.md +58 -3
- package/docs/en/commands/changes-and-verification.md +74 -2
- package/docs/en/commands/operating-profiles.md +49 -5
- package/docs/en/commands/verify.md +67 -5
- package/docs/en/commands/worktrees.md +39 -4
- package/docs/pt-BR/commands/changes-and-verification.md +73 -2
- package/docs/pt-BR/commands/operating-profiles.md +51 -5
- package/docs/pt-BR/commands/verify.md +67 -6
- package/docs/pt-BR/commands/worktrees.md +38 -3
- package/hooks/active-context-store.mjs +530 -2
- package/hooks/change-core.mjs +203 -123
- package/hooks/harness-doctor.mjs +51 -1
- package/hooks/obsidian-common.mjs +175 -9
- package/hooks/spec-core.mjs +118 -36
- package/package.json +2 -2
- 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/schema/wendkeep.provenance-receipt-v2.schema.json +66 -0
- package/src/archive-operation-lock.mjs +235 -0
- package/src/change.mjs +1832 -48
- package/src/delivery.mjs +724 -67
- package/src/evidence-envelope.mjs +288 -0
- package/src/memory.mjs +2 -1
- package/src/provenance-gate.mjs +575 -0
- package/src/provenance-sources.mjs +547 -0
- package/src/receipt-ledger.mjs +841 -0
- package/src/release-provenance.mjs +48 -0
- package/src/skills-seed.mjs +11 -5
- package/src/verify.mjs +85 -22
- package/src/worktree-cleanup.mjs +1733 -118
- package/src/worktree.mjs +94 -5
|
@@ -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/memory.mjs
CHANGED
|
@@ -1889,7 +1889,7 @@ function legacyCheckpointMigration(vault, sessionId, entry, authority, fullRepla
|
|
|
1889
1889
|
}
|
|
1890
1890
|
|
|
1891
1891
|
export function migrateLegacyMemoryCheckpoints(vault, {
|
|
1892
|
-
now = new Date().toISOString(), memoryLock = {},
|
|
1892
|
+
now = new Date().toISOString(), memoryLock = {}, beforeRegistryMutation,
|
|
1893
1893
|
} = {}) {
|
|
1894
1894
|
const expectedAuthority = readMemoryAuthority(vault);
|
|
1895
1895
|
const outcome = withMemoryLock(vault, () => {
|
|
@@ -1910,6 +1910,7 @@ export function migrateLegacyMemoryCheckpoints(vault, {
|
|
|
1910
1910
|
status: 'unchanged', migrated: 0, sessions: [], backupPath: null,
|
|
1911
1911
|
};
|
|
1912
1912
|
|
|
1913
|
+
if (beforeRegistryMutation) beforeRegistryMutation();
|
|
1913
1914
|
let backupPath = null;
|
|
1914
1915
|
let backupCreated = false;
|
|
1915
1916
|
try {
|