wendkeep 0.78.0 → 0.80.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.en.md +58 -3
  3. package/README.md +58 -3
  4. package/docs/en/commands/changes-and-verification.md +116 -1
  5. package/docs/en/commands/operating-profiles.md +49 -5
  6. package/docs/en/commands/sessions-and-import.md +6 -0
  7. package/docs/en/commands/verify.md +54 -0
  8. package/docs/en/commands/worktrees.md +39 -4
  9. package/docs/pt-BR/commands/changes-and-verification.md +115 -1
  10. package/docs/pt-BR/commands/operating-profiles.md +51 -5
  11. package/docs/pt-BR/commands/sessions-and-import.md +7 -0
  12. package/docs/pt-BR/commands/verify.md +53 -0
  13. package/docs/pt-BR/commands/worktrees.md +38 -3
  14. package/hooks/active-context-store.mjs +530 -2
  15. package/hooks/change-core.mjs +220 -123
  16. package/hooks/obsidian-common.mjs +175 -9
  17. package/hooks/session-stop.mjs +40 -1
  18. package/hooks/spec-core.mjs +93 -29
  19. package/package.json +2 -2
  20. package/packages/cli/src/index.mjs +7 -0
  21. package/packages/vault/src/memory-handoff.mjs +15 -0
  22. package/schema/artifact-manifest-v1.schema.json +35 -0
  23. package/schema/handoff-contract-v1.schema.json +37 -0
  24. package/schema/task-contract-v1.schema.json +57 -0
  25. package/schema/wendkeep.provenance-receipt-v2.schema.json +66 -0
  26. package/src/archive-operation-lock.mjs +235 -0
  27. package/src/change.mjs +1780 -79
  28. package/src/delivery.mjs +724 -67
  29. package/src/memory.mjs +2 -1
  30. package/src/provenance-gate.mjs +575 -0
  31. package/src/provenance-sources.mjs +547 -0
  32. package/src/receipt-ledger.mjs +841 -0
  33. package/src/release-provenance.mjs +48 -0
  34. package/src/task-contracts.mjs +510 -0
  35. package/src/task-leases.mjs +105 -0
  36. package/src/task.mjs +115 -0
  37. package/src/verify.mjs +32 -0
  38. package/src/worktree-cleanup.mjs +1733 -118
  39. package/src/worktree.mjs +94 -5
@@ -0,0 +1,235 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import {
3
+ closeSync, constants, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync,
4
+ readFileSync, readdirSync, realpathSync, renameSync, rmdirSync, unlinkSync, writeFileSync,
5
+ } from 'node:fs';
6
+ import { basename, dirname, join, resolve } from 'node:path';
7
+
8
+ function archiveLockError(code, details = {}) {
9
+ const error = new Error(code);
10
+ error.code = code;
11
+ Object.assign(error, details);
12
+ return error;
13
+ }
14
+
15
+ function samePhysicalFile(left, right) {
16
+ if (!left?.isFile() || !right?.isFile() || left.ino !== right.ino) return false;
17
+ if (left.dev === right.dev) return true;
18
+ return process.platform === 'win32' && (left.dev === 0 || right.dev === 0);
19
+ }
20
+
21
+ function ownerProcessAlive(pid) {
22
+ try { process.kill(pid, 0); return true; }
23
+ catch (error) { return !['ESRCH', 'EINVAL'].includes(error?.code); }
24
+ }
25
+
26
+ function assertLockDirectory(target) {
27
+ const stat = lstatSync(target);
28
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
29
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_UNAVAILABLE');
30
+ }
31
+ return stat;
32
+ }
33
+
34
+ function readOwnerMarker(markerPath) {
35
+ const stat = lstatSync(markerPath);
36
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) {
37
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_UNAVAILABLE');
38
+ }
39
+ let record;
40
+ try { record = JSON.parse(readFileSync(markerPath, 'utf8')); }
41
+ catch { throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_UNAVAILABLE'); }
42
+ if (record?.schema_version !== 1
43
+ || typeof record.owner_token !== 'string' || !record.owner_token
44
+ || !Number.isSafeInteger(record.owner_pid) || record.owner_pid <= 0
45
+ || typeof record.acquired_at !== 'string' || !Number.isFinite(Date.parse(record.acquired_at))) {
46
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_UNAVAILABLE');
47
+ }
48
+ return { stat, record };
49
+ }
50
+
51
+ function inspectExistingLock(target) {
52
+ try { assertLockDirectory(target); }
53
+ catch (error) {
54
+ if (error?.code === 'ENOENT') return;
55
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_UNAVAILABLE');
56
+ }
57
+ const entries = readdirSync(target);
58
+ if (entries.length === 0) {
59
+ try { rmdirSync(target); } catch { /* re-observe on the next bounded attempt */ }
60
+ return;
61
+ }
62
+ if (entries.length !== 1 || !/^owner\.[0-9a-f-]+\.json$/i.test(entries[0])) {
63
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_UNAVAILABLE');
64
+ }
65
+ const markerPath = join(target, entries[0]);
66
+ const observed = readOwnerMarker(markerPath);
67
+ if (basename(markerPath) !== `owner.${observed.record.owner_token}.json`) {
68
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_UNAVAILABLE');
69
+ }
70
+ if (ownerProcessAlive(observed.record.owner_pid)) {
71
+ throw archiveLockError('WENDKEEP_ARCHIVE_BUSY', { owner_state: 'live' });
72
+ }
73
+ const confirmed = readOwnerMarker(markerPath);
74
+ if (!samePhysicalFile(observed.stat, confirmed.stat)
75
+ || confirmed.record.owner_token !== observed.record.owner_token
76
+ || confirmed.record.owner_pid !== observed.record.owner_pid) {
77
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_UNAVAILABLE');
78
+ }
79
+ try { unlinkSync(markerPath); }
80
+ catch { throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_UNAVAILABLE'); }
81
+ try { rmdirSync(target); } catch { /* replacement/nonempty directory remains authoritative */ }
82
+ }
83
+
84
+ export function acquireArchiveOperationLock({
85
+ lockPath,
86
+ now = () => Date.now(),
87
+ maxAcquireAttempts = 3,
88
+ faultInjection = {},
89
+ }) {
90
+ if (!lockPath || !Number.isSafeInteger(maxAcquireAttempts) || maxAcquireAttempts < 1) {
91
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_INVALID');
92
+ }
93
+ const runtime = resolve(dirname(lockPath));
94
+ const target = resolve(lockPath);
95
+ mkdirSync(runtime, { recursive: true });
96
+ const runtimeStat = lstatSync(runtime);
97
+ if (!runtimeStat.isDirectory() || runtimeStat.isSymbolicLink()
98
+ || resolve(realpathSync(runtime)).toLowerCase() !== runtime.toLowerCase()) {
99
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_UNAVAILABLE');
100
+ }
101
+
102
+ const token = randomUUID();
103
+ const markerName = `owner.${token}.json`;
104
+ const pending = `${target}.pending.${process.pid}.${token}`;
105
+ const pendingMarker = join(pending, markerName);
106
+ let descriptor;
107
+ let descriptorIdentity;
108
+ let pendingExists = false;
109
+ let published = false;
110
+ try {
111
+ mkdirSync(pending);
112
+ pendingExists = true;
113
+ descriptor = openSync(
114
+ pendingMarker,
115
+ constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0),
116
+ 0o600,
117
+ );
118
+ writeFileSync(descriptor, `${JSON.stringify({
119
+ schema_version: 1,
120
+ owner_token: token,
121
+ owner_pid: process.pid,
122
+ acquired_at: new Date(now()).toISOString(),
123
+ })}\n`, 'utf8');
124
+ fsyncSync(descriptor);
125
+ closeSync(descriptor);
126
+ descriptor = undefined;
127
+
128
+ for (let attempt = 1; attempt <= maxAcquireAttempts; attempt += 1) {
129
+ try {
130
+ if (typeof faultInjection?.beforePublishRename === 'function') {
131
+ faultInjection.beforePublishRename({ attempt, pending, lockPath: target });
132
+ }
133
+ // The pending directory is already nonempty and durable. Rename publishes metadata and
134
+ // exclusivity together; the canonical namespace never observes a partial marker/hardlink.
135
+ renameSync(pending, target);
136
+ pendingExists = false;
137
+ published = true;
138
+ if (typeof faultInjection?.afterPublishRename === 'function') {
139
+ faultInjection.afterPublishRename({ attempt, lockPath: target });
140
+ }
141
+ break;
142
+ } catch (error) {
143
+ if (published) throw error;
144
+ if (existsSync(target)) inspectExistingLock(target);
145
+ if (attempt === maxAcquireAttempts) {
146
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_UNAVAILABLE');
147
+ }
148
+ }
149
+ }
150
+
151
+ const markerPath = join(target, markerName);
152
+ descriptor = openSync(
153
+ markerPath,
154
+ constants.O_RDONLY | (constants.O_NOFOLLOW || 0),
155
+ );
156
+ const identity = readOwnerMarker(markerPath);
157
+ descriptorIdentity = fstatSync(descriptor);
158
+ if (!samePhysicalFile(descriptorIdentity, identity.stat)
159
+ || identity.record.owner_token !== token) {
160
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_OWNERSHIP_LOST');
161
+ }
162
+ } catch (error) {
163
+ if (descriptor !== undefined) {
164
+ try { closeSync(descriptor); } catch { /* original code remains authoritative */ }
165
+ descriptor = undefined;
166
+ }
167
+ if (pendingExists) {
168
+ try { unlinkSync(pendingMarker); } catch { /* marker may be absent */ }
169
+ try { rmdirSync(pending); } catch { /* private pending state is recoverable */ }
170
+ }
171
+ if (error?.code?.startsWith('WENDKEEP_ARCHIVE_')) throw error;
172
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_UNAVAILABLE');
173
+ }
174
+
175
+ const markerPath = join(target, markerName);
176
+ let terminal = false;
177
+ let assertionCount = 0;
178
+ const closeHandle = () => {
179
+ if (descriptor === undefined) return;
180
+ try { closeSync(descriptor); } finally { descriptor = undefined; }
181
+ };
182
+ const assertOwned = () => {
183
+ if (terminal || descriptor === undefined) {
184
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_OWNERSHIP_LOST');
185
+ }
186
+ try { assertLockDirectory(target); }
187
+ catch { throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_OWNERSHIP_LOST'); }
188
+ let identity;
189
+ try { identity = readOwnerMarker(markerPath); }
190
+ catch { throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_OWNERSHIP_LOST'); }
191
+ const currentDescriptorIdentity = fstatSync(descriptor);
192
+ if (!samePhysicalFile(currentDescriptorIdentity, identity.stat)
193
+ || identity.record.owner_token !== token) {
194
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_OWNERSHIP_LOST');
195
+ }
196
+ descriptorIdentity = currentDescriptorIdentity;
197
+ assertionCount += 1;
198
+ if (typeof faultInjection?.afterAssertOwned === 'function') {
199
+ faultInjection.afterAssertOwned({ count: assertionCount, lockPath: target, markerPath });
200
+ }
201
+ return true;
202
+ };
203
+
204
+ return Object.freeze({
205
+ token,
206
+ assertOwned,
207
+ release() {
208
+ if (terminal) return;
209
+ try {
210
+ assertOwned();
211
+ if (typeof faultInjection?.beforeReleaseCommit === 'function') {
212
+ faultInjection.beforeReleaseCommit({ lockPath: target, markerPath });
213
+ }
214
+ closeHandle();
215
+ const finalIdentity = readOwnerMarker(markerPath);
216
+ if (!samePhysicalFile(descriptorIdentity, finalIdentity.stat)
217
+ || finalIdentity.record.owner_token !== token) {
218
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_OWNERSHIP_LOST');
219
+ }
220
+ unlinkSync(markerPath);
221
+ rmdirSync(target);
222
+ } catch {
223
+ terminal = true;
224
+ closeHandle();
225
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_OWNERSHIP_LOST');
226
+ }
227
+ terminal = true;
228
+ closeHandle();
229
+ },
230
+ get active() {
231
+ if (terminal) return false;
232
+ try { return assertOwned(); } catch { return false; }
233
+ },
234
+ });
235
+ }