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/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
- export function activeDelivery(vaultBase) {
99
- const id = readPointer(vaultBase);
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
- return safeId(parsed.positionals[1] || readPointer(vaultBase));
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({ vaultBase, repoRoot, id, capabilities, sourceChange = '', sourceCommit = '', now = new Date() }) {
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
- setPointer(vaultBase, deliveryId);
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({ vaultBase, repoRoot, id, target = 'HEAD', evidence = {}, now = new Date() }) {
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
- appendReceipt(vaultBase, receipt);
238
- writeState(vaultBase, { ...state, state: 'completed', target, target_commit: targetCommit, finished_at: receipt.finished_at, receipt });
239
- if (readPointer(vaultBase) === state.id) setPointer(vaultBase);
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
- appendReceipt(vaultBase, receipt);
252
- writeState(vaultBase, { ...state, state: 'abandoned', reason: receipt.reason, abandoned_at: receipt.abandoned_at });
253
- if (readPointer(vaultBase) === state.id) setPointer(vaultBase);
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 state = readState(vaultBase, currentId(parsed, vaultBase));
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
- const slug = opt(argv, '--change') || activeChange(vaultBase);
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);