wendkeep 0.76.3 → 0.76.5
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 +24 -0
- package/README.en.md +10 -5
- package/README.md +10 -5
- package/docs/en/commands/changes-and-verification.md +12 -5
- package/docs/en/commands/context.md +16 -0
- package/docs/en/commands/operating-profiles.md +13 -4
- package/docs/pt-BR/commands/changes-and-verification.md +12 -5
- package/docs/pt-BR/commands/context.md +16 -0
- package/docs/pt-BR/commands/operating-profiles.md +13 -4
- package/hooks/active-context-store.mjs +271 -0
- package/hooks/change-context.mjs +11 -2
- package/hooks/change-core.mjs +35 -17
- package/hooks/change-warn.mjs +12 -3
- package/hooks/obsidian-common.mjs +5 -2
- package/package.json +1 -1
- package/packages/cli/src/index.mjs +3 -2
- package/src/active-context-runtime.mjs +131 -0
- package/src/change.mjs +39 -10
- package/src/delivery.mjs +134 -22
- package/src/spec.mjs +15 -2
- package/src/verify.mjs +15 -1
package/src/delivery.mjs
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
3
3
|
import { basename, join, resolve } from 'node:path';
|
|
4
4
|
import {
|
|
5
5
|
assertVaultPathSafe, mkdirVaultPath, writeVaultFileSync,
|
|
6
6
|
} from '../hooks/vault-path-safety.mjs';
|
|
7
7
|
import { resolveProjectVault } from './project-vault.mjs';
|
|
8
8
|
import { createWorkRoute } from './work-kind.mjs';
|
|
9
|
+
import {
|
|
10
|
+
activeContextKey,
|
|
11
|
+
clearActiveContextDelivery,
|
|
12
|
+
resolveActiveContext,
|
|
13
|
+
setActiveContextDelivery,
|
|
14
|
+
} from '../hooks/active-context-store.mjs';
|
|
15
|
+
import { resolveCommandActiveContext } from './active-context-runtime.mjs';
|
|
9
16
|
|
|
10
17
|
export const DELIVERY_HELP = `wendkeep delivery <subcommand>
|
|
11
18
|
|
|
@@ -15,7 +22,7 @@ export const DELIVERY_HELP = `wendkeep delivery <subcommand>
|
|
|
15
22
|
[--npm-integrity <sha512-...>] [--release-url <url>]
|
|
16
23
|
abandon [id] --reason <text>
|
|
17
24
|
|
|
18
|
-
Common options: --project <path> --vault <path> --json
|
|
25
|
+
Common options: --project <path> --vault <path> --session <id> --json
|
|
19
26
|
Delivery authorizes operational risk and creates an append-only receipt. It never creates a change,
|
|
20
27
|
spec, or ADR. If code/config must change, abandon or pause delivery and resume an implementation.
|
|
21
28
|
`;
|
|
@@ -26,7 +33,7 @@ export const DELIVERY_CAPABILITIES = Object.freeze([
|
|
|
26
33
|
|
|
27
34
|
const VALUE_OPTIONS = new Set([
|
|
28
35
|
'--project', '--vault', '--allow', '--source-change', '--source-commit', '--target',
|
|
29
|
-
'--ci-url', '--version', '--npm-integrity', '--release-url', '--reason',
|
|
36
|
+
'--ci-url', '--version', '--npm-integrity', '--release-url', '--reason', '--session',
|
|
30
37
|
]);
|
|
31
38
|
|
|
32
39
|
function parseArgv(argv) {
|
|
@@ -95,11 +102,49 @@ function readPointer(vaultBase) {
|
|
|
95
102
|
try { return safeId(readFileSync(pointer, 'utf8').trim()); } catch { return ''; }
|
|
96
103
|
}
|
|
97
104
|
|
|
98
|
-
|
|
99
|
-
const
|
|
105
|
+
function deliveryContextError(message) {
|
|
106
|
+
const error = new Error(message);
|
|
107
|
+
error.code = 'WENDKEEP_DELIVERY_CONTEXT_MISMATCH';
|
|
108
|
+
return error;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function deliveryContextBusy(id) {
|
|
112
|
+
const error = new Error(`active context já possui delivery ativo: ${id}`);
|
|
113
|
+
error.code = 'WENDKEEP_DELIVERY_CONTEXT_BUSY';
|
|
114
|
+
return error;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function contextualBinding(vaultBase, context, expectedId = '') {
|
|
118
|
+
const binding = resolveActiveContext(vaultBase, context);
|
|
119
|
+
const id = String(binding.delivery_id || '').trim();
|
|
120
|
+
if (expectedId && id !== expectedId) {
|
|
121
|
+
throw deliveryContextError(`delivery ${expectedId} não pertence ao active context chamador`);
|
|
122
|
+
}
|
|
123
|
+
return { binding, id, key: activeContextKey(context) };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function assertStateContext(state, binding) {
|
|
127
|
+
if (!binding) return;
|
|
128
|
+
if (state.context_key !== binding.key) {
|
|
129
|
+
throw deliveryContextError(`delivery ${state.id} pertence a outro active context`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function activeDelivery(vaultBase, { context = null } = {}) {
|
|
134
|
+
let binding = null;
|
|
135
|
+
let id = readPointer(vaultBase);
|
|
136
|
+
if (context) {
|
|
137
|
+
try { binding = contextualBinding(vaultBase, context); }
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (error?.code === 'WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND') return null;
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
id = binding.id;
|
|
143
|
+
}
|
|
100
144
|
if (!id) return null;
|
|
101
145
|
try {
|
|
102
146
|
const state = readState(vaultBase, id);
|
|
147
|
+
assertStateContext(state, binding);
|
|
103
148
|
return state.state === 'active' ? state : null;
|
|
104
149
|
} catch {
|
|
105
150
|
return null;
|
|
@@ -156,8 +201,12 @@ function context(parsed) {
|
|
|
156
201
|
return { projectRoot, repoRoot, vaultBase: resolved.base };
|
|
157
202
|
}
|
|
158
203
|
|
|
159
|
-
function currentId(parsed, vaultBase) {
|
|
160
|
-
|
|
204
|
+
function currentId(parsed, vaultBase, commandContext = null) {
|
|
205
|
+
const explicit = parsed.positionals[1];
|
|
206
|
+
if (explicit) return safeId(explicit);
|
|
207
|
+
return safeId(commandContext
|
|
208
|
+
? activeDelivery(vaultBase, { context: commandContext })?.id
|
|
209
|
+
: readPointer(vaultBase));
|
|
161
210
|
}
|
|
162
211
|
|
|
163
212
|
function ensureClean(repoRoot) {
|
|
@@ -169,11 +218,30 @@ function ensureClean(repoRoot) {
|
|
|
169
218
|
}
|
|
170
219
|
}
|
|
171
220
|
|
|
172
|
-
export function startDelivery({
|
|
221
|
+
export function startDelivery({
|
|
222
|
+
vaultBase,
|
|
223
|
+
repoRoot,
|
|
224
|
+
id,
|
|
225
|
+
capabilities,
|
|
226
|
+
sourceChange = '',
|
|
227
|
+
sourceCommit = '',
|
|
228
|
+
context = null,
|
|
229
|
+
bindDelivery = setActiveContextDelivery,
|
|
230
|
+
removeState = rmSync,
|
|
231
|
+
now = new Date(),
|
|
232
|
+
}) {
|
|
173
233
|
ensureClean(repoRoot);
|
|
174
234
|
const deliveryId = safeId(id || generatedId(now));
|
|
175
235
|
const paths = deliveryPaths(vaultBase, deliveryId);
|
|
176
236
|
if (existsSync(paths.state)) throw new Error(`delivery já existe: ${deliveryId}`);
|
|
237
|
+
if (context) {
|
|
238
|
+
try {
|
|
239
|
+
const current = contextualBinding(vaultBase, context);
|
|
240
|
+
if (current.id) throw deliveryContextBusy(current.id);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (error?.code !== 'WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND') throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
177
245
|
const commit = sourceCommit || git(repoRoot, ['rev-parse', 'HEAD']);
|
|
178
246
|
git(repoRoot, ['cat-file', '-e', `${commit}^{commit}`]);
|
|
179
247
|
const requestedCapabilities = [...new Set((capabilities || []).map((item) => String(item).trim()).filter(Boolean))];
|
|
@@ -195,16 +263,29 @@ export function startDelivery({ vaultBase, repoRoot, id, capabilities, sourceCha
|
|
|
195
263
|
worktree: repoRoot,
|
|
196
264
|
branch: git(repoRoot, ['branch', '--show-current'], true),
|
|
197
265
|
source_commit: commit,
|
|
266
|
+
...(context ? { context_key: activeContextKey(context) } : {}),
|
|
198
267
|
started_at: now.toISOString(),
|
|
199
268
|
};
|
|
200
269
|
writeState(vaultBase, state);
|
|
201
|
-
|
|
270
|
+
if (context) {
|
|
271
|
+
try { bindDelivery(vaultBase, context, deliveryId); }
|
|
272
|
+
catch (error) {
|
|
273
|
+
try { removeState(paths.state, { force: true }); } catch { /* rollback best-effort */ }
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
setPointer(vaultBase, deliveryId);
|
|
278
|
+
}
|
|
202
279
|
return state;
|
|
203
280
|
}
|
|
204
281
|
|
|
205
|
-
export function finishDelivery({
|
|
282
|
+
export function finishDelivery({
|
|
283
|
+
vaultBase, repoRoot, id, target = 'HEAD', evidence = {}, context = null, now = new Date(),
|
|
284
|
+
}) {
|
|
206
285
|
ensureClean(repoRoot);
|
|
207
286
|
const state = readState(vaultBase, safeId(id));
|
|
287
|
+
const binding = context ? contextualBinding(vaultBase, context, state.id) : null;
|
|
288
|
+
assertStateContext(state, binding);
|
|
208
289
|
if (state.state !== 'active') throw new Error(`delivery ${id} não está ativa`);
|
|
209
290
|
const targetCommit = git(repoRoot, ['rev-parse', `${target}^{commit}`]);
|
|
210
291
|
git(repoRoot, ['merge-base', '--is-ancestor', state.source_commit, targetCommit]);
|
|
@@ -228,29 +309,49 @@ export function finishDelivery({ vaultBase, repoRoot, id, target = 'HEAD', evide
|
|
|
228
309
|
work_kind: 'delivery',
|
|
229
310
|
source_change: state.route.source_change || '',
|
|
230
311
|
source_commit: state.source_commit,
|
|
312
|
+
...(state.context_key ? { context_key: state.context_key } : {}),
|
|
231
313
|
target,
|
|
232
314
|
target_commit: targetCommit,
|
|
233
315
|
capabilities,
|
|
234
316
|
evidence,
|
|
235
317
|
finished_at: now.toISOString(),
|
|
236
318
|
};
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
319
|
+
if (context) clearActiveContextDelivery(vaultBase, context, { expectedRevision: binding.binding.revision });
|
|
320
|
+
try {
|
|
321
|
+
appendReceipt(vaultBase, receipt);
|
|
322
|
+
writeState(vaultBase, { ...state, state: 'completed', target, target_commit: targetCommit, finished_at: receipt.finished_at, receipt });
|
|
323
|
+
} catch (error) {
|
|
324
|
+
if (context) {
|
|
325
|
+
try { setActiveContextDelivery(vaultBase, context, state.id); } catch { /* rollback best-effort */ }
|
|
326
|
+
}
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
if (!context && readPointer(vaultBase) === state.id) setPointer(vaultBase);
|
|
240
330
|
return receipt;
|
|
241
331
|
}
|
|
242
332
|
|
|
243
|
-
export function abandonDelivery({ vaultBase, id, reason, now = new Date() }) {
|
|
333
|
+
export function abandonDelivery({ vaultBase, id, reason, context = null, now = new Date() }) {
|
|
244
334
|
const state = readState(vaultBase, safeId(id));
|
|
335
|
+
const binding = context ? contextualBinding(vaultBase, context, state.id) : null;
|
|
336
|
+
assertStateContext(state, binding);
|
|
245
337
|
if (state.state !== 'active') throw new Error(`delivery ${id} não está ativa`);
|
|
246
338
|
if (!String(reason || '').trim()) throw new Error('delivery abandon requer --reason <text>');
|
|
247
339
|
const receipt = {
|
|
248
340
|
schema_version: 1, delivery_id: state.id, outcome: 'abandoned',
|
|
341
|
+
...(state.context_key ? { context_key: state.context_key } : {}),
|
|
249
342
|
reason: String(reason).trim(), abandoned_at: now.toISOString(),
|
|
250
343
|
};
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
344
|
+
if (context) clearActiveContextDelivery(vaultBase, context, { expectedRevision: binding.binding.revision });
|
|
345
|
+
try {
|
|
346
|
+
appendReceipt(vaultBase, receipt);
|
|
347
|
+
writeState(vaultBase, { ...state, state: 'abandoned', reason: receipt.reason, abandoned_at: receipt.abandoned_at });
|
|
348
|
+
} catch (error) {
|
|
349
|
+
if (context) {
|
|
350
|
+
try { setActiveContextDelivery(vaultBase, context, state.id); } catch { /* rollback best-effort */ }
|
|
351
|
+
}
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
if (!context && readPointer(vaultBase) === state.id) setPointer(vaultBase);
|
|
254
355
|
return receipt;
|
|
255
356
|
}
|
|
256
357
|
|
|
@@ -263,23 +364,33 @@ export function runDelivery(argv = []) {
|
|
|
263
364
|
try {
|
|
264
365
|
const parsed = parseArgv(argv);
|
|
265
366
|
const sub = parsed.positionals[0] || 'status';
|
|
266
|
-
const { repoRoot, vaultBase } = context(parsed);
|
|
367
|
+
const { projectRoot, repoRoot, vaultBase } = context(parsed);
|
|
368
|
+
const commandContext = resolveCommandActiveContext({
|
|
369
|
+
vaultBase,
|
|
370
|
+
projectRoot,
|
|
371
|
+
sessionId: parsed.value('--session') || process.env.CODEX_THREAD_ID || process.env.CLAUDE_SESSION_ID || '',
|
|
372
|
+
});
|
|
267
373
|
if (sub === 'start') {
|
|
268
374
|
const state = startDelivery({
|
|
269
375
|
vaultBase, repoRoot, id: parsed.positionals[1], capabilities: parsed.all('--allow'),
|
|
270
376
|
sourceChange: parsed.value('--source-change'), sourceCommit: parsed.value('--source-commit'),
|
|
377
|
+
context: commandContext,
|
|
271
378
|
});
|
|
272
379
|
emit(state, parsed.json);
|
|
273
380
|
return 0;
|
|
274
381
|
}
|
|
275
382
|
if (sub === 'status') {
|
|
276
|
-
const
|
|
383
|
+
const id = currentId(parsed, vaultBase, commandContext);
|
|
384
|
+
const binding = commandContext ? contextualBinding(vaultBase, commandContext, id) : null;
|
|
385
|
+
const state = readState(vaultBase, id);
|
|
386
|
+
assertStateContext(state, binding);
|
|
277
387
|
emit(state, parsed.json);
|
|
278
388
|
return 0;
|
|
279
389
|
}
|
|
280
390
|
if (sub === 'finish') {
|
|
281
391
|
const receipt = finishDelivery({
|
|
282
|
-
vaultBase, repoRoot, id: currentId(parsed, vaultBase), target: parsed.value('--target') || 'HEAD',
|
|
392
|
+
vaultBase, repoRoot, id: currentId(parsed, vaultBase, commandContext), target: parsed.value('--target') || 'HEAD',
|
|
393
|
+
context: commandContext,
|
|
283
394
|
evidence: {
|
|
284
395
|
ci_url: parsed.value('--ci-url'), version: parsed.value('--version'),
|
|
285
396
|
npm_integrity: parsed.value('--npm-integrity'), release_url: parsed.value('--release-url'),
|
|
@@ -290,14 +401,15 @@ export function runDelivery(argv = []) {
|
|
|
290
401
|
}
|
|
291
402
|
if (sub === 'abandon') {
|
|
292
403
|
const receipt = abandonDelivery({
|
|
293
|
-
vaultBase, id: currentId(parsed, vaultBase), reason: parsed.value('--reason'),
|
|
404
|
+
vaultBase, id: currentId(parsed, vaultBase, commandContext), reason: parsed.value('--reason'),
|
|
405
|
+
context: commandContext,
|
|
294
406
|
});
|
|
295
407
|
emit(receipt, parsed.json);
|
|
296
408
|
return 0;
|
|
297
409
|
}
|
|
298
410
|
throw new Error(`subcomando desconhecido: ${sub}`);
|
|
299
411
|
} catch (error) {
|
|
300
|
-
process.stderr.write(`wendkeep delivery: ${error.message}\n`);
|
|
412
|
+
process.stderr.write(`wendkeep delivery: ${error.code ? `${error.code}: ` : ''}${error.message}\n`);
|
|
301
413
|
return 2;
|
|
302
414
|
}
|
|
303
415
|
}
|
package/src/spec.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from '../hooks/spec-core.mjs';
|
|
12
12
|
import { activeChange, parseTasks } from '../hooks/change-core.mjs';
|
|
13
13
|
import { getLocale } from '../hooks/locale.mjs';
|
|
14
|
+
import { resolveCommandActiveContext } from './active-context-runtime.mjs';
|
|
14
15
|
|
|
15
16
|
function resolveVault(argv) {
|
|
16
17
|
let vault;
|
|
@@ -38,9 +39,21 @@ export function runSpec(argv) {
|
|
|
38
39
|
const entry = rest.find((a) => a.startsWith(`${name}=`));
|
|
39
40
|
return entry ? entry.slice(name.length + 1) : undefined;
|
|
40
41
|
};
|
|
42
|
+
const commandContext = () => {
|
|
43
|
+
try {
|
|
44
|
+
return resolveCommandActiveContext({
|
|
45
|
+
vaultBase,
|
|
46
|
+
projectRoot: resolve(option('--project') || process.cwd()),
|
|
47
|
+
sessionId: option('--session') || process.env.CODEX_THREAD_ID || process.env.CLAUDE_SESSION_ID || '',
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
process.stderr.write(`wendkeep spec: ${error.code || 'WENDKEEP_ACTIVE_CONTEXT_FAILED'}: ${error.message}\n`);
|
|
51
|
+
process.exit(2);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
41
54
|
|
|
42
55
|
if (sub === 'effective') {
|
|
43
|
-
const slug = option('--change') || activeChange(vaultBase);
|
|
56
|
+
const slug = option('--change') || activeChange(vaultBase, { context: commandContext() });
|
|
44
57
|
if (!slug) { process.stderr.write('wendkeep spec effective: no change (--change or current)\n'); process.exit(2); }
|
|
45
58
|
const changeDir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|
|
46
59
|
let tasks = [];
|
|
@@ -74,7 +87,7 @@ export function runSpec(argv) {
|
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
if (sub === 'rebase') {
|
|
77
|
-
const slug = option('--change') || activeChange(vaultBase);
|
|
90
|
+
const slug = option('--change') || activeChange(vaultBase, { context: commandContext() });
|
|
78
91
|
if (!slug) { process.stderr.write('wendkeep spec rebase: no change (--change or current)\n'); process.exit(2); }
|
|
79
92
|
const changeDir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|
|
80
93
|
try { readFileSync(join(changeDir, 'proposta.md'), 'utf8'); }
|
package/src/verify.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from '../hooks/spec-core.mjs';
|
|
21
21
|
import { addLesson } from '../hooks/lessons-core.mjs';
|
|
22
22
|
import { getLocale } from '../hooks/locale.mjs';
|
|
23
|
+
import { resolveCommandActiveContext } from './active-context-runtime.mjs';
|
|
23
24
|
|
|
24
25
|
function today() {
|
|
25
26
|
const d = new Date();
|
|
@@ -40,7 +41,20 @@ export function runVerify(argv) {
|
|
|
40
41
|
// --project wins; otherwise climb from cwd to the nearest project marker (agent shells
|
|
41
42
|
// keep their cwd across commands, so verify from a subdirectory is a recurring miss).
|
|
42
43
|
const projectRoot = resolve(opt(argv, '--project') || findProjectRoot(process.cwd()) || process.cwd());
|
|
43
|
-
|
|
44
|
+
let commandContext = null;
|
|
45
|
+
if (!opt(argv, '--change')) {
|
|
46
|
+
try {
|
|
47
|
+
commandContext = resolveCommandActiveContext({
|
|
48
|
+
vaultBase,
|
|
49
|
+
projectRoot,
|
|
50
|
+
sessionId: opt(argv, '--session') || process.env.CODEX_THREAD_ID || process.env.CLAUDE_SESSION_ID || '',
|
|
51
|
+
});
|
|
52
|
+
} catch (error) {
|
|
53
|
+
process.stderr.write(`wendkeep verify: ${error.code || 'WENDKEEP_ACTIVE_CONTEXT_FAILED'}: ${error.message}\n`);
|
|
54
|
+
process.exit(2);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const slug = opt(argv, '--change') || activeChange(vaultBase, { context: commandContext });
|
|
44
58
|
if (!slug) { process.stderr.write('wendkeep verify: no change (--change or active).\n'); process.exit(2); }
|
|
45
59
|
|
|
46
60
|
const changeDir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|