wendkeep 0.72.0 → 0.73.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 (44) hide show
  1. package/CHANGELOG.md +66 -3
  2. package/README.en.md +28 -11
  3. package/README.md +28 -11
  4. package/docs/en/commands/changes-and-verification.md +10 -5
  5. package/docs/en/commands/maintenance-and-diagnostics.md +17 -9
  6. package/docs/en/commands/observer.md +18 -12
  7. package/docs/en/commands/operating-profiles.md +28 -3
  8. package/docs/en/commands/sessions-and-import.md +4 -4
  9. package/docs/pt-BR/commands/changes-and-verification.md +10 -5
  10. package/docs/pt-BR/commands/maintenance-and-diagnostics.md +12 -5
  11. package/docs/pt-BR/commands/observer.md +18 -12
  12. package/docs/pt-BR/commands/operating-profiles.md +28 -3
  13. package/docs/pt-BR/commands/sessions-and-import.md +4 -4
  14. package/hooks/brain-inject.mjs +6 -6
  15. package/hooks/change-context.mjs +11 -0
  16. package/hooks/change-core.mjs +53 -21
  17. package/hooks/change-warn.mjs +2 -0
  18. package/hooks/harness-doctor.mjs +13 -5
  19. package/hooks/understand-inject.mjs +1 -1
  20. package/hooks/vault-health.mjs +2 -2
  21. package/package.json +5 -4
  22. package/packages/cli/src/index.mjs +12 -2
  23. package/packages/integrations/src/host-hooks.mjs +1 -1
  24. package/packages/vault/src/memory-store.mjs +17 -4
  25. package/src/change.mjs +10 -4
  26. package/src/delivery.mjs +303 -0
  27. package/src/doctor.mjs +47 -10
  28. package/src/init.mjs +2 -2
  29. package/src/observer-auth.mjs +10 -0
  30. package/src/observer-memory-publish.mjs +13 -8
  31. package/src/observer-privacy.mjs +23 -0
  32. package/src/observer-publish.mjs +10 -7
  33. package/src/observer-server.mjs +51 -0
  34. package/src/observer-sql-publish.mjs +72 -31
  35. package/src/observer-sql-store.mjs +33 -2
  36. package/src/observer.mjs +10 -3
  37. package/src/release-changelog.mjs +1 -1
  38. package/src/release-provenance.mjs +115 -0
  39. package/src/skills-seed.mjs +25 -9
  40. package/src/sync-defs.mjs +5 -2
  41. package/src/sync.mjs +2 -2
  42. package/src/taxonomy.mjs +1 -1
  43. package/src/vault-readme.mjs +2 -2
  44. package/src/work-kind.mjs +62 -0
@@ -48,9 +48,9 @@ Usage:
48
48
  package first (npm i -D wendkeep@latest); a running process
49
49
  cannot replace itself. · --vault P · --profile <name> · --yes.
50
50
 
51
- wendkeep doctor [--vault P] Run a vault health check.
51
+ wendkeep doctor [--vault P] Health check. --scope core|runtime · --strict for CI/release.
52
52
  wendkeep observer <sub> Local multi-project Observer: serve | register | publish | status.
53
- wendkeep change <sub> Change lifecycle: new [--simple] | use | bind <slug> --session <id> | continue | list | show |
53
+ wendkeep change <sub> Change lifecycle: new [--simple|--guide] | use | bind <slug> --session <id> | continue | list | show |
54
54
  status | done <id> | undone <id> | diff | archive [--force] | abandon | relink | backlink.
55
55
  archive exige verdict (rode verify --deep); abandon descarta sem ADR.
56
56
  backlink [--apply]: injeta o backlink pro proposta em design/tarefas/spec órfãos (open + _arquivo).
@@ -62,6 +62,8 @@ Usage:
62
62
  the project default. The Vault/session/memory core is always active.
63
63
  wendkeep flow <sub> Low-ceremony E -> V contract: start | status | show | finish | promote.
64
64
  FLOW records scope, sensors and a receipt without creating a change.
65
+ wendkeep delivery <sub> Operational delivery: start | status | finish | abandon.
66
+ Records authorization and an append-only receipt; never creates a change/spec/ADR.
65
67
  wendkeep spec <sub> Specs: list | show | effective [--change] [--json] | migrate | rebase.
66
68
  wendkeep sensors <sub> list | add <id> "<command>" [--severity --type --report].
67
69
  wendkeep cost [opts] Aggregate AI-coding spend across the vault's sessions.
@@ -184,6 +186,9 @@ async function main(argv) {
184
186
  if (cmd === 'flow') {
185
187
  const { FLOW_HELP } = await import('../../../src/flow.mjs');
186
188
  process.stdout.write(FLOW_HELP);
189
+ } else if (cmd === 'delivery') {
190
+ const { DELIVERY_HELP } = await import('../../../src/delivery.mjs');
191
+ process.stdout.write(DELIVERY_HELP);
187
192
  } else if (cmd === 'profile') {
188
193
  const { PROFILE_HELP } = await import('../../../src/profile.mjs');
189
194
  process.stdout.write(PROFILE_HELP);
@@ -273,6 +278,11 @@ async function main(argv) {
273
278
  process.exit(await runFlow(rest));
274
279
  break;
275
280
  }
281
+ case 'delivery': {
282
+ const { runDelivery } = await import('../../../src/delivery.mjs');
283
+ process.exit(runDelivery(rest));
284
+ break;
285
+ }
276
286
  case 'theme': {
277
287
  const { runTheme } = await import('../../../src/theme.mjs');
278
288
  runTheme(rest);
@@ -32,7 +32,7 @@ export const SESSION_HOOKS = [
32
32
  ];
33
33
 
34
34
  export function hookCommand(name) {
35
- return `npx wendkeep hook ${name}`;
35
+ return `npx --no-install wendkeep hook ${name}`;
36
36
  }
37
37
 
38
38
  // Forma node-direta do comando de hook: 1 processo (~100-250ms) em vez dos 3 do npx (cold-start
@@ -112,12 +112,25 @@ function readCheckedMemoryFile(vaultBase, path, encoding, label, { allowMissing
112
112
  return readFileSync(checked.target, encoding);
113
113
  }
114
114
 
115
+ export function memoryFileIdentityMatches(descriptor, target, {
116
+ platform = process.platform,
117
+ } = {}) {
118
+ if (descriptor.ino !== target.ino) return false;
119
+ // libuv before 1.51 can report an inconsistent Windows volume serial number
120
+ // between stat(path) and fstat(fd). The inode is still the file index; path
121
+ // containment/reparse checks and nlink validation remain independent guards.
122
+ return platform === 'win32' || descriptor.dev === target.dev;
123
+ }
124
+
115
125
  function assertOpenedMemoryFile(vaultBase, path, fd, label) {
116
126
  const checked = checkedMemoryFile(vaultBase, path, label, { allowMissing: false });
117
- const descriptor = fstatSync(fd);
118
- const target = statSync(checked.target);
119
- if (!descriptor.isFile() || descriptor.nlink > 1 || target.nlink > 1
120
- || descriptor.dev !== target.dev || descriptor.ino !== target.ino) {
127
+ // Windows file identities can exceed Number's safe integer range. Node 22.13 may
128
+ // round stat(path) and fstat(fd) differently for the same file, so compare the
129
+ // exact bigint values and keep nlink as the independent hardlink guard.
130
+ const descriptor = fstatSync(fd, { bigint: true });
131
+ const target = statSync(checked.target, { bigint: true });
132
+ if (!descriptor.isFile() || descriptor.nlink > 1n || target.nlink > 1n
133
+ || !memoryFileIdentityMatches(descriptor, target)) {
121
134
  throw unsafeMemoryPath(`${label} mudou de inode ou possui hardlink antes da mutação: ${checked.target}`);
122
135
  }
123
136
  return checked.target;
package/src/change.mjs CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  relinkChanges,
17
17
  backfillArtifactLinks,
18
18
  scaffoldPlaceholders,
19
+ isGuideCompactChange,
19
20
  } from '../hooks/change-core.mjs';
20
21
  import { evaluateGate, requiredSensors } from '../hooks/sensors-core.mjs';
21
22
  import { buildEffectiveRequirementPackage, evaluateVerdict, formatOrphanReqs, tasksHashOf, parseSpecsList, parseDelta, parseRequirements, applyDelta, validateSpecImpact } from '../hooks/spec-core.mjs';
@@ -61,7 +62,9 @@ export function runChange(argv) {
61
62
  // G2: link the active session into the proposta's source: (graph edge proposta->sessão).
62
63
  let sessionRel = '';
63
64
  try { sessionRel = readControl(vaultBase).session_file || ''; } catch { /* sem control */ }
64
- const r = newChange(vaultBase, slug, { dateStr: today(), simple: rest.includes('--simple'), sessionRel });
65
+ const r = newChange(vaultBase, slug, {
66
+ dateStr: today(), simple: rest.includes('--simple'), guide: rest.includes('--guide'), sessionRel,
67
+ });
65
68
  process.stdout.write(`change ${r.created ? 'created' : 'exists'}: ${r.rel} (active)\n`);
66
69
  process.exit(0);
67
70
  }
@@ -97,7 +100,7 @@ export function runChange(argv) {
97
100
  let sessionRel = '';
98
101
  try { sessionRel = readControl(vaultBase).session_file || ''; } catch { /* no control */ }
99
102
  const r = continueChange(vaultBase, archivedSlug, newSlug, {
100
- dateStr: today(), simple: rest.includes('--simple'), sessionRel,
103
+ dateStr: today(), simple: rest.includes('--simple'), guide: rest.includes('--guide'), sessionRel,
101
104
  });
102
105
  if (!r.ok) { process.stderr.write(`wendkeep change continue: ${r.error}\n`); process.exit(2); }
103
106
  process.stdout.write(`change created: ${r.rel} (continues ${r.archived}; active)\n`);
@@ -287,13 +290,16 @@ export function runChange(argv) {
287
290
  try { tasks = parseTasks(readFileSync(join(vaultBase, getLocale(vaultBase).folders.changes, slug, 'tarefas.md'), 'utf8')); } catch { /* sem tarefas */ }
288
291
  const forced = rest.includes('--force') && tasks.some((t) => !t.done);
289
292
  const trivial = !tasks.some((t) => t.req) && !tasks.some((t) => t.sensor);
290
- if (trivial) process.stderr.write('aviso: change trivial (sem [req:]/[sensor:]) — ADR marcado trivial: true\n');
293
+ const compactGuide = isGuideCompactChange(join(vaultBase, getLocale(vaultBase).folders.changes, slug));
294
+ if (trivial) process.stderr.write(compactGuide
295
+ ? 'aviso: GUIDE compacta sem [req:]/[sensor:] — resultado permanece auditável no archive, sem ADR automático\n'
296
+ : 'aviso: change trivial (sem [req:]/[sensor:]) — ADR marcado trivial: true\n');
291
297
  const r = archiveChange(vaultBase, slug, { dateStr: today(), adrNum: getNextAdrNumber(vaultBase), gate, adrFlags: { forced, trivial } });
292
298
  if (!r.ok) {
293
299
  process.stderr.write(`change archive BLOCKED (gate): ${r.failing.join('; ')}\n`);
294
300
  process.exit(1);
295
301
  }
296
- process.stdout.write(`archived: ${r.archivedRel}; ADR: ${r.adrRel}\n`);
302
+ process.stdout.write(`archived: ${r.archivedRel}${r.adrRel ? `; ADR: ${r.adrRel}` : '; GUIDE compacta: sem ADR'}\n`);
297
303
  if (r.promoted && r.promoted.length) process.stdout.write(`specs promovidas: ${r.promoted.join(', ')}\n`);
298
304
  if (r.specWarnings && r.specWarnings.length) for (const w of r.specWarnings) process.stderr.write(` aviso spec: ${w}\n`);
299
305
  process.exit(0);
@@ -0,0 +1,303 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { basename, join, resolve } from 'node:path';
4
+ import {
5
+ assertVaultPathSafe, mkdirVaultPath, writeVaultFileSync,
6
+ } from '../hooks/vault-path-safety.mjs';
7
+ import { resolveProjectVault } from './project-vault.mjs';
8
+ import { createWorkRoute } from './work-kind.mjs';
9
+
10
+ export const DELIVERY_HELP = `wendkeep delivery <subcommand>
11
+
12
+ start [id] --allow <capability> [--source-change <slug>] [--source-commit <sha>]
13
+ status [id]
14
+ finish [id] [--target <ref>] [--ci-url <url>] [--version <x.y.z>]
15
+ [--npm-integrity <sha512-...>] [--release-url <url>]
16
+ abandon [id] --reason <text>
17
+
18
+ Common options: --project <path> --vault <path> --json
19
+ Delivery authorizes operational risk and creates an append-only receipt. It never creates a change,
20
+ spec, or ADR. If code/config must change, abandon or pause delivery and resume an implementation.
21
+ `;
22
+
23
+ export const DELIVERY_CAPABILITIES = Object.freeze([
24
+ 'git:merge', 'git:pull', 'git:push', 'git:tag', 'publish',
25
+ ]);
26
+
27
+ const VALUE_OPTIONS = new Set([
28
+ '--project', '--vault', '--allow', '--source-change', '--source-commit', '--target',
29
+ '--ci-url', '--version', '--npm-integrity', '--release-url', '--reason',
30
+ ]);
31
+
32
+ function parseArgv(argv) {
33
+ const values = new Map();
34
+ const positionals = [];
35
+ let json = false;
36
+ for (let index = 0; index < argv.length; index += 1) {
37
+ const item = argv[index];
38
+ if (item === '--json') { json = true; continue; }
39
+ if (item.startsWith('--')) {
40
+ const eq = item.indexOf('=');
41
+ const name = eq > 0 ? item.slice(0, eq) : item;
42
+ if (!VALUE_OPTIONS.has(name)) throw new Error(`opção desconhecida: ${name}`);
43
+ const value = eq > 0 ? item.slice(eq + 1) : argv[++index];
44
+ if (!value || value.startsWith('--')) throw new Error(`${name} requer um valor`);
45
+ const list = values.get(name) || [];
46
+ list.push(value);
47
+ values.set(name, list);
48
+ continue;
49
+ }
50
+ positionals.push(item);
51
+ }
52
+ return {
53
+ json,
54
+ positionals,
55
+ value: (name) => values.get(name)?.at(-1) || '',
56
+ all: (name) => values.get(name) || [],
57
+ };
58
+ }
59
+
60
+ function git(projectRoot, args, optional = false) {
61
+ try {
62
+ return execFileSync('git', args, {
63
+ cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
64
+ }).trim();
65
+ } catch (error) {
66
+ if (optional) return '';
67
+ throw new Error(`git ${args.join(' ')} falhou: ${String(error.stderr || error.message).trim()}`);
68
+ }
69
+ }
70
+
71
+ function deliveryPaths(vaultBase, id = '') {
72
+ const runtime = join(vaultBase, '.brain', 'runtime');
73
+ const deliveries = join(runtime, 'deliveries');
74
+ return {
75
+ runtime,
76
+ deliveries,
77
+ pointer: join(runtime, 'CURRENT_DELIVERY'),
78
+ receipts: join(runtime, 'delivery-receipts.jsonl'),
79
+ state: id ? join(deliveries, `${id}.json`) : '',
80
+ };
81
+ }
82
+
83
+ function safeId(value) {
84
+ const id = String(value || '').trim();
85
+ if (!/^[a-z0-9][a-z0-9._-]{1,100}$/i.test(id)) throw new Error('id de delivery inválido');
86
+ return id;
87
+ }
88
+
89
+ function generatedId(now = new Date()) {
90
+ return `delivery-${now.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z').toLowerCase()}`;
91
+ }
92
+
93
+ function readPointer(vaultBase) {
94
+ const { pointer } = deliveryPaths(vaultBase);
95
+ try { return safeId(readFileSync(pointer, 'utf8').trim()); } catch { return ''; }
96
+ }
97
+
98
+ export function activeDelivery(vaultBase) {
99
+ const id = readPointer(vaultBase);
100
+ if (!id) return null;
101
+ try {
102
+ const state = readState(vaultBase, id);
103
+ return state.state === 'active' ? state : null;
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ function readState(vaultBase, id) {
110
+ const path = deliveryPaths(vaultBase, id).state;
111
+ const checked = assertVaultPathSafe(vaultBase, path, {
112
+ allowMissing: false, expectedType: 'file', label: `delivery ${id}`,
113
+ });
114
+ return JSON.parse(readFileSync(checked.target, 'utf8'));
115
+ }
116
+
117
+ function writeState(vaultBase, state) {
118
+ const paths = deliveryPaths(vaultBase, state.id);
119
+ mkdirVaultPath(vaultBase, paths.runtime, { label: 'runtime de delivery' });
120
+ mkdirVaultPath(vaultBase, paths.deliveries, { label: 'estados de delivery' });
121
+ writeVaultFileSync(vaultBase, paths.state, `${JSON.stringify(state, null, 2)}\n`, 'utf8', {
122
+ label: `delivery ${state.id}`,
123
+ });
124
+ }
125
+
126
+ function setPointer(vaultBase, id = '') {
127
+ const paths = deliveryPaths(vaultBase);
128
+ mkdirVaultPath(vaultBase, paths.runtime, { label: 'runtime de delivery' });
129
+ writeVaultFileSync(vaultBase, paths.pointer, id ? `${id}\n` : '', 'utf8', {
130
+ label: 'ponteiro de delivery',
131
+ });
132
+ }
133
+
134
+ function appendReceipt(vaultBase, receipt) {
135
+ const paths = deliveryPaths(vaultBase);
136
+ mkdirVaultPath(vaultBase, paths.runtime, { label: 'runtime de delivery' });
137
+ let previous = '';
138
+ if (existsSync(paths.receipts)) {
139
+ const checked = assertVaultPathSafe(vaultBase, paths.receipts, {
140
+ allowMissing: false, expectedType: 'file', label: 'ledger de receipts de delivery',
141
+ });
142
+ previous = readFileSync(checked.target, 'utf8');
143
+ }
144
+ writeVaultFileSync(vaultBase, paths.receipts, `${previous}${JSON.stringify(receipt)}\n`, 'utf8', {
145
+ label: 'ledger de receipts de delivery',
146
+ });
147
+ }
148
+
149
+ function context(parsed) {
150
+ const projectRoot = resolve(parsed.value('--project') || process.cwd());
151
+ const resolved = resolveProjectVault({
152
+ startDir: projectRoot,
153
+ explicitVault: parsed.value('--vault'),
154
+ });
155
+ const repoRoot = git(projectRoot, ['rev-parse', '--show-toplevel']);
156
+ return { projectRoot, repoRoot, vaultBase: resolved.base };
157
+ }
158
+
159
+ function currentId(parsed, vaultBase) {
160
+ return safeId(parsed.positionals[1] || readPointer(vaultBase));
161
+ }
162
+
163
+ function ensureClean(repoRoot) {
164
+ const dirty = git(repoRoot, ['status', '--porcelain']);
165
+ if (dirty) {
166
+ const error = new Error('delivery requer working tree limpa; alterações de código/config exigem implementation.');
167
+ error.code = 'WENDKEEP_DELIVERY_IMPLEMENTATION_REQUIRED';
168
+ throw error;
169
+ }
170
+ }
171
+
172
+ export function startDelivery({ vaultBase, repoRoot, id, capabilities, sourceChange = '', sourceCommit = '', now = new Date() }) {
173
+ ensureClean(repoRoot);
174
+ const deliveryId = safeId(id || generatedId(now));
175
+ const paths = deliveryPaths(vaultBase, deliveryId);
176
+ if (existsSync(paths.state)) throw new Error(`delivery já existe: ${deliveryId}`);
177
+ const commit = sourceCommit || git(repoRoot, ['rev-parse', 'HEAD']);
178
+ git(repoRoot, ['cat-file', '-e', `${commit}^{commit}`]);
179
+ const requestedCapabilities = [...new Set((capabilities || []).map((item) => String(item).trim()).filter(Boolean))];
180
+ const invalidCapabilities = requestedCapabilities.filter((item) => !DELIVERY_CAPABILITIES.includes(item));
181
+ if (invalidCapabilities.length) {
182
+ throw new Error(`capability inválida: ${invalidCapabilities.join(', ')}. Use ${DELIVERY_CAPABILITIES.join(', ')}.`);
183
+ }
184
+ const route = createWorkRoute({
185
+ workKind: 'delivery', profile: 'ASSURE', contractImpact: 'none',
186
+ operationRisk: requestedCapabilities, sourceChange, sourceCommit: commit,
187
+ });
188
+ if (!route.operation_risk.length) throw new Error('delivery start requer ao menos um --allow <capability>');
189
+ const state = {
190
+ schema_version: 1,
191
+ id: deliveryId,
192
+ state: 'active',
193
+ route,
194
+ repository: repoRoot,
195
+ worktree: repoRoot,
196
+ branch: git(repoRoot, ['branch', '--show-current'], true),
197
+ source_commit: commit,
198
+ started_at: now.toISOString(),
199
+ };
200
+ writeState(vaultBase, state);
201
+ setPointer(vaultBase, deliveryId);
202
+ return state;
203
+ }
204
+
205
+ export function finishDelivery({ vaultBase, repoRoot, id, target = 'HEAD', evidence = {}, now = new Date() }) {
206
+ ensureClean(repoRoot);
207
+ const state = readState(vaultBase, safeId(id));
208
+ if (state.state !== 'active') throw new Error(`delivery ${id} não está ativa`);
209
+ const targetCommit = git(repoRoot, ['rev-parse', `${target}^{commit}`]);
210
+ git(repoRoot, ['merge-base', '--is-ancestor', state.source_commit, targetCommit]);
211
+ const capabilities = state.route?.operation_risk || [];
212
+ if (capabilities.includes('git:tag') || capabilities.includes('publish')) {
213
+ if (!evidence.version) throw new Error('delivery com tag/publicação requer --version');
214
+ const pkg = JSON.parse(readFileSync(join(repoRoot, 'package.json'), 'utf8'));
215
+ if (pkg.version !== evidence.version) throw new Error(`package.json ${pkg.version} diverge de ${evidence.version}`);
216
+ const tagCommit = git(repoRoot, ['rev-list', '-n', '1', `refs/tags/v${evidence.version}`]);
217
+ if (tagCommit !== targetCommit) throw new Error(`v${evidence.version} não aponta para o target comprovado`);
218
+ }
219
+ if (capabilities.includes('publish')) {
220
+ for (const [key, label] of [
221
+ ['ci_url', '--ci-url'], ['npm_integrity', '--npm-integrity'], ['release_url', '--release-url'],
222
+ ]) if (!evidence[key]) throw new Error(`delivery com publish requer ${label}`);
223
+ }
224
+ const receipt = {
225
+ schema_version: 1,
226
+ delivery_id: state.id,
227
+ outcome: 'completed',
228
+ work_kind: 'delivery',
229
+ source_change: state.route.source_change || '',
230
+ source_commit: state.source_commit,
231
+ target,
232
+ target_commit: targetCommit,
233
+ capabilities,
234
+ evidence,
235
+ finished_at: now.toISOString(),
236
+ };
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);
240
+ return receipt;
241
+ }
242
+
243
+ export function abandonDelivery({ vaultBase, id, reason, now = new Date() }) {
244
+ const state = readState(vaultBase, safeId(id));
245
+ if (state.state !== 'active') throw new Error(`delivery ${id} não está ativa`);
246
+ if (!String(reason || '').trim()) throw new Error('delivery abandon requer --reason <text>');
247
+ const receipt = {
248
+ schema_version: 1, delivery_id: state.id, outcome: 'abandoned',
249
+ reason: String(reason).trim(), abandoned_at: now.toISOString(),
250
+ };
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);
254
+ return receipt;
255
+ }
256
+
257
+ function emit(payload, json) {
258
+ if (json) process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
259
+ else process.stdout.write(`${payload.id || payload.delivery_id}: ${payload.state || payload.outcome}\n`);
260
+ }
261
+
262
+ export function runDelivery(argv = []) {
263
+ try {
264
+ const parsed = parseArgv(argv);
265
+ const sub = parsed.positionals[0] || 'status';
266
+ const { repoRoot, vaultBase } = context(parsed);
267
+ if (sub === 'start') {
268
+ const state = startDelivery({
269
+ vaultBase, repoRoot, id: parsed.positionals[1], capabilities: parsed.all('--allow'),
270
+ sourceChange: parsed.value('--source-change'), sourceCommit: parsed.value('--source-commit'),
271
+ });
272
+ emit(state, parsed.json);
273
+ return 0;
274
+ }
275
+ if (sub === 'status') {
276
+ const state = readState(vaultBase, currentId(parsed, vaultBase));
277
+ emit(state, parsed.json);
278
+ return 0;
279
+ }
280
+ if (sub === 'finish') {
281
+ const receipt = finishDelivery({
282
+ vaultBase, repoRoot, id: currentId(parsed, vaultBase), target: parsed.value('--target') || 'HEAD',
283
+ evidence: {
284
+ ci_url: parsed.value('--ci-url'), version: parsed.value('--version'),
285
+ npm_integrity: parsed.value('--npm-integrity'), release_url: parsed.value('--release-url'),
286
+ },
287
+ });
288
+ emit(receipt, parsed.json);
289
+ return 0;
290
+ }
291
+ if (sub === 'abandon') {
292
+ const receipt = abandonDelivery({
293
+ vaultBase, id: currentId(parsed, vaultBase), reason: parsed.value('--reason'),
294
+ });
295
+ emit(receipt, parsed.json);
296
+ return 0;
297
+ }
298
+ throw new Error(`subcomando desconhecido: ${sub}`);
299
+ } catch (error) {
300
+ process.stderr.write(`wendkeep delivery: ${error.message}\n`);
301
+ return 2;
302
+ }
303
+ }
package/src/doctor.mjs CHANGED
@@ -7,7 +7,7 @@ import { checkSyncDefs } from './sync-defs.mjs';
7
7
  import { resolveProjectVault } from './project-vault.mjs';
8
8
 
9
9
  const healthStatusLabel = (status) => ({
10
- healthy: 'saudável', warning: 'atenção', blocked: 'bloqueada', legacy: 'legado',
10
+ healthy: 'saudável', warning: 'atenção', degraded: 'degradada', blocked: 'bloqueada', legacy: 'legado',
11
11
  }[status] || status || 'desconhecido');
12
12
 
13
13
  const metricValue = (value) => value === null || value === undefined || value === '' ? 'n/a' : value;
@@ -55,6 +55,8 @@ export function runDoctor(argv) {
55
55
  let vault;
56
56
  let project;
57
57
  let session = '';
58
+ let scope = 'all';
59
+ let strict = false;
58
60
  for (let i = 0; i < argv.length; i += 1) {
59
61
  const a = argv[i];
60
62
  if (a === '--vault') vault = argv[++i];
@@ -63,6 +65,13 @@ export function runDoctor(argv) {
63
65
  else if (a.startsWith('--project=')) project = a.slice(10);
64
66
  else if (a === '--session') session = argv[++i] || '';
65
67
  else if (a.startsWith('--session=')) session = a.slice(10);
68
+ else if (a === '--scope') scope = argv[++i] || '';
69
+ else if (a.startsWith('--scope=')) scope = a.slice(8);
70
+ else if (a === '--strict') strict = true;
71
+ }
72
+ if (!['all', 'core', 'runtime'].includes(scope)) {
73
+ process.stderr.write('wendkeep doctor: --scope deve ser all, core ou runtime\n');
74
+ return 2;
66
75
  }
67
76
 
68
77
  const projectRoot = resolve(project || process.cwd());
@@ -97,18 +106,29 @@ export function runDoctor(argv) {
97
106
  memoryStatus: 'blocked',
98
107
  };
99
108
  }
100
- process.stdout.write(`${renderVaultHealthLines(health).join('\n')}\n`);
109
+ if (scope !== 'runtime') process.stdout.write(`${renderVaultHealthLines(health).join('\n')}\n`);
101
110
  const healthStatus = health.ok ? 0 : 1;
102
111
 
112
+ if (scope === 'core') {
113
+ const strictDebt = strict && (
114
+ (health.warnings || []).length > 0
115
+ || !['healthy'].includes(health.memoryStatus)
116
+ );
117
+ process.stdout.write(`\n[core] ${healthStatus ? 'erro estrutural' : health.memoryStatus === 'degraded' ? 'saudável com memória degradada' : 'saudável'}\n`);
118
+ return healthStatus || strictDebt ? 1 : 0;
119
+ }
120
+
103
121
  // 2. Harness integrity (Wave B).
104
- const { errors, warnings } = checkHarness(vaultBase, projectRoot);
122
+ const { errors, warnings, attention, repairable } = checkHarness(vaultBase, projectRoot);
105
123
  const defs = checkSyncDefs(vaultBase, projectRoot);
106
124
  if (!defs.ok) {
107
- warnings.push(...defs.issues.map((issue) => `defs: ${issue}`));
108
- warnings.push('defs stale — rode `wendkeep sync-defs --reseed` e reinicie Claude Code/Codex');
125
+ repairable.push(...defs.issues.map((issue) => `defs: ${issue}`));
126
+ repairable.push('defs stale — rode `wendkeep sync-defs --reseed` e reinicie Claude Code/Codex');
109
127
  }
110
- process.stdout.write(`\n[harness] ${errors.length} erro(s), ${warnings.length} aviso(s)\n`);
128
+ process.stdout.write(`\n[runtime] ${errors.length} erro(s) estrutural(is), ${attention.length} atenção(ões), ${repairable.length} reparável(is), ${warnings.length} aviso(s)\n`);
111
129
  for (const e of errors) process.stdout.write(` ✗ ${e}\n`);
130
+ for (const item of attention) process.stdout.write(` ! ${item}\n`);
131
+ for (const item of repairable) process.stdout.write(` → ${item}\n`);
112
132
  for (const w of warnings) process.stdout.write(` ! ${w}\n`);
113
133
 
114
134
  // 3. Link/graph health — órfãos que o grafo do Obsidian mostraria, com o comando de reparo.
@@ -125,13 +145,16 @@ export function runDoctor(argv) {
125
145
  process.stdout.write(`\n${renderStackedFrontmatterLines(vaultBase, stacked).join('\n')}\n`);
126
146
 
127
147
  // 3c. Modelo fora de pricing.json fecha a sessão com custo zero, sem erro — só aparece aqui.
128
- process.stdout.write(`\n${renderUnpricedModelLines(checkUnpricedModels(vaultBase)).join('\n')}\n`);
148
+ const unpriced = checkUnpricedModels(vaultBase);
149
+ process.stdout.write(`\n${renderUnpricedModelLines(unpriced).join('\n')}\n`);
129
150
 
130
151
  // 3d. Seções derivadas do corpo que ficaram para trás do Encerramento (notas pré-0.53.0).
131
- process.stdout.write(`\n${renderStaleDerivedSectionLines(checkStaleDerivedSections(vaultBase)).join('\n')}\n`);
152
+ const staleDerived = checkStaleDerivedSections(vaultBase);
153
+ process.stdout.write(`\n${renderStaleDerivedSectionLines(staleDerived).join('\n')}\n`);
132
154
 
133
155
  // 3e. Observabilidade materializada: schema vigente não basta sem frontier + manifest frescos.
134
- process.stdout.write(`\n${renderSessionObservabilityLines(checkSessionObservability(vaultBase)).join('\n')}\n`);
156
+ const observability = checkSessionObservability(vaultBase);
157
+ process.stdout.write(`\n${renderSessionObservabilityLines(observability).join('\n')}\n`);
135
158
 
136
159
  // 4. Sessão: não mente "inativa" quando há atividade recente (workflow/subagente em background).
137
160
  const act = checkSessionActivity(vaultBase);
@@ -146,5 +169,19 @@ export function runDoctor(argv) {
146
169
 
147
170
  // Devolve o código em vez de sair: `wendkeep sync` encadeia este comando, e um
148
171
  // process.exit aqui mataria a cadeia. Quem faz o exit é o bin.
149
- return healthStatus !== 0 || errors.length ? 1 : 0;
172
+ const strictDebt = strict && (
173
+ (scope !== 'runtime' && (health.warnings || []).length)
174
+ || (scope !== 'runtime' && health.memoryStatus !== 'healthy')
175
+ || attention.length
176
+ || repairable.length
177
+ || warnings.length
178
+ || links.derivedOrphans
179
+ || links.artifactOrphans
180
+ || links.graphColors === false
181
+ || stacked.count
182
+ || (unpriced.models || unpriced.items || []).length
183
+ || (staleDerived.notes || staleDerived.items || []).length
184
+ || !observability.ok
185
+ );
186
+ return (scope !== 'runtime' && healthStatus !== 0) || errors.length || strictDebt ? 1 : 0;
150
187
  }
package/src/init.mjs CHANGED
@@ -323,7 +323,7 @@ const MESSAGES = {
323
323
  mcpSkipped: ' [4/5] .mcp.json ignorado (--no-mcp, sem companions MCP)',
324
324
  colorsSkipped: ' [5/5] cores ignoradas (--no-colors)',
325
325
  colors: (r) => ` [5/5] cores: ${r}`,
326
- runtimeIgnore: ' [!] ignore runtimes locais do wendkeep no Git quando o vault for versionado: .brain/.change-* .brain/runtime/flows/',
326
+ runtimeIgnore: ' [!] ignore runtimes locais do wendkeep no Git quando o vault for versionado: .brain/.change-* .brain/runtime/flows/ .brain/observer-sql-state.json .brain/observer-sql-outbox/',
327
327
  merged: 'mesclado', created: 'criado', bakSaved: ', .bak salvo',
328
328
  nextSteps: '\nPróximos passos:',
329
329
  step1: (v) => ` 1. Abra o vault no Obsidian: "Abrir pasta como cofre" -> ${v}`,
@@ -352,7 +352,7 @@ const MESSAGES = {
352
352
  mcpSkipped: ' [4/5] .mcp.json skipped (--no-mcp, no MCP companions)',
353
353
  colorsSkipped: ' [5/5] colors skipped (--no-colors)',
354
354
  colors: (r) => ` [5/5] colors: ${r}`,
355
- runtimeIgnore: ' [!] keep local wendkeep runtimes out of Git when the vault is versioned: .brain/.change-* .brain/runtime/flows/',
355
+ runtimeIgnore: ' [!] keep local wendkeep runtimes out of Git when the vault is versioned: .brain/.change-* .brain/runtime/flows/ .brain/observer-sql-state.json .brain/observer-sql-outbox/',
356
356
  merged: 'merged', created: 'created', bakSaved: ', .bak saved',
357
357
  nextSteps: '\nNext steps:',
358
358
  step1: (v) => ` 1. Open the vault in Obsidian: "Open folder as vault" -> ${v}`,
@@ -0,0 +1,10 @@
1
+ export function resolveObserverToken(value = '') {
2
+ return String(value || process.env.WENDKEEP_OBSERVER_TOKEN || '').trim();
3
+ }
4
+
5
+ export function observerAuthHeaders(token, headers = {}) {
6
+ const resolved = resolveObserverToken(token);
7
+ return resolved
8
+ ? { ...headers, authorization: `Bearer ${resolved}` }
9
+ : { ...headers };
10
+ }
@@ -12,6 +12,8 @@ import {
12
12
  } from 'node:fs';
13
13
  import { join } from 'node:path';
14
14
  import { MAX_MEMORY_CONTENT_BYTES } from './observer-memory.mjs';
15
+ import { observerAuthHeaders } from './observer-auth.mjs';
16
+ import { sanitizeObserverContent } from './observer-privacy.mjs';
15
17
 
16
18
  export const MEMORY_OUTBOX_REL = '.brain/observer-memory-outbox';
17
19
  export const MEMORY_STATE_FILE = '.brain/observer-memory-state.json';
@@ -103,7 +105,7 @@ function memoryFiles(vaultBase) {
103
105
 
104
106
  export function localMemoryManifest(vaultBase) {
105
107
  return Object.fromEntries(memoryFiles(vaultBase).map((file) => {
106
- const content = readFileSync(file.absolute, 'utf8');
108
+ const content = sanitizeObserverContent(readFileSync(file.absolute, 'utf8'));
107
109
  return [file.logicalPath, {
108
110
  logical_path: file.logicalPath,
109
111
  content_hash: hash(content),
@@ -159,7 +161,7 @@ export function buildMemoryEventBatch({
159
161
  const events = [];
160
162
 
161
163
  for (const file of currentFiles) {
162
- const content = readFileSync(file.absolute, 'utf8');
164
+ const content = sanitizeObserverContent(readFileSync(file.absolute, 'utf8'));
163
165
  if (Buffer.byteLength(content, 'utf8') > MAX_MEMORY_CONTENT_BYTES) {
164
166
  throw new Error('arquivo excede o limite de memória: ' + file.logicalPath);
165
167
  }
@@ -231,12 +233,12 @@ export function listMemoryOutbox(vaultBase) {
231
233
  .map((name) => join(dir, name));
232
234
  }
233
235
 
234
- async function postBatch(url, projectId, events, fetchImpl = globalThis.fetch) {
236
+ async function postBatch(url, projectId, events, fetchImpl = globalThis.fetch, token = '') {
235
237
  const response = await fetchImpl(
236
238
  String(url).replace(/\/$/, '') + '/v1/projects/' + encodeURIComponent(projectId) + '/memory/events',
237
239
  {
238
240
  method: 'POST',
239
- headers: { 'content-type': 'application/json', accept: 'application/json' },
241
+ headers: observerAuthHeaders(token, { 'content-type': 'application/json', accept: 'application/json' }),
240
242
  body: JSON.stringify({ events }),
241
243
  },
242
244
  );
@@ -249,6 +251,7 @@ export async function retryObserverMemoryOutbox({
249
251
  projectId = projectIdFromVault(vaultBase),
250
252
  url,
251
253
  fetchImpl = globalThis.fetch,
254
+ token = process.env.WENDKEEP_OBSERVER_TOKEN || '',
252
255
  } = {}) {
253
256
  const files = listMemoryOutbox(vaultBase);
254
257
  if (!url) return { attempted: 0, confirmed: 0, pending: files.length };
@@ -262,7 +265,7 @@ export async function retryObserverMemoryOutbox({
262
265
  unlinkSync(path);
263
266
  continue;
264
267
  }
265
- await postBatch(url, projectId, batch.events, fetchImpl);
268
+ await postBatch(url, projectId, batch.events, fetchImpl, token);
266
269
  unlinkSync(path);
267
270
  confirmed += 1;
268
271
  } catch {
@@ -277,10 +280,11 @@ export async function compareMemoryParity({
277
280
  projectId = projectIdFromVault(vaultBase),
278
281
  url,
279
282
  fetchImpl = globalThis.fetch,
283
+ token = process.env.WENDKEEP_OBSERVER_TOKEN || '',
280
284
  } = {}) {
281
285
  const response = await fetchImpl(
282
286
  String(url).replace(/\/$/, '') + '/v1/projects/' + encodeURIComponent(projectId) + '/memory/tree',
283
- { headers: { accept: 'application/json' } },
287
+ { headers: observerAuthHeaders(token, { accept: 'application/json' }) },
284
288
  );
285
289
  if (!response.ok) throw new Error('Observer respondeu HTTP ' + response.status + '.');
286
290
  const body = await response.json();
@@ -309,9 +313,10 @@ export async function publishObserverMemory({
309
313
  sourceTurnId = '',
310
314
  now = new Date(),
311
315
  fetchImpl = globalThis.fetch,
316
+ token = process.env.WENDKEEP_OBSERVER_TOKEN || '',
312
317
  } = {}) {
313
318
  if (!vaultBase || !projectId) throw new Error('vaultBase e projectId são obrigatórios.');
314
- await retryObserverMemoryOutbox({ vaultBase, projectId, url, fetchImpl });
319
+ await retryObserverMemoryOutbox({ vaultBase, projectId, url, fetchImpl, token });
315
320
  const state = readState(vaultBase);
316
321
  const batch = buildMemoryEventBatch({ vaultBase, projectId, sourceSessionId, sourceTurnId, now, state });
317
322
  if (batch.events.length === 0) {
@@ -324,7 +329,7 @@ export async function publishObserverMemory({
324
329
  return { ok: false, queued: true, scanned: batch.scanned, changed: batch.changed, pending: listMemoryOutbox(vaultBase).length, hookExitCode: 0 };
325
330
  }
326
331
  try {
327
- await postBatch(url, projectId, batch.events, fetchImpl);
332
+ await postBatch(url, projectId, batch.events, fetchImpl, token);
328
333
  commitMemoryPublishState(vaultBase, batch.nextState);
329
334
  return { ok: true, queued: false, scanned: batch.scanned, changed: batch.changed, pending: listMemoryOutbox(vaultBase).length };
330
335
  } catch (error) {