wendkeep 0.67.2 → 0.67.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ All notable changes to **wendkeep** are documented here. Format based on
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project follows
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.67.3] — 2026-08-02
8
+
9
+ ### Fixed
10
+
11
+ - **Conflitos semânticos agora levam a uma curadoria humana executável.** O doctor diferencia
12
+ candidates em conflito de corrupção estrutural, explica que `memory repair` não escolhe um
13
+ vencedor e mostra `memory candidates --active --vault <vault>` com o Vault resolvido, seguido
14
+ pelos modelos explícitos para promover ou rejeitar a decisão.
15
+ - **A inspeção de candidates deixa de exigir leitura direta do sidecar.** O novo comando read-only
16
+ `memory candidates [--active]` devolve somente `candidate_id`, `reason`, `status`, `memory_key` e
17
+ `event_ids`, sem valores ou conteúdo, em ordem determinística e sem alterar o bundle de memória.
18
+
7
19
  ## [0.67.2] — 2026-08-02
8
20
 
9
21
  ### Fixed
package/README.en.md CHANGED
@@ -369,8 +369,10 @@ backup/audit; divergent mirrors fail closed. A demonstrably superseded
369
369
  ambiguity uses `memory reconcile <session> --by-session <successor>
370
370
  --reason <reason>` as a dry run and requires `--apply`; the decision is backed up and audited
371
371
  without rewriting ledger, CORE, or notes. Run `status --gate` again afterwards. Conflicts require
372
- explicit, durable curation: `memory promote <id> --event <event-id>` selects one event from the
373
- candidate, while `memory reject <id>` keeps the current value. The decision is idempotent, and a
372
+ explicit, durable curation: `memory candidates --active --vault <vault>` lists only safe IDs and
373
+ metadata in read-only mode it does not expose memory values or content. After human review,
374
+ `memory promote <id> --event <event-id>` selects one event from the candidate, while
375
+ `memory reject <id>` keeps the current value. `memory repair` does not choose a winner. The decision is idempotent, and a
374
376
  new promotion accepts a later Stop from the same session/activation without recreating a conflict.
375
377
  A promotion written by 0.66.1 remains historical: if the next Stop forms a new candidate, update
376
378
  to 0.66.3 and run `memory repair`. During replay, a transient candidate is re-evaluated against the
package/README.md CHANGED
@@ -369,8 +369,10 @@ backup/audit; divergent mirrors fail closed. A demonstrably superseded
369
369
  ambiguity uses `memory reconcile <session> --by-session <successor>
370
370
  --reason <reason>` as a dry run and requires `--apply`; the decision is backed up and audited
371
371
  without rewriting ledger, CORE, or notes. Run `status --gate` again afterwards. Conflicts require
372
- explicit, durable curation: `memory promote <id> --event <event-id>` selects one event from the
373
- candidate, while `memory reject <id>` keeps the current value. The decision is idempotent, and a
372
+ explicit, durable curation: `memory candidates --active --vault <vault>` lists only safe IDs and
373
+ metadata in read-only mode it does not expose memory values or content. After human review,
374
+ `memory promote <id> --event <event-id>` selects one event from the candidate, while
375
+ `memory reject <id>` keeps the current value. `memory repair` does not choose a winner. The decision is idempotent, and a
374
376
  new promotion accepts a later Stop from the same session/activation without recreating a conflict.
375
377
  A promotion written by 0.66.1 remains historical: if the next Stop forms a new candidate, update
376
378
  to 0.66.3 and run `memory repair`. During replay, a transient candidate is re-evaluated against the
@@ -57,6 +57,7 @@ npx --no-install wendkeep --version
57
57
  npx --no-install wendkeep sync-defs --check --vault .MyApp-vault --project .
58
58
  npx --no-install wendkeep doctor --vault .MyApp-vault
59
59
  npx --no-install wendkeep memory status --gate --vault .MyApp-vault
60
+ npx --no-install wendkeep memory candidates --active --vault .MyApp-vault
60
61
  npx --no-install wendkeep cost rebuild --session <id> --json --vault .MyApp-vault
61
62
  npx --no-install wendkeep cost rebuild --session <id> --json --vault .MyApp-vault --apply
62
63
  ```
@@ -74,11 +75,17 @@ dry-run path before any write.
74
75
 
75
76
  - `no vault`: run from the bound root or pass `--vault`.
76
77
  - `defs stale`: confirm the version and run `sync-defs --reseed`.
77
- - Legacy vault: this is a non-blocking warning; plan `memory migrate --apply` separately.
78
+ - Legacy vault: this is a non-blocking warning; doctor shows
79
+ `npx --no-install wendkeep memory migrate --apply --vault <vault>` with the resolved Vault, but
80
+ migration remains opt-in and must be planned separately.
78
81
  - `degraded` plus an intact outbox: warning; preserve the outbox and allow idempotent replay.
79
82
  - `ambiguous`, lost publication, or a mismatched checkpoint: blocking; preserve registry, ledger,
80
83
  outbox, and SHARED so `last_memory_attempt` can be correlated before repair.
81
84
  - Corrupt bundle: preserve evidence and run `memory status --gate` before `memory repair`.
85
+ - An active semantic conflict requires a human decision: `memory repair` does not choose a winner.
86
+ List safe IDs with `memory candidates --active --vault <vault>`, review the evidence, and then use
87
+ `memory promote <candidate-id> --event <event-id> --vault <vault>` to select an event or
88
+ `memory reject <candidate-id> --vault <vault>` to keep the current operational value.
82
89
  - `legacy`/`degraded`/`stale`/`manifest-unproven` observability: run
83
90
  `npx --no-install wendkeep cost rebuild --session <id> --json --vault <vault>`, review diagnostics,
84
91
  and only then authorize the second variant with `--apply`.
@@ -24,6 +24,7 @@ Pass the vault explicitly in automation. Preserve backups and evidence before re
24
24
 
25
25
  ```bash
26
26
  npx wendkeep memory status [--gate] --vault <vault>
27
+ npx wendkeep memory candidates [--active] --vault <vault>
27
28
  npx wendkeep memory repair --vault <vault>
28
29
  npx wendkeep memory recover-attempt <session> [--apply] --vault <vault>
29
30
  npx wendkeep memory reconcile <ambiguous-session> --by-session <successor-session> --reason <reason> [--apply] --vault <vault>
@@ -36,6 +37,13 @@ npx wendkeep validate-memory --vault <v2-vault>
36
37
  ## Options and exit codes
37
38
 
38
39
  - `memory status` is read-only; `--gate` exits `1` only for blocking state.
40
+ - `memory candidates` is read-only and prints deterministic JSON containing only `candidate_id`,
41
+ `reason`, `status`, `memory_key`, and `event_ids`; it does not expose memory values or content and
42
+ does not create a lock or mutate the bundle. `--active` omits terminal candidates (`resolved`,
43
+ `rejected`, and `superseded`). A missing status is normalized to `active`.
44
+ - For `memory candidates`, exit `0` means a valid inventory (including empty or conflicted), exit
45
+ `1` means an invalid sidecar/unsafe topology, and exit `2` means a missing `--vault`, unknown or
46
+ duplicate option, extra argument, or an invalid value passed to `--active`.
39
47
  - `Stop` writes events to the outbox before acknowledging `last_memory_attempt: enqueued`, then the
40
48
  projector runs outside the registry lock. Retrying the same attempt reuses its frozen event IDs
41
49
  and can project them at most once.
@@ -105,6 +113,7 @@ npx wendkeep validate-memory --vault <v2-vault>
105
113
 
106
114
  ```bash
107
115
  npx wendkeep memory status --gate --vault .MyApp-vault
116
+ npx wendkeep memory candidates --active --vault .MyApp-vault
108
117
  npx wendkeep memory recover-attempt session-123 --vault .MyApp-vault
109
118
  npx wendkeep memory recover-attempt session-123 --apply --vault .MyApp-vault
110
119
  npx wendkeep memory reconcile old --by-session current --reason "delivery continued" --vault .MyApp-vault
@@ -121,6 +130,9 @@ state of the last attempt. CORE stays hand-curated and canonical; SHARED stays a
121
130
  operational projection. After successful projection, an attempt checkpoint may be a valid prefix
122
131
  of a global projection that has already advanced with concurrent events.
123
132
 
133
+ `memory candidates` returns `status: "ok"` and the sanitized candidate list in stable order; with
134
+ `--active`, the list contains only decisions still open for human curation.
135
+
124
136
  ## Common errors and diagnosis
125
137
 
126
138
  - `legacy`: follow the migration guide; this is not corruption.
@@ -57,6 +57,7 @@ npx --no-install wendkeep --version
57
57
  npx --no-install wendkeep sync-defs --check --vault .MeuApp-vault --project .
58
58
  npx --no-install wendkeep doctor --vault .MeuApp-vault
59
59
  npx --no-install wendkeep memory status --gate --vault .MeuApp-vault
60
+ npx --no-install wendkeep memory candidates --active --vault .MeuApp-vault
60
61
  npx --no-install wendkeep cost rebuild --session <id> --json --vault .MeuApp-vault
61
62
  npx --no-install wendkeep cost rebuild --session <id> --json --vault .MeuApp-vault --apply
62
63
  ```
@@ -74,11 +75,17 @@ ou sem manifest comprovado e oferece um caminho dry-run antes de qualquer escrit
74
75
 
75
76
  - `no vault`: execute da raiz vinculada ou passe `--vault`.
76
77
  - `defs stale`: confirme a versão e rode `sync-defs --reseed`.
77
- - Vault legado: é warning não bloqueante; planeje `memory migrate --apply` separadamente.
78
+ - Vault legado: é warning não bloqueante; o doctor mostra
79
+ `npx --no-install wendkeep memory migrate --apply --vault <cofre>` com o Vault resolvido, mas a
80
+ migração continua sendo opt-in e deve ser planejada separadamente.
78
81
  - `degraded` + outbox íntegra: warning; preserve a outbox e permita replay idempotente.
79
82
  - `ambiguous`, publicação perdida ou checkpoint divergente: bloqueante; preserve registry, ledger,
80
83
  outbox e SHARED para correlacionar `last_memory_attempt` antes de reparar.
81
84
  - Bundle corrompido: preserve a evidência e use `memory status --gate` antes de `memory repair`.
85
+ - Conflito semântico ativo exige decisão humana: `memory repair` não escolhe vencedor. Liste os IDs
86
+ seguros com `memory candidates --active --vault <cofre>`, revise a evidência e então use
87
+ `memory promote <candidate-id> --event <event-id> --vault <cofre>` para selecionar um evento ou
88
+ `memory reject <candidate-id> --vault <cofre>` para manter o valor operacional atual.
82
89
  - Observabilidade `legacy`/`degraded`/`stale`/`manifest-unproven`: rode
83
90
  `npx --no-install wendkeep cost rebuild --session <id> --json --vault <cofre>`, revise diagnostics
84
91
  e só então autorize a segunda variante com `--apply`.
@@ -24,6 +24,7 @@ Informe o vault explicitamente em automações. Preserve backups e evidências a
24
24
 
25
25
  ```bash
26
26
  npx wendkeep memory status [--gate] --vault <cofre>
27
+ npx wendkeep memory candidates [--active] --vault <cofre>
27
28
  npx wendkeep memory repair --vault <cofre>
28
29
  npx wendkeep memory recover-attempt <sessão> [--apply] --vault <cofre>
29
30
  npx wendkeep memory reconcile <sessão-ambígua> --by-session <sessão-sucessora> --reason <motivo> [--apply] --vault <cofre>
@@ -36,6 +37,13 @@ npx wendkeep validate-memory --vault <cofre-v2>
36
37
  ## Opções e códigos de saída
37
38
 
38
39
  - `memory status` é read-only; `--gate` retorna exit `1` apenas para estado bloqueante.
40
+ - `memory candidates` é read-only e imprime JSON determinístico com somente `candidate_id`,
41
+ `reason`, `status`, `memory_key` e `event_ids`; não expõe valores nem conteúdo da memória e não
42
+ cria lock nem altera o bundle. `--active` omite candidates terminais (`resolved`, `rejected` e
43
+ `superseded`). Status ausente é normalizado para `active`.
44
+ - Em `memory candidates`, exit `0` indica inventário válido (inclusive vazio ou com conflitos),
45
+ exit `1` indica sidecar inválido/topologia insegura e exit `2` indica `--vault` ausente, opção
46
+ desconhecida/duplicada, argumento extra ou valor indevido em `--active`.
39
47
  - O `Stop` grava os eventos na outbox antes de reconhecer `last_memory_attempt: enqueued`; depois o
40
48
  projector roda fora do lock do registry. Retry do mesmo attempt reutiliza os event IDs congelados
41
49
  e pode projetá-los no máximo uma vez.
@@ -103,6 +111,7 @@ npx wendkeep validate-memory --vault <cofre-v2>
103
111
 
104
112
  ```bash
105
113
  npx wendkeep memory status --gate --vault .MeuApp-vault
114
+ npx wendkeep memory candidates --active --vault .MeuApp-vault
106
115
  npx wendkeep memory recover-attempt sessao-123 --vault .MeuApp-vault
107
116
  npx wendkeep memory recover-attempt sessao-123 --apply --vault .MeuApp-vault
108
117
  npx wendkeep memory reconcile antiga --by-session atual --reason "entrega continuada" --vault .MeuApp-vault
@@ -119,6 +128,9 @@ causal do último attempt. CORE permanece canônico e curado à mão; SHARED per
119
128
  operacional verificável. Depois de uma projeção bem-sucedida, o checkpoint do attempt pode ser um
120
129
  prefixo válido de uma projeção global que já avançou com eventos concorrentes.
121
130
 
131
+ `memory candidates` retorna `status: "ok"` e a lista sanitizada de candidates em ordem estável;
132
+ com `--active`, a lista contém somente decisões ainda abertas para curadoria humana.
133
+
122
134
  ## Erros comuns e diagnóstico
123
135
 
124
136
  - `legacy`: siga o guia de migração; não é corrupção.
@@ -106,6 +106,18 @@ const memoryStatusCommand = (vaultBase) => (
106
106
  const memoryRepairCommand = (vaultBase) => (
107
107
  `${WENDKEEP_COMMAND} memory repair --vault ${quoteCommandArgument(vaultBase)}`
108
108
  );
109
+ const memoryMigrateCommand = (vaultBase) => (
110
+ `${WENDKEEP_COMMAND} memory migrate --apply --vault ${quoteCommandArgument(vaultBase)}`
111
+ );
112
+ const memoryCandidatesCommand = (vaultBase) => (
113
+ `${WENDKEEP_COMMAND} memory candidates --active --vault ${quoteCommandArgument(vaultBase)}`
114
+ );
115
+ const memoryPromoteCommand = (vaultBase) => (
116
+ `${WENDKEEP_COMMAND} memory promote <candidate-id> --event <event-id> --vault ${quoteCommandArgument(vaultBase)}`
117
+ );
118
+ const memoryRejectCommand = (vaultBase) => (
119
+ `${WENDKEEP_COMMAND} memory reject <candidate-id> --vault ${quoteCommandArgument(vaultBase)}`
120
+ );
109
121
 
110
122
  function readJsonLines(vaultBase, path, label) {
111
123
  let checked;
@@ -397,7 +409,7 @@ export function checkMemoryBundle(vaultBase, { registry } = {}) {
397
409
  ok: true,
398
410
  status: 'legacy',
399
411
  failures: [],
400
- warnings: [LEGACY_MEMORY_WARNING],
412
+ warnings: [`${LEGACY_MEMORY_WARNING} Comando com Vault resolvido: ${memoryMigrateCommand(vaultBase)}.`],
401
413
  metrics: memoryMetrics(),
402
414
  };
403
415
  }
@@ -466,7 +478,12 @@ export function checkMemoryBundle(vaultBase, { registry } = {}) {
466
478
  const activeConflicts = unresolved.filter((item) => item?.reason === 'conflict');
467
479
  const ordinaryCandidates = unresolved.filter((item) => item?.reason !== 'conflict');
468
480
  if (activeConflicts.length) {
469
- failures.push(`${activeConflicts.length} conflito ativo em chave operacional (${activeConflicts.map((item) => item.memory_key || item.candidate_id).join(', ')}). Inspecione com: ${memoryStatusCommand(vaultBase)}.`);
481
+ const keys = [...new Set(activeConflicts.map((item) => item.memory_key || item.candidate_id))]
482
+ .sort((left, right) => String(left).localeCompare(String(right)));
483
+ const label = activeConflicts.length === 1
484
+ ? '1 conflito ativo'
485
+ : `${activeConflicts.length} conflitos ativos`;
486
+ failures.push(`${label} em chave operacional (${keys.join(', ')}). Conflito semântico exige curadoria humana; memory repair não escolhe vencedor. Liste IDs seguros com: ${memoryCandidatesCommand(vaultBase)}. Depois da revisão, use ${memoryPromoteCommand(vaultBase)} ou ${memoryRejectCommand(vaultBase)}.`);
470
487
  }
471
488
  if (outbox.count) warnings.push(`${outbox.count} evento(s) pendente(s) na outbox; execute o projector quando seguro.`);
472
489
  if (ordinaryCandidates.length) warnings.push(`${ordinaryCandidates.length} candidate(s) aguardando curadoria humana.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.67.2",
3
+ "version": "0.67.3",
4
4
  "description": "Vault-first persistent memory for AI coding agents, with an optional profile-aware governance runtime: OFF, FLOW, GUIDE, GOVERN, or ASSURE. Local-first and agent-agnostic (Claude Code, Codex, Cursor…).",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -70,6 +70,6 @@
70
70
  },
71
71
  "devDependencies": {
72
72
  "acorn": "^8.18.0",
73
- "wendkeep": "^0.67.1"
73
+ "wendkeep": "^0.67.2"
74
74
  }
75
75
  }
@@ -99,7 +99,7 @@ Usage:
99
99
  in session notes from the linked derived notes — the body used to lag
100
100
  behind the closing block. Dry-run by default · --apply · --json.
101
101
  wendkeep lesson add "t" "l" Record a project-local lesson (injected at SessionStart).
102
- wendkeep memory <sub> Shared memory v2: status | migrate [--apply] | repair |
102
+ wendkeep memory <sub> Shared memory v2: status | candidates [--active] | migrate [--apply] | repair |
103
103
  recover-attempt <session> [--apply] |
104
104
  reconcile <session> --by-session <session> --reason <text> [--apply] |
105
105
  promote <candidate> [--event <event-id>] | reject <candidate>. --vault P.
package/src/memory.mjs CHANGED
@@ -237,6 +237,48 @@ function readCandidates(vault) {
237
237
  .split('\n').filter(Boolean).map((line) => JSON.parse(line));
238
238
  }
239
239
 
240
+ const TERMINAL_CANDIDATE_STATUSES = new Set(['resolved', 'rejected', 'superseded']);
241
+
242
+ function lexicalCompare(left, right) {
243
+ if (left < right) return -1;
244
+ if (left > right) return 1;
245
+ return 0;
246
+ }
247
+
248
+ function sanitizedCandidate(candidate, index) {
249
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
250
+ throw new Error(`MEMORY_CANDIDATES.jsonl: candidate ${index + 1} inválido.`);
251
+ }
252
+ const required = ['candidate_id', 'reason', 'memory_key'];
253
+ const missing = required.filter((field) => typeof candidate[field] !== 'string' || !candidate[field]);
254
+ if (missing.length) {
255
+ throw new Error(`MEMORY_CANDIDATES.jsonl: candidate ${index + 1} sem ${missing.join(', ')}.`);
256
+ }
257
+ if (candidate.status !== undefined && (typeof candidate.status !== 'string' || !candidate.status)) {
258
+ throw new Error(`MEMORY_CANDIDATES.jsonl: candidate ${index + 1} possui status inválido.`);
259
+ }
260
+ const eventIds = candidate.event_ids ?? [];
261
+ if (!Array.isArray(eventIds) || eventIds.some((eventId) => typeof eventId !== 'string' || !eventId)) {
262
+ throw new Error(`MEMORY_CANDIDATES.jsonl: candidate ${index + 1} possui event_ids inválidos.`);
263
+ }
264
+ return {
265
+ candidate_id: candidate.candidate_id,
266
+ reason: candidate.reason,
267
+ status: candidate.status || 'active',
268
+ memory_key: candidate.memory_key,
269
+ event_ids: [...eventIds].sort(lexicalCompare),
270
+ };
271
+ }
272
+
273
+ export function listMemoryCandidates(vault, { activeOnly = false } = {}) {
274
+ const candidates = readCandidates(vault)
275
+ .map(sanitizedCandidate)
276
+ .filter((candidate) => !activeOnly || !TERMINAL_CANDIDATE_STATUSES.has(candidate.status))
277
+ .sort((left, right) => lexicalCompare(left.memory_key, right.memory_key)
278
+ || lexicalCompare(left.candidate_id, right.candidate_id));
279
+ return { status: 'ok', candidates };
280
+ }
281
+
240
282
  function priorCandidateDecision(vault, candidateId) {
241
283
  return readMemoryLedger(vault).events.find(
242
284
  (event) => event.candidate_decision?.candidate_id === candidateId,
@@ -1897,10 +1939,49 @@ function parseRecoverAttemptArgs(argv) {
1897
1939
  };
1898
1940
  }
1899
1941
 
1942
+ function parseCandidatesArgs(argv) {
1943
+ const seen = new Set();
1944
+ let activeOnly = false;
1945
+ let vault = '';
1946
+
1947
+ for (let index = 1; index < argv.length; index += 1) {
1948
+ const token = argv[index];
1949
+ if (!token.startsWith('--')) {
1950
+ throw memoryUsageError(`memory candidates recebeu argumento posicional extra: ${token}.`);
1951
+ }
1952
+
1953
+ const equalAt = token.indexOf('=');
1954
+ const name = equalAt >= 0 ? token.slice(0, equalAt) : token;
1955
+ if (name !== '--active' && name !== '--vault') {
1956
+ throw memoryUsageError(`memory candidates recebeu opção desconhecida: ${name}.`);
1957
+ }
1958
+ if (seen.has(name)) {
1959
+ throw memoryUsageError(`memory candidates recebeu opção duplicada: ${name}.`);
1960
+ }
1961
+ seen.add(name);
1962
+
1963
+ if (name === '--active') {
1964
+ if (equalAt >= 0) throw memoryUsageError('--active não aceita valor.');
1965
+ activeOnly = true;
1966
+ continue;
1967
+ }
1968
+
1969
+ const value = equalAt >= 0 ? token.slice(equalAt + 1) : argv[index + 1];
1970
+ if (!value || !value.trim() || value.startsWith('--')) {
1971
+ throw memoryUsageError('--vault requer valor não vazio que não comece com --.');
1972
+ }
1973
+ vault = value;
1974
+ if (equalAt < 0) index += 1;
1975
+ }
1976
+
1977
+ return { activeOnly, vault };
1978
+ }
1979
+
1900
1980
  export function runMemory(argv) {
1901
1981
  const [sub, positional] = argv;
1902
1982
  let reconcileArgs = null;
1903
1983
  let recoverAttemptArgs = null;
1984
+ let candidatesArgs = null;
1904
1985
  if (sub === 'reconcile') {
1905
1986
  try {
1906
1987
  reconcileArgs = parseReconcileArgs(argv);
@@ -1919,14 +2000,26 @@ export function runMemory(argv) {
1919
2000
  return;
1920
2001
  }
1921
2002
  }
2003
+ if (sub === 'candidates') {
2004
+ try {
2005
+ candidatesArgs = parseCandidatesArgs(argv);
2006
+ } catch (error) {
2007
+ process.stderr.write(`wendkeep memory: ${error.message}\n`);
2008
+ process.exitCode = error.code === 'WENDKEEP_MEMORY_USAGE' ? 2 : 1;
2009
+ return;
2010
+ }
2011
+ }
1922
2012
  const vault = (
1923
- recoverAttemptArgs?.vault || reconcileArgs?.vault || option(argv, '--vault')
2013
+ recoverAttemptArgs?.vault || reconcileArgs?.vault || candidatesArgs?.vault || option(argv, '--vault')
1924
2014
  ) || process.env.OBSIDIAN_VAULT_PATH;
1925
2015
  if (!vault) { process.stderr.write('wendkeep memory: passe --vault <path>.\n'); process.exitCode = 2; return; }
1926
2016
  if (!existsSync(vault)) { process.stderr.write(`wendkeep memory: not found: ${vault}\n`); process.exitCode = 2; return; }
1927
2017
  try {
1928
2018
  let result;
1929
2019
  if (sub === 'status') result = memoryStatus(vault);
2020
+ else if (sub === 'candidates') {
2021
+ result = listMemoryCandidates(vault, { activeOnly: candidatesArgs.activeOnly });
2022
+ }
1930
2023
  else if (sub === 'migrate') result = migrateMemory(vault, { apply: argv.includes('--apply') });
1931
2024
  else if (sub === 'repair') result = repairMemory(vault);
1932
2025
  else if (sub === 'reconcile') {
@@ -1950,7 +2043,7 @@ export function runMemory(argv) {
1950
2043
  action: sub, candidateId: positional, ...(eventId ? { eventId } : {}),
1951
2044
  });
1952
2045
  }
1953
- else { process.stderr.write('wendkeep memory: use status | migrate [--apply] | repair | recover-attempt <session> [--apply] | reconcile <session> --by-session <session> --reason <text> [--apply] | promote <candidate> [--event <event-id>] | reject <candidate>.\n'); process.exitCode = 2; return; }
2046
+ else { process.stderr.write('wendkeep memory: use status | candidates [--active] | migrate [--apply] | repair | recover-attempt <session> [--apply] | reconcile <session> --by-session <session> --reason <text> [--apply] | promote <candidate> [--event <event-id>] | reject <candidate>.\n'); process.exitCode = 2; return; }
1954
2047
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
1955
2048
  if (sub === 'status' && argv.includes('--gate')) process.exitCode = result.status === 'blocked' ? 1 : 0;
1956
2049
  else if (sub === 'reconcile' && reconcileArgs.apply) process.exitCode = result.health?.status === 'blocked' ? 1 : 0;