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
package/src/delivery.mjs CHANGED
@@ -1,6 +1,8 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- import { existsSync, readFileSync, rmSync } from 'node:fs';
3
- import { basename, join, resolve } from 'node:path';
2
+ import { createHash } from 'node:crypto';
3
+ import { existsSync, readFileSync, realpathSync, rmSync } from 'node:fs';
4
+ import { join, resolve } from 'node:path';
5
+ import { isDeepStrictEqual } from 'node:util';
4
6
  import {
5
7
  assertVaultPathSafe, mkdirVaultPath, writeVaultFileSync,
6
8
  } from '../hooks/vault-path-safety.mjs';
@@ -13,12 +15,30 @@ import {
13
15
  setActiveContextDelivery,
14
16
  } from '../hooks/active-context-store.mjs';
15
17
  import { resolveCommandActiveContext } from './active-context-runtime.mjs';
18
+ import {
19
+ evaluateProvenanceGate,
20
+ evaluateReleaseChain,
21
+ } from './provenance-gate.mjs';
22
+ import {
23
+ collectCiObservation,
24
+ collectGitHubReleaseObservation,
25
+ collectGitSubject,
26
+ collectNpmObservation,
27
+ collectTagObservation,
28
+ normalizeRepository,
29
+ } from './provenance-sources.mjs';
30
+ import {
31
+ appendReceipt as appendLedgerReceipt,
32
+ createFileReceiptStore,
33
+ readReceiptLedger,
34
+ } from './receipt-ledger.mjs';
35
+ import { collectArtifactAtCommit } from './release-provenance.mjs';
16
36
 
17
37
  export const DELIVERY_HELP = `wendkeep delivery <subcommand>
18
38
 
19
39
  start [id] --allow <capability> [--source-change <slug>] [--source-commit <sha>]
20
40
  status [id]
21
- finish [id] [--target <ref>] [--ci-url <url>] [--version <x.y.z>]
41
+ finish [id] [--target <remote>/<branch>] [--ci-url <url>] [--version <x.y.z>]
22
42
  [--npm-integrity <sha512-...>] [--release-url <url>]
23
43
  abandon [id] --reason <text>
24
44
 
@@ -31,6 +51,9 @@ export const DELIVERY_CAPABILITIES = Object.freeze([
31
51
  'git:merge', 'git:pull', 'git:push', 'git:tag', 'publish',
32
52
  ]);
33
53
 
54
+ const REMOTE_TARGET_CAPABILITIES = Object.freeze(['git:merge', 'git:push']);
55
+ const REMOTE_BOUND_CAPABILITIES = Object.freeze([...REMOTE_TARGET_CAPABILITIES, 'publish']);
56
+
34
57
  const VALUE_OPTIONS = new Set([
35
58
  '--project', '--vault', '--allow', '--source-change', '--source-commit', '--target',
36
59
  '--ci-url', '--version', '--npm-integrity', '--release-url', '--reason', '--session',
@@ -64,6 +87,41 @@ function parseArgv(argv) {
64
87
  };
65
88
  }
66
89
 
90
+ function sanitizeDeliveryText(value, maximum = 240) {
91
+ const clean = String(value || '')
92
+ .replace(/\b(?:ghp|github_pat|npm_|sk-|xox[baprs]-)[A-Za-z0-9_-]+/gi, '[redacted-token]')
93
+ .replace(/\b(?:token|authorization|bearer|password|secret|api[_-]?key)\s*[=:]\s*[^\s,;]+/gi, '[redacted-token]')
94
+ .replace(/\bBearer\s+[^\s,;]+/gi, '[redacted-token]')
95
+ .replace(/[A-Za-z]:\\[^\r\n"'`;|&]+/g, '[redacted-path]')
96
+ .replace(/(?:^|\s)\/[^\s"'`;|&]+/g, ' [redacted-path]')
97
+ .replace(/[\r\n\t]+/g, ' ')
98
+ .trim();
99
+ return clean.length > maximum ? `${clean.slice(0, maximum - 3)}...` : clean;
100
+ }
101
+
102
+ function sanitizedDeliveryValue(value, depth = 0) {
103
+ if (depth > 4 || value === undefined) return undefined;
104
+ if (value === null || typeof value === 'boolean' || typeof value === 'number') return value;
105
+ if (typeof value === 'string') return sanitizeDeliveryText(value);
106
+ if (Array.isArray(value)) return value.slice(0, 20).map((item) => sanitizedDeliveryValue(item, depth + 1));
107
+ if (typeof value === 'object') {
108
+ return Object.fromEntries(Object.entries(value).slice(0, 30)
109
+ .map(([key, item]) => [sanitizeDeliveryText(key, 80), sanitizedDeliveryValue(item, depth + 1)]));
110
+ }
111
+ return sanitizeDeliveryText(value);
112
+ }
113
+
114
+ function gitFailure(error) {
115
+ const failure = new Error('Falha ao consultar o repositório Git para a delivery.');
116
+ failure.code = 'WENDKEEP_DELIVERY_GIT_FAILED';
117
+ failure.operation = 'delivery.git';
118
+ failure.state = 'unproven';
119
+ failure.blocker = failure.code;
120
+ failure.recovery = 'wendkeep delivery status --json';
121
+ if (Number.isSafeInteger(Number(error?.status))) failure.status = Number(error.status);
122
+ return failure;
123
+ }
124
+
67
125
  function git(projectRoot, args, optional = false) {
68
126
  try {
69
127
  return execFileSync('git', args, {
@@ -71,7 +129,18 @@ function git(projectRoot, args, optional = false) {
71
129
  }).trim();
72
130
  } catch (error) {
73
131
  if (optional) return '';
74
- throw new Error(`git ${args.join(' ')} falhou: ${String(error.stderr || error.message).trim()}`);
132
+ throw gitFailure(error);
133
+ }
134
+ }
135
+
136
+ function gitWith(projectRoot, args, execute = execFileSync, optional = false) {
137
+ try {
138
+ return String(execute('git', args, {
139
+ cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], shell: false, timeout: 15_000,
140
+ }) || '').trim();
141
+ } catch (error) {
142
+ if (optional) return '';
143
+ throw gitFailure(error);
75
144
  }
76
145
  }
77
146
 
@@ -82,7 +151,10 @@ function deliveryPaths(vaultBase, id = '') {
82
151
  runtime,
83
152
  deliveries,
84
153
  pointer: join(runtime, 'CURRENT_DELIVERY'),
85
- receipts: join(runtime, 'delivery-receipts.jsonl'),
154
+ legacyReceipts: join(runtime, 'delivery-receipts.jsonl'),
155
+ receipts: join(runtime, 'delivery-receipts-v2.jsonl'),
156
+ receiptsCheckpoint: join(runtime, 'delivery-receipts-v2.checkpoint.json'),
157
+ receiptsLock: join(runtime, 'delivery-receipts-v2.lock'),
86
158
  state: id ? join(deliveries, `${id}.json`) : '',
87
159
  };
88
160
  }
@@ -105,6 +177,10 @@ function readPointer(vaultBase) {
105
177
  function deliveryContextError(message) {
106
178
  const error = new Error(message);
107
179
  error.code = 'WENDKEEP_DELIVERY_CONTEXT_MISMATCH';
180
+ error.operation = 'delivery';
181
+ error.state = 'conflict';
182
+ error.blocker = error.code;
183
+ error.recovery = 'wendkeep delivery status --json';
108
184
  return error;
109
185
  }
110
186
 
@@ -123,13 +199,57 @@ function contextualBinding(vaultBase, context, expectedId = '') {
123
199
  return { binding, id, key: activeContextKey(context) };
124
200
  }
125
201
 
202
+ function contextIdentityBinding(context, authority = {}) {
203
+ return {
204
+ project_id: String(authority.project_id ?? context?.projectId ?? context?.project_id ?? ''),
205
+ repository_id: String(authority.repository_id ?? context?.repositoryId ?? context?.repository_id ?? ''),
206
+ worktree_id: String(authority.worktree_id ?? context?.worktreeId ?? context?.worktree_id ?? ''),
207
+ work_session_id: String(authority.work_session_id ?? context?.workSessionId ?? context?.work_session_id ?? ''),
208
+ change_slug: String(authority.change_slug || ''),
209
+ };
210
+ }
211
+
126
212
  function assertStateContext(state, binding) {
213
+ if (state.context_key && !binding) {
214
+ throw deliveryContextError(`delivery ${state.id} exige o active context proprietário`);
215
+ }
127
216
  if (!binding) return;
128
217
  if (state.context_key !== binding.key) {
129
218
  throw deliveryContextError(`delivery ${state.id} pertence a outro active context`);
130
219
  }
131
220
  }
132
221
 
222
+ function normalizedCheckoutPath(value) {
223
+ let canonical = resolve(String(value || ''));
224
+ try { canonical = realpathSync.native(canonical); } catch { /* mismatch stays fail-closed below */ }
225
+ const normalized = canonical.replace(/\\/g, '/').replace(/\/$/, '');
226
+ return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
227
+ }
228
+
229
+ function deliveryScopeError(details) {
230
+ const error = new Error('delivery não pertence ao repository, worktree ou branch atuais');
231
+ error.code = 'WENDKEEP_DELIVERY_SCOPE_MISMATCH';
232
+ error.details = details;
233
+ error.operation = 'delivery.finish';
234
+ error.state = 'conflict';
235
+ error.blocker = error.code;
236
+ error.recovery = 'wendkeep delivery status --json';
237
+ return error;
238
+ }
239
+
240
+ function assertStateCheckout(state, repoRoot, execute) {
241
+ const currentWorktree = gitWith(repoRoot, ['rev-parse', '--show-toplevel'], execute, true);
242
+ const currentBranch = gitWith(repoRoot, ['branch', '--show-current'], execute, true);
243
+ const sameRepository = Boolean(typeof state.repository === 'string' && state.repository
244
+ && normalizedCheckoutPath(repoRoot) === normalizedCheckoutPath(state.repository));
245
+ const sameWorktree = Boolean(typeof state.worktree === 'string' && state.worktree && currentWorktree
246
+ && normalizedCheckoutPath(currentWorktree) === normalizedCheckoutPath(state.worktree));
247
+ const sameBranch = typeof state.branch === 'string' && currentBranch === state.branch;
248
+ if (!sameRepository || !sameWorktree || !sameBranch) {
249
+ throw deliveryScopeError({ same_repository: sameRepository, same_worktree: sameWorktree, same_branch: sameBranch });
250
+ }
251
+ }
252
+
133
253
  export function activeDelivery(vaultBase, { context = null } = {}) {
134
254
  let binding = null;
135
255
  let id = readPointer(vaultBase);
@@ -176,18 +296,24 @@ function setPointer(vaultBase, id = '') {
176
296
  });
177
297
  }
178
298
 
179
- function appendReceipt(vaultBase, receipt) {
299
+ function receiptStore(vaultBase) {
180
300
  const paths = deliveryPaths(vaultBase);
181
301
  mkdirVaultPath(vaultBase, paths.runtime, { label: 'runtime de delivery' });
182
- let previous = '';
183
- if (existsSync(paths.receipts)) {
184
- const checked = assertVaultPathSafe(vaultBase, paths.receipts, {
185
- allowMissing: false, expectedType: 'file', label: 'ledger de receipts de delivery',
186
- });
187
- previous = readFileSync(checked.target, 'utf8');
188
- }
189
- writeVaultFileSync(vaultBase, paths.receipts, `${previous}${JSON.stringify(receipt)}\n`, 'utf8', {
190
- label: 'ledger de receipts de delivery',
302
+ for (const [path, label] of [
303
+ [paths.receipts, 'ledger v2 de receipts de delivery'],
304
+ [paths.receiptsCheckpoint, 'checkpoint do ledger de delivery'],
305
+ [paths.receiptsLock, 'lock do ledger de delivery'],
306
+ [paths.legacyReceipts, 'ledger legado de delivery'],
307
+ ]) assertVaultPathSafe(vaultBase, path, {
308
+ allowMissing: true,
309
+ expectedType: 'file',
310
+ label,
311
+ });
312
+ return createFileReceiptStore({
313
+ ledgerPath: paths.receipts,
314
+ checkpointPath: paths.receiptsCheckpoint,
315
+ legacyPath: paths.legacyReceipts,
316
+ lockPath: paths.receiptsLock,
191
317
  });
192
318
  }
193
319
 
@@ -234,36 +360,51 @@ export function startDelivery({
234
360
  const deliveryId = safeId(id || generatedId(now));
235
361
  const paths = deliveryPaths(vaultBase, deliveryId);
236
362
  if (existsSync(paths.state)) throw new Error(`delivery já existe: ${deliveryId}`);
363
+ let existingContext = null;
237
364
  if (context) {
238
365
  try {
239
366
  const current = contextualBinding(vaultBase, context);
240
367
  if (current.id) throw deliveryContextBusy(current.id);
368
+ existingContext = current.binding;
241
369
  } catch (error) {
242
370
  if (error?.code !== 'WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND') throw error;
243
371
  }
244
372
  }
245
- const commit = sourceCommit || git(repoRoot, ['rev-parse', 'HEAD']);
246
- git(repoRoot, ['cat-file', '-e', `${commit}^{commit}`]);
247
373
  const requestedCapabilities = [...new Set((capabilities || []).map((item) => String(item).trim()).filter(Boolean))];
248
374
  const invalidCapabilities = requestedCapabilities.filter((item) => !DELIVERY_CAPABILITIES.includes(item));
249
375
  if (invalidCapabilities.length) {
250
376
  throw new Error(`capability inválida: ${invalidCapabilities.join(', ')}. Use ${DELIVERY_CAPABILITIES.join(', ')}.`);
251
377
  }
378
+ if (!requestedCapabilities.length) throw new Error('delivery start requer ao menos um --allow <capability>');
379
+ const commit = git(repoRoot, [
380
+ 'rev-parse', '--verify', '--end-of-options', `${sourceCommit || 'HEAD'}^{commit}`,
381
+ ]);
382
+ const remoteName = 'origin';
383
+ const remoteRepository = normalizeRepository(git(repoRoot, ['remote', 'get-url', remoteName], true));
384
+ if (requestedCapabilities.some((capability) => REMOTE_BOUND_CAPABILITIES.includes(capability)) && !remoteRepository) {
385
+ const error = new Error('delivery externa requer remote origin GitHub identificável no start.');
386
+ error.code = 'WENDKEEP_DELIVERY_REPOSITORY_REQUIRED';
387
+ throw error;
388
+ }
252
389
  const route = createWorkRoute({
253
390
  workKind: 'delivery', profile: 'ASSURE', contractImpact: 'none',
254
391
  operationRisk: requestedCapabilities, sourceChange, sourceCommit: commit,
255
392
  });
256
- if (!route.operation_risk.length) throw new Error('delivery start requer ao menos um --allow <capability>');
257
393
  const state = {
258
394
  schema_version: 1,
259
395
  id: deliveryId,
260
396
  state: 'active',
261
397
  route,
262
398
  repository: repoRoot,
399
+ remote_name: remoteName,
400
+ remote_repository: remoteRepository,
263
401
  worktree: repoRoot,
264
402
  branch: git(repoRoot, ['branch', '--show-current'], true),
265
403
  source_commit: commit,
266
- ...(context ? { context_key: activeContextKey(context) } : {}),
404
+ ...(context ? {
405
+ context_key: activeContextKey(context),
406
+ ...contextIdentityBinding(context, existingContext || {}),
407
+ } : {}),
267
408
  started_at: now.toISOString(),
268
409
  };
269
410
  writeState(vaultBase, state);
@@ -279,77 +420,553 @@ export function startDelivery({
279
420
  return state;
280
421
  }
281
422
 
423
+ const DEFAULT_PROVENANCE_COLLECTORS = Object.freeze({
424
+ collectGitSubject,
425
+ collectArtifactObservation: collectArtifactAtCommit,
426
+ collectTagObservation,
427
+ collectCiObservation,
428
+ collectNpmObservation,
429
+ collectGitHubReleaseObservation,
430
+ });
431
+
432
+ function observationAssessment(kind, observation) {
433
+ const state = String(observation?.state || 'unproven');
434
+ return {
435
+ kind,
436
+ state,
437
+ reasonCodes: observation?.reasonCodes || (state === 'verified' ? [] : ['PROV_REQUIRED_ASSESSMENT_MISSING']),
438
+ diagnostics: observation?.diagnostics || [{ kind, state }],
439
+ };
440
+ }
441
+
442
+ function verifiedAssessment(kind, diagnostics = []) {
443
+ return { kind, state: 'verified', reasonCodes: [], diagnostics };
444
+ }
445
+
446
+ function conflictAssessment(kind, code, diagnostics = []) {
447
+ return { kind, state: 'conflict', reasonCodes: [code], diagnostics };
448
+ }
449
+
450
+ function provenanceBlocked(gate, message = 'delivery bloqueada por evidência de proveniência insuficiente') {
451
+ const error = new Error(message);
452
+ error.code = 'WENDKEEP_PROVENANCE_GATE_BLOCKED';
453
+ error.provenance = gate;
454
+ error.operation = 'delivery.finish';
455
+ error.state = gate?.state || 'unproven';
456
+ error.blocker = gate?.reasonCodes?.[0] || 'PROV_REQUIRED_ASSESSMENT_MISSING';
457
+ error.recovery = gate?.repair?.command || 'wendkeep delivery status --json';
458
+ const first = gate?.diagnostics?.[0] || {};
459
+ error.expected = first.expected ?? null;
460
+ error.observed = first.observed ?? null;
461
+ return error;
462
+ }
463
+
464
+ function remoteBindingObservation({ repoRoot, state, remote = state.remote_name || 'origin', execute }) {
465
+ const expectedRepository = normalizeRepository(state.remote_repository || '');
466
+ const remoteUrl = gitWith(repoRoot, ['remote', 'get-url', remote], execute, true);
467
+ const repository = normalizeRepository(remoteUrl);
468
+ if (!expectedRepository) {
469
+ return {
470
+ state: 'unproven', reasonCodes: ['PROVENANCE_REPOSITORY_UNBOUND'], remote, repository,
471
+ };
472
+ }
473
+ if (!repository) {
474
+ return {
475
+ state: 'unproven', reasonCodes: ['PROVENANCE_REPOSITORY_UNOBSERVED'], remote, repository,
476
+ };
477
+ }
478
+ if (repository !== expectedRepository) {
479
+ return {
480
+ state: 'conflict', reasonCodes: ['PROVENANCE_REPOSITORY_MISMATCH'],
481
+ remote, repository, expectedRepository,
482
+ };
483
+ }
484
+ return {
485
+ state: 'verified', reasonCodes: [], remote, repository, expectedRepository,
486
+ };
487
+ }
488
+
489
+ function parseRemoteTarget(target) {
490
+ const value = String(target || '').trim();
491
+ const match = value.match(/^([A-Za-z0-9][A-Za-z0-9._-]*)\/(.+)$/);
492
+ if (!match) return null;
493
+ const remote = match[1];
494
+ const branch = match[2].replace(/^refs\/heads\//, '');
495
+ if (!branch || branch.startsWith('-') || branch.endsWith('/') || branch.endsWith('.')
496
+ || branch.includes('..') || branch.includes('@{') || branch.includes('\\')
497
+ || /[\u0000-\u0020~^:?*[\u007f]/.test(branch)
498
+ || branch.split('/').some((part) => !part || part.endsWith('.lock'))) return null;
499
+ return { remote, branch, ref: `refs/heads/${branch}` };
500
+ }
501
+
502
+ function remoteTargetObservation({ repoRoot, state, target, execute }) {
503
+ const parsed = parseRemoteTarget(target);
504
+ if (!parsed) {
505
+ return { state: 'unproven', reasonCodes: ['PROVENANCE_REMOTE_TARGET_REQUIRED'] };
506
+ }
507
+ const expectedRemote = String(state.remote_name || 'origin');
508
+ if (parsed.remote !== expectedRemote) {
509
+ return {
510
+ state: 'conflict', reasonCodes: ['PROVENANCE_REMOTE_TARGET_MISMATCH'],
511
+ remote: parsed.remote, expectedRemote, ref: parsed.ref,
512
+ };
513
+ }
514
+ const binding = remoteBindingObservation({ repoRoot, state, remote: parsed.remote, execute });
515
+ if (binding.state !== 'verified') return { ...binding, ref: parsed.ref };
516
+ let output;
517
+ try {
518
+ output = gitWith(repoRoot, [
519
+ 'ls-remote', '--exit-code', parsed.remote, parsed.ref,
520
+ ], execute);
521
+ } catch (error) {
522
+ return {
523
+ state: Number(error?.status) === 2 ? 'unproven' : 'reported',
524
+ reasonCodes: [Number(error?.status) === 2
525
+ ? 'PROVENANCE_REMOTE_TARGET_UNOBSERVED'
526
+ : 'PROVENANCE_SOURCE_UNAVAILABLE'],
527
+ remote: parsed.remote,
528
+ ref: parsed.ref,
529
+ repository: binding.repository,
530
+ };
531
+ }
532
+ const matches = String(output || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
533
+ .map((line) => line.match(/^([0-9a-f]{40})\s+(.+)$/i)).filter(Boolean)
534
+ .filter((match) => match[2] === parsed.ref);
535
+ if (matches.length !== 1) {
536
+ return {
537
+ state: 'unproven', reasonCodes: ['PROVENANCE_REMOTE_TARGET_UNOBSERVED'],
538
+ remote: parsed.remote, ref: parsed.ref, repository: binding.repository,
539
+ };
540
+ }
541
+ return {
542
+ state: 'verified', reasonCodes: [], commit: matches[0][1], remote: parsed.remote,
543
+ ref: parsed.ref, repository: binding.repository, locator: `${parsed.remote}/${parsed.branch}`,
544
+ };
545
+ }
546
+
547
+ function completedReceipt(vaultBase, state) {
548
+ const receipt = state.receipt;
549
+ if (!receipt || receipt.schema_version !== 2) {
550
+ const error = new Error('State completed não contém receipt v2 verificável.');
551
+ error.code = 'WENDKEEP_RECEIPT_LEDGER_CORRUPT';
552
+ throw error;
553
+ }
554
+ const ledger = readReceiptLedger({ store: receiptStore(vaultBase) });
555
+ const record = ledger.records.find((candidate) => candidate.receipt_id === receipt.receipt_id);
556
+ const binding = deliveryBinding(state);
557
+ const recordMatches = record
558
+ && record.receipt_hash === receipt.receipt_hash
559
+ && record.sequence === receipt.sequence
560
+ && record.kind === 'delivery.completed'
561
+ && record.subject?.delivery_id === state.id
562
+ && record.subject?.source_commit === state.source_commit
563
+ && record.subject?.target_commit === state.target_commit
564
+ && receipt.delivery_id === state.id
565
+ && receipt.source_commit === state.source_commit
566
+ && receipt.target_commit === state.target_commit
567
+ && receipt.target === state.target
568
+ && Object.entries(binding).every(([key, value]) => record.subject?.[key] === value && receipt[key] === value)
569
+ && Object.keys(record).every((key) => isDeepStrictEqual(receipt[key], record[key]));
570
+ if (!recordMatches) {
571
+ const error = new Error('Receipt do state completed diverge do ledger v2 verificado.');
572
+ error.code = 'WENDKEEP_RECEIPT_LEDGER_CORRUPT';
573
+ throw error;
574
+ }
575
+ return receipt;
576
+ }
577
+
578
+ function sanitizedLocator(value) {
579
+ try {
580
+ const url = new URL(String(value || ''));
581
+ if (url.protocol !== 'https:') return '';
582
+ return `${url.origin}${url.pathname}`;
583
+ } catch {
584
+ return '';
585
+ }
586
+ }
587
+
588
+ function sanitizedEvidence(evidence = {}) {
589
+ return {
590
+ version: String(evidence.version || ''),
591
+ npm_integrity: String(evidence.npm_integrity || ''),
592
+ ci_url: sanitizedLocator(evidence.ci_url),
593
+ release_url: sanitizedLocator(evidence.release_url),
594
+ };
595
+ }
596
+
597
+ function sha256Text(value) {
598
+ return `sha256:${createHash('sha256').update(String(value), 'utf8').digest('hex')}`;
599
+ }
600
+
601
+ function privateReason(reason) {
602
+ return { reason: 'operator-provided', reason_digest: sha256Text(String(reason).trim()) };
603
+ }
604
+
605
+ function deliveryBinding(state) {
606
+ return {
607
+ project_id: String(state.project_id || ''),
608
+ repository_id: String(state.repository_id || ''),
609
+ repository: String(state.remote_repository || ''),
610
+ worktree_id: String(state.worktree_id || ''),
611
+ work_session_id: String(state.work_session_id || ''),
612
+ change_slug: String(state.route?.source_change || state.change_slug || ''),
613
+ branch: String(state.branch || ''),
614
+ };
615
+ }
616
+
617
+ function releaseLocatorMatches(locator, repository, tag) {
618
+ try {
619
+ const url = new URL(String(locator || ''));
620
+ const parts = url.pathname.split('/').filter(Boolean).map(decodeURIComponent);
621
+ return url.protocol === 'https:'
622
+ && url.hostname.toLowerCase() === 'github.com'
623
+ && normalizeRepository(`${parts[0] || ''}/${parts[1] || ''}`) === repository
624
+ && parts[2] === 'releases'
625
+ && parts[3] === 'tag'
626
+ && parts.slice(4).join('/') === tag;
627
+ } catch {
628
+ return false;
629
+ }
630
+ }
631
+
632
+ function sanitizedObservation(value = {}) {
633
+ const clean = {};
634
+ for (const key of [
635
+ 'state', 'kind', 'commit', 'sourceCommit', 'targetCommit', 'name', 'version', 'tag',
636
+ 'integrity', 'repository', 'locator', 'remote', 'ref', 'conclusion', 'status',
637
+ 'expectedCommit', 'expectedIntegrity', 'expectedRepository',
638
+ ]) if (value[key] !== undefined) clean[key] = value[key];
639
+ if (Array.isArray(value.reasonCodes)) clean.reasonCodes = value.reasonCodes.map(String);
640
+ if (value.package && typeof value.package === 'object') {
641
+ clean.package = { name: String(value.package.name || ''), version: String(value.package.version || '') };
642
+ }
643
+ return clean;
644
+ }
645
+
646
+ function sanitizedObservations(observations = {}) {
647
+ return Object.fromEntries(Object.entries(observations).map(([kind, value]) => [kind, sanitizedObservation(value)]));
648
+ }
649
+
650
+ function deliveryReceiptDraft({ state, target, targetCommit, capabilities, evidence, observations, now }) {
651
+ return {
652
+ kind: 'delivery.completed',
653
+ subject: {
654
+ delivery_id: state.id,
655
+ source_commit: state.source_commit,
656
+ target,
657
+ target_commit: targetCommit,
658
+ ...deliveryBinding(state),
659
+ },
660
+ claims: {
661
+ work_kind: 'delivery',
662
+ source_change: state.route.source_change || '',
663
+ capabilities,
664
+ evidence: sanitizedEvidence(evidence),
665
+ ...(state.context_key ? { context_key: state.context_key } : {}),
666
+ },
667
+ observations: sanitizedObservations(observations),
668
+ recorded_at: now.toISOString(),
669
+ };
670
+ }
671
+
672
+ function publicDeliveryReceipt(record, state, target, targetCommit, capabilities, evidence) {
673
+ return {
674
+ ...record,
675
+ delivery_id: state.id,
676
+ outcome: 'completed',
677
+ work_kind: 'delivery',
678
+ source_change: state.route.source_change || '',
679
+ source_commit: state.source_commit,
680
+ ...deliveryBinding(state),
681
+ ...(state.context_key ? { context_key: state.context_key } : {}),
682
+ target,
683
+ target_commit: targetCommit,
684
+ capabilities,
685
+ evidence: sanitizedEvidence(evidence),
686
+ finished_at: record.recorded_at,
687
+ };
688
+ }
689
+
282
690
  export function finishDelivery({
283
- vaultBase, repoRoot, id, target = 'HEAD', evidence = {}, context = null, now = new Date(),
691
+ vaultBase,
692
+ repoRoot,
693
+ id,
694
+ target = 'HEAD',
695
+ evidence = {},
696
+ context = null,
697
+ now = new Date(),
698
+ collectors = DEFAULT_PROVENANCE_COLLECTORS,
699
+ execute = execFileSync,
700
+ appendLedger = appendLedgerReceipt,
701
+ persistState = writeState,
702
+ clearContextDelivery = clearActiveContextDelivery,
284
703
  }) {
285
- ensureClean(repoRoot);
286
704
  const state = readState(vaultBase, safeId(id));
287
- const binding = context ? contextualBinding(vaultBase, context, state.id) : null;
705
+ const binding = context
706
+ ? contextualBinding(vaultBase, context, state.state === 'active' ? state.id : '')
707
+ : null;
288
708
  assertStateContext(state, binding);
709
+ assertStateCheckout(state, repoRoot, execute);
710
+ ensureClean(repoRoot);
711
+ if (state.state === 'completed') {
712
+ const receipt = completedReceipt(vaultBase, state);
713
+ if (context && binding.id === state.id) {
714
+ clearContextDelivery(vaultBase, context, { expectedRevision: binding.binding.revision });
715
+ }
716
+ if (!context && readPointer(vaultBase) === state.id) setPointer(vaultBase);
717
+ return receipt;
718
+ }
289
719
  if (state.state !== 'active') throw new Error(`delivery ${id} não está ativa`);
290
- const targetCommit = git(repoRoot, ['rev-parse', `${target}^{commit}`]);
291
- git(repoRoot, ['merge-base', '--is-ancestor', state.source_commit, targetCommit]);
720
+
292
721
  const capabilities = state.route?.operation_risk || [];
722
+ const needsRemoteTarget = capabilities.some((capability) => REMOTE_TARGET_CAPABILITIES.includes(capability));
723
+ let remoteTarget = null;
724
+ if (needsRemoteTarget) {
725
+ remoteTarget = remoteTargetObservation({ repoRoot, state, target, execute });
726
+ const remoteGate = evaluateProvenanceGate({
727
+ purpose: 'delivery', assessments: [observationAssessment('remote-target', remoteTarget)], requiredKinds: ['remote-target'],
728
+ });
729
+ if (!remoteGate.ok) throw provenanceBlocked(remoteGate);
730
+ }
731
+ const subject = collectors.collectGitSubject?.({
732
+ repoRoot,
733
+ sourceRef: state.source_commit,
734
+ targetRef: remoteTarget?.commit || target,
735
+ execute,
736
+ }) || { state: 'unproven', reasonCodes: ['PROVENANCE_GIT_SUBJECT_UNRESOLVED'] };
737
+ const targetCommit = String(subject.targetCommit || remoteTarget?.commit
738
+ || gitWith(repoRoot, ['rev-parse', `${target}^{commit}`], execute, true));
739
+ const assessments = [
740
+ ...(remoteTarget ? [observationAssessment('remote-target', remoteTarget)] : []),
741
+ observationAssessment('git-subject', subject),
742
+ ];
743
+ let ancestor = false;
744
+ if (subject.state === 'verified' && subject.sourceCommit === state.source_commit && targetCommit) {
745
+ try {
746
+ gitWith(repoRoot, ['merge-base', '--is-ancestor', state.source_commit, targetCommit], execute);
747
+ ancestor = true;
748
+ } catch { /* represented by the assessment below */ }
749
+ }
750
+ assessments.push(ancestor
751
+ ? verifiedAssessment('ancestry', [{ source_commit: state.source_commit, target_commit: targetCommit }])
752
+ : conflictAssessment('ancestry', 'PROVENANCE_SOURCE_NOT_ANCESTOR', [{ source_commit: state.source_commit, target_commit: targetCommit }]));
753
+ const observations = {
754
+ ...(remoteTarget ? { remote_target: remoteTarget } : {}),
755
+ git_subject: subject,
756
+ ancestry: { state: ancestor ? 'verified' : 'conflict' },
757
+ };
758
+ const requiredKinds = [...(remoteTarget ? ['remote-target'] : []), 'git-subject', 'ancestry'];
759
+ let tagObservation = null;
760
+ let artifactObservation = null;
761
+ let ciObservation = null;
762
+ let npmObservation = null;
763
+ let releaseObservation = null;
293
764
  if (capabilities.includes('git:tag') || capabilities.includes('publish')) {
294
765
  if (!evidence.version) throw new Error('delivery com tag/publicação requer --version');
295
- const pkg = JSON.parse(readFileSync(join(repoRoot, 'package.json'), 'utf8'));
296
- if (pkg.version !== evidence.version) throw new Error(`package.json ${pkg.version} diverge de ${evidence.version}`);
297
- const tagCommit = git(repoRoot, ['rev-list', '-n', '1', `refs/tags/v${evidence.version}`]);
298
- if (tagCommit !== targetCommit) throw new Error(`v${evidence.version} não aponta para o target comprovado`);
766
+ const packageMatches = subject.state === 'verified' && subject.package?.version === evidence.version;
767
+ assessments.push(packageMatches
768
+ ? verifiedAssessment('package', [{ version: subject.package.version, target_commit: targetCommit }])
769
+ : conflictAssessment('package', 'PROVENANCE_VERSION_MISMATCH', [{ expected: evidence.version, observed: subject.package?.version || '' }]));
770
+ requiredKinds.push('package', 'tag');
771
+ tagObservation = collectors.collectTagObservation?.({
772
+ repoRoot,
773
+ tag: `v${evidence.version}`,
774
+ expectedCommit: targetCommit,
775
+ execute,
776
+ }) || { state: 'unproven', reasonCodes: ['PROVENANCE_TAG_UNRESOLVED'] };
777
+ assessments.push(observationAssessment('tag', tagObservation));
778
+ observations.tag = tagObservation;
299
779
  }
300
780
  if (capabilities.includes('publish')) {
301
781
  for (const [key, label] of [
302
782
  ['ci_url', '--ci-url'], ['npm_integrity', '--npm-integrity'], ['release_url', '--release-url'],
303
783
  ]) if (!evidence[key]) throw new Error(`delivery com publish requer ${label}`);
784
+ const repositoryBinding = remoteBindingObservation({ repoRoot, state, execute });
785
+ assessments.push(observationAssessment('repository', repositoryBinding));
786
+ requiredKinds.push('repository');
787
+ observations.repository = repositoryBinding;
788
+ const repositoryGate = evaluateProvenanceGate({
789
+ purpose: 'delivery',
790
+ assessments: [observationAssessment('repository', repositoryBinding)],
791
+ requiredKinds: ['repository'],
792
+ });
793
+ if (!repositoryGate.ok) throw provenanceBlocked(repositoryGate);
794
+ const repository = repositoryBinding.repository || state.remote_repository || '';
795
+ artifactObservation = collectors.collectArtifactObservation?.({
796
+ repoRoot,
797
+ targetCommit,
798
+ execute,
799
+ }) || { state: 'unproven', reasonCodes: ['PROVENANCE_INTEGRITY_MISSING'] };
800
+ ciObservation = collectors.collectCiObservation?.({
801
+ locator: evidence.ci_url,
802
+ repository,
803
+ expectedCommit: targetCommit,
804
+ execute,
805
+ }) || { state: 'unproven', reasonCodes: ['PROVENANCE_SOURCE_UNAVAILABLE'] };
806
+ npmObservation = collectors.collectNpmObservation?.({
807
+ name: subject.package?.name,
808
+ version: subject.package?.version,
809
+ expectedIntegrity: artifactObservation.integrity,
810
+ expectedCommit: targetCommit,
811
+ repository,
812
+ execute,
813
+ }) || { state: 'unproven', reasonCodes: ['PROVENANCE_SOURCE_UNAVAILABLE'] };
814
+ releaseObservation = collectors.collectGitHubReleaseObservation?.({
815
+ repository,
816
+ tag: `v${evidence.version}`,
817
+ expectedCommit: targetCommit,
818
+ expectedVersion: evidence.version,
819
+ expectedNotes: subject.notes,
820
+ locator: evidence.release_url,
821
+ execute,
822
+ }) || { state: 'unproven', reasonCodes: ['PROVENANCE_SOURCE_UNAVAILABLE'] };
823
+ observations.artifact = artifactObservation;
824
+ observations.ci = ciObservation;
825
+ observations.npm = npmObservation;
826
+ observations.release = releaseObservation;
827
+ for (const [kind, observation] of [
828
+ ['artifact', artifactObservation], ['ci', ciObservation], ['npm', npmObservation], ['release', releaseObservation],
829
+ ]) assessments.push(observationAssessment(kind, observation));
830
+ const integrityClaimMatches = artifactObservation.state === 'verified'
831
+ && evidence.npm_integrity === artifactObservation.integrity;
832
+ assessments.push(integrityClaimMatches
833
+ ? verifiedAssessment('integrity-claim', [{ integrity: artifactObservation.integrity }])
834
+ : conflictAssessment('integrity-claim', 'PROVENANCE_INTEGRITY_MISMATCH', [{
835
+ expected: artifactObservation.integrity || '', observed: evidence.npm_integrity,
836
+ }]));
837
+ const releaseClaimMatches = releaseLocatorMatches(evidence.release_url, repository, `v${evidence.version}`);
838
+ assessments.push(releaseClaimMatches
839
+ ? verifiedAssessment('release-locator')
840
+ : conflictAssessment('release-locator', 'PROVENANCE_REPOSITORY_MISMATCH'));
841
+ requiredKinds.push('artifact', 'ci', 'npm', 'release', 'integrity-claim', 'release-locator', 'release-chain');
842
+ const releaseParts = [tagObservation, artifactObservation, ciObservation, npmObservation, releaseObservation];
843
+ const releaseChain = releaseParts.every((item) => item?.state === 'verified')
844
+ ? evaluateReleaseChain({
845
+ chain: {
846
+ commit: { sha: targetCommit },
847
+ tag: { name: tagObservation?.tag || tagObservation?.name || `v${evidence.version}`, commit: tagObservation?.commit },
848
+ package: { ...subject.package, commit: targetCommit },
849
+ artifact: artifactObservation,
850
+ npm: npmObservation,
851
+ ci: ciObservation,
852
+ release: releaseObservation,
853
+ },
854
+ context: {
855
+ repository,
856
+ target_commit: targetCommit,
857
+ package_name: subject.package?.name,
858
+ package_version: subject.package?.version,
859
+ tag: `v${evidence.version}`,
860
+ },
861
+ })
862
+ : {
863
+ kind: 'release-chain',
864
+ state: releaseParts.some((item) => item?.state === 'conflict')
865
+ ? 'conflict'
866
+ : (releaseParts.some((item) => item?.state === 'reported') ? 'reported' : 'unproven'),
867
+ reasonCodes: [...new Set(releaseParts.flatMap((item) => item?.reasonCodes || []))],
868
+ diagnostics: releaseParts.flatMap((item) => item?.diagnostics || []),
869
+ };
870
+ assessments.push({ ...releaseChain, kind: 'release-chain' });
304
871
  }
305
- const receipt = {
306
- schema_version: 1,
307
- delivery_id: state.id,
308
- outcome: 'completed',
309
- work_kind: 'delivery',
310
- source_change: state.route.source_change || '',
311
- source_commit: state.source_commit,
312
- ...(state.context_key ? { context_key: state.context_key } : {}),
313
- target,
314
- target_commit: targetCommit,
315
- capabilities,
316
- evidence,
317
- finished_at: now.toISOString(),
318
- };
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;
872
+ const gate = evaluateProvenanceGate({ purpose: 'delivery', assessments, requiredKinds });
873
+ if (!gate.ok) throw provenanceBlocked(gate);
874
+ const appended = appendLedger({
875
+ store: receiptStore(vaultBase),
876
+ draft: deliveryReceiptDraft({ state, target, targetCommit, capabilities, evidence, observations, now }),
877
+ });
878
+ const receipt = publicDeliveryReceipt(appended.record, state, target, targetCommit, capabilities, evidence);
879
+ persistState(vaultBase, {
880
+ ...state, state: 'completed', target, target_commit: targetCommit, finished_at: receipt.finished_at, receipt,
881
+ });
882
+ if (context) {
883
+ clearContextDelivery(vaultBase, context, { expectedRevision: binding.binding.revision });
328
884
  }
329
885
  if (!context && readPointer(vaultBase) === state.id) setPointer(vaultBase);
330
886
  return receipt;
331
887
  }
332
888
 
333
- export function abandonDelivery({ vaultBase, id, reason, context = null, now = new Date() }) {
889
+ function abandonedReceipt(vaultBase, state) {
890
+ const receipt = state.receipt;
891
+ if (!receipt || receipt.schema_version !== 2 || !receipt.reason_digest) {
892
+ const error = new Error('State abandoned não contém receipt v2 verificável.');
893
+ error.code = 'WENDKEEP_RECEIPT_LEDGER_CORRUPT';
894
+ throw error;
895
+ }
896
+ const ledger = readReceiptLedger({ store: receiptStore(vaultBase) });
897
+ const record = ledger.records.find((candidate) => candidate.receipt_id === receipt.receipt_id);
898
+ const binding = deliveryBinding(state);
899
+ const matches = record
900
+ && record.receipt_hash === receipt.receipt_hash
901
+ && record.sequence === receipt.sequence
902
+ && record.kind === 'delivery.abandoned'
903
+ && record.subject?.delivery_id === state.id
904
+ && record.subject?.source_commit === state.source_commit
905
+ && Object.entries(binding).every(([key, value]) => record.subject?.[key] === value && receipt[key] === value)
906
+ && record.claims?.reason_digest === receipt.reason_digest
907
+ && Object.keys(record).every((key) => isDeepStrictEqual(receipt[key], record[key]));
908
+ if (!matches) {
909
+ const error = new Error('Receipt de abandono diverge do ledger v2 verificado.');
910
+ error.code = 'WENDKEEP_RECEIPT_LEDGER_CORRUPT';
911
+ throw error;
912
+ }
913
+ return receipt;
914
+ }
915
+
916
+ export function abandonDelivery({
917
+ vaultBase,
918
+ id,
919
+ reason,
920
+ context = null,
921
+ now = new Date(),
922
+ appendLedger = appendLedgerReceipt,
923
+ persistState = writeState,
924
+ clearContextDelivery = clearActiveContextDelivery,
925
+ }) {
334
926
  const state = readState(vaultBase, safeId(id));
335
- const binding = context ? contextualBinding(vaultBase, context, state.id) : null;
927
+ const binding = context
928
+ ? contextualBinding(vaultBase, context, state.state === 'active' ? state.id : '')
929
+ : null;
336
930
  assertStateContext(state, binding);
931
+ if (state.state === 'abandoned') {
932
+ const receipt = abandonedReceipt(vaultBase, state);
933
+ if (context && binding.id === state.id) {
934
+ clearContextDelivery(vaultBase, context, { expectedRevision: binding.binding.revision });
935
+ }
936
+ if (!context && readPointer(vaultBase) === state.id) setPointer(vaultBase);
937
+ return receipt;
938
+ }
337
939
  if (state.state !== 'active') throw new Error(`delivery ${id} não está ativa`);
338
940
  if (!String(reason || '').trim()) throw new Error('delivery abandon requer --reason <text>');
941
+ const reasonBinding = privateReason(reason);
942
+ const appended = appendLedger({
943
+ store: receiptStore(vaultBase),
944
+ draft: {
945
+ kind: 'delivery.abandoned',
946
+ subject: { delivery_id: state.id, source_commit: state.source_commit, ...deliveryBinding(state) },
947
+ claims: {
948
+ outcome: 'abandoned',
949
+ ...reasonBinding,
950
+ ...(state.context_key ? { context_key: state.context_key } : {}),
951
+ },
952
+ observations: { local_state: { state: 'verified', delivery_state: state.state } },
953
+ recorded_at: now.toISOString(),
954
+ },
955
+ });
339
956
  const receipt = {
340
- schema_version: 1, delivery_id: state.id, outcome: 'abandoned',
957
+ ...appended.record,
958
+ delivery_id: state.id,
959
+ outcome: 'abandoned',
960
+ ...deliveryBinding(state),
341
961
  ...(state.context_key ? { context_key: state.context_key } : {}),
342
- reason: String(reason).trim(), abandoned_at: now.toISOString(),
962
+ ...reasonBinding,
963
+ abandoned_at: now.toISOString(),
343
964
  };
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;
965
+ persistState(vaultBase, {
966
+ ...state, state: 'abandoned', ...reasonBinding, abandoned_at: receipt.abandoned_at, receipt,
967
+ });
968
+ if (context) {
969
+ clearContextDelivery(vaultBase, context, { expectedRevision: binding.binding.revision });
353
970
  }
354
971
  if (!context && readPointer(vaultBase) === state.id) setPointer(vaultBase);
355
972
  return receipt;
@@ -360,10 +977,49 @@ function emit(payload, json) {
360
977
  else process.stdout.write(`${payload.id || payload.delivery_id}: ${payload.state || payload.outcome}\n`);
361
978
  }
362
979
 
980
+ function deliveryFailurePayload(error, operation) {
981
+ const provenance = error?.provenance || null;
982
+ const code = /^[A-Z0-9_]+$/.test(String(error?.code || ''))
983
+ ? String(error.code)
984
+ : 'WENDKEEP_DELIVERY_FAILED';
985
+ const state = sanitizeDeliveryText(error?.state || provenance?.state || 'unproven', 40) || 'unproven';
986
+ const blocker = sanitizeDeliveryText(
987
+ error?.blocker || provenance?.reasonCodes?.[0] || code,
988
+ 120,
989
+ ) || code;
990
+ const recovery = sanitizeDeliveryText(
991
+ error?.recovery?.command || error?.recovery || provenance?.repair?.command
992
+ || 'wendkeep delivery status --json',
993
+ );
994
+ return {
995
+ ok: false,
996
+ code,
997
+ error: code === 'WENDKEEP_PROVENANCE_GATE_BLOCKED'
998
+ ? 'Delivery bloqueada pelo gate de proveniência.'
999
+ : (code === 'WENDKEEP_DELIVERY_GIT_FAILED'
1000
+ ? 'Falha ao consultar o repositório Git para a delivery.'
1001
+ : 'Falha no comando delivery.'),
1002
+ operation: sanitizeDeliveryText(error?.operation || operation, 80) || 'delivery',
1003
+ state,
1004
+ blocker,
1005
+ expected: sanitizedDeliveryValue(error?.expected ?? provenance?.diagnostics?.[0]?.expected ?? null),
1006
+ observed: sanitizedDeliveryValue(error?.observed ?? provenance?.diagnostics?.[0]?.observed ?? null),
1007
+ recovery,
1008
+ };
1009
+ }
1010
+
1011
+ function renderDeliveryFailure(error, operation, json) {
1012
+ const payload = deliveryFailurePayload(error, operation);
1013
+ if (json) return JSON.stringify(payload);
1014
+ return `${payload.code}: ${payload.error} operation=${payload.operation} state=${payload.state} blocker=${payload.blocker} expected=${JSON.stringify(payload.expected)} observed=${JSON.stringify(payload.observed)} recovery=${payload.recovery}`;
1015
+ }
1016
+
363
1017
  export function runDelivery(argv = []) {
1018
+ let parsed = null;
1019
+ let sub = 'status';
364
1020
  try {
365
- const parsed = parseArgv(argv);
366
- const sub = parsed.positionals[0] || 'status';
1021
+ parsed = parseArgv(argv);
1022
+ sub = parsed.positionals[0] || 'status';
367
1023
  const { projectRoot, repoRoot, vaultBase } = context(parsed);
368
1024
  const commandContext = resolveCommandActiveContext({
369
1025
  vaultBase,
@@ -409,7 +1065,8 @@ export function runDelivery(argv = []) {
409
1065
  }
410
1066
  throw new Error(`subcomando desconhecido: ${sub}`);
411
1067
  } catch (error) {
412
- process.stderr.write(`wendkeep delivery: ${error.code ? `${error.code}: ` : ''}${error.message}\n`);
1068
+ const json = Boolean(parsed?.json || argv.includes('--json'));
1069
+ process.stderr.write(`${renderDeliveryFailure(error, `delivery.${sub}`, json)}\n`);
413
1070
  return 2;
414
1071
  }
415
1072
  }