wendkeep 0.67.0 → 0.67.1

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,30 @@ 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.1] — 2026-08-02
8
+
9
+ ### Fixed
10
+
11
+ - **Turnos de subagents não bloqueiam mais a captura da sessão-mãe.** `UserPromptSubmit` de rollout
12
+ filho registra o path para observabilidade sem avançar a sequência principal, e `SessionStop`
13
+ prefere o mapeamento causal `turn_sequences[turn_id]` antes da ordem local do transcript. Isso
14
+ preserva os marcadores `wk-turn`, o encerramento e a memória mesmo com filhos intercalados.
15
+ - **`SubagentStop` consome o transcript correto do payload Codex.** O hook resolve o filho por
16
+ `agent_transcript_path`/`agentTranscriptPath`, valida identidade, sessão e `parent_thread_id`
17
+ contra um root comprovado e mantém `transcript_path` como contexto da sessão-mãe, permitindo
18
+ preencher `Subagents e workflows` no último filho sem aceitar sinais de outra cadeia.
19
+ - **O backfill não grava o turno Codex ainda em execução.** `hook session-backfill` exige
20
+ `task_complete`, separa `missingTurns` de `incompleteTurns`, permanece dry-run por padrão e aplica
21
+ somente turnos concluídos de forma idempotente. A CLI agora encaminha `--session`, `--vault`,
22
+ `--write` e demais argumentos ao hook executado. README e guias PT-BR/EN documentam o reparo.
23
+ - **O bloco gerenciado do `AGENTS.md` exige roteamento adaptativo antes de editar.** O harness
24
+ inspeciona o perfil-base, classifica a solicitação, registra uma lease temporária entre `FLOW`,
25
+ `GUIDE`, `GOVERN` e `ASSURE` e confirma o perfil efetivo; `OFF` nunca é escolhido pela LLM e a
26
+ escolha explícita do usuário prevalece.
27
+ - **O scanner de privacidade distingue configuração pública de identificadores privados.** Comandos
28
+ longos legítimos em `wendkeep.sensors.json` deixam de gerar falso positivo, sem permitir que UUID,
29
+ hash, token compacto, campo desconhecido ou path absoluto sejam ocultados pela exceção.
30
+
7
31
  ## [0.67.0] — 2026-08-01
8
32
 
9
33
  ### Added
package/README.en.md CHANGED
@@ -385,7 +385,7 @@ acknowledgement pending on 0.66.4 or later, first run the targeted dry run
385
385
  registry/checkpoint. See syntax, preconditions, and fail-closed behavior in
386
386
  [memory and curation](docs/en/commands/memory.md).
387
387
 
388
- Session notes use one live `## Agentes, tokens e custos` snapshot. Main-agent and subagent hooks recompose it atomically, with costs, token dimensions, reasoning tokens and effort per model/source. Every hook that rewrites a session note takes a per-file lock and writes through a temp file + rename, so the `SubagentStop` fan-out (one hook run per subagent) can never leave a note half-written; a note whose frontmatter reads back damaged is left untouched rather than patched.
388
+ Session notes use one live `## Agentes, tokens e custos` snapshot. Main-agent and subagent hooks recompose it atomically, with costs, token dimensions, reasoning tokens and effort per model/source. On Codex, subagent prompts register their rollout for observability without advancing the main agent's sequence; `SubagentStop` reads the child from `agent_transcript_path` and persists its signal only when `parent_thread_id` matches a validated session root. The main Stop uses the registry's causal `turn_id` mapping before falling back to local transcript order. To recover missing markers while a conversation is open, `hook session-backfill` is a dry-run by default and never writes a Codex turn without `task_complete`. Every hook that rewrites a session note takes a per-file lock and writes through a temp file + rename, so the `SubagentStop` fan-out (one hook run per subagent) can never leave a note half-written; a note whose frontmatter reads back damaged is left untouched rather than patched.
389
389
 
390
390
  ## Retroactive memory (`import`) — install today, remember yesterday
391
391
 
package/README.md CHANGED
@@ -385,7 +385,7 @@ acknowledgement pending on 0.66.4 or later, first run the targeted dry run
385
385
  registry/checkpoint. See syntax, preconditions, and fail-closed behavior in
386
386
  [memory and curation](docs/en/commands/memory.md).
387
387
 
388
- Session notes use one live `## Agentes, tokens e custos` snapshot. Main-agent and subagent hooks recompose it atomically, with costs, token dimensions, reasoning tokens and effort per model/source. Every hook that rewrites a session note takes a per-file lock and writes through a temp file + rename, so the `SubagentStop` fan-out (one hook run per subagent) can never leave a note half-written; a note whose frontmatter reads back damaged is left untouched rather than patched.
388
+ Session notes use one live `## Agentes, tokens e custos` snapshot. Main-agent and subagent hooks recompose it atomically, with costs, token dimensions, reasoning tokens and effort per model/source. On Codex, subagent prompts register their rollout for observability without advancing the main agent's sequence; `SubagentStop` reads the child from `agent_transcript_path` and persists its signal only when `parent_thread_id` matches a validated session root. The main Stop uses the registry's causal `turn_id` mapping before falling back to local transcript order. To recover missing markers while a conversation is open, `hook session-backfill` is a dry-run by default and never writes a Codex turn without `task_complete`. Every hook that rewrites a session note takes a per-file lock and writes through a temp file + rename, so the `SubagentStop` fan-out (one hook run per subagent) can never leave a note half-written; a note whose frontmatter reads back damaged is left untouched rather than patched.
389
389
 
390
390
  ## Retroactive memory (`import`) — install today, remember yesterday
391
391
 
@@ -30,6 +30,7 @@ npx wendkeep hook <name>
30
30
  npx wendkeep session list
31
31
  npx wendkeep session show <id>
32
32
  npx wendkeep session use <id>
33
+ npx wendkeep hook session-backfill --session <id> [--write]
33
34
  npx wendkeep import [options]
34
35
  ```
35
36
 
@@ -38,19 +39,27 @@ npx wendkeep import [options]
38
39
  - `wendkeep hook <name>` reads the agent payload from stdin; valid names are listed by `--help`.
39
40
  - `SessionStart` opens an activation: an epoch that remains active across multiple `Stop` events;
40
41
  only a new `SessionStart` supersedes the previous epoch.
41
- - `UserPromptSubmit` advances the active activation's native turn. If it finds a legacy registry
42
- with a closed epoch, it opens exactly one recovery activation; replaying the same prompt does
43
- not open another one.
44
- - On Codex, `session_id`, the native `turn_id`, and observed transcript order are enough to resolve
45
- the turn. Hook payloads do not need invented `activation_id` or `turn_sequence` fields.
42
+ - Main-agent `UserPromptSubmit` advances the active activation's native turn. A Codex rollout
43
+ prompt with `source.subagent` only registers its path for observability: it does not advance the
44
+ sequence, enter `turn_sequences`, or replace the main transcript. If a main prompt finds a
45
+ legacy registry with a closed epoch, it opens exactly one recovery activation; replaying the
46
+ same prompt does not open another one.
47
+ - On Codex, `session_id` and the native `turn_id` are enough to resolve the turn. Stop proves the
48
+ ID in the transcript and prefers `SESSION_REGISTRY.turn_sequences[turn_id]`; local transcript
49
+ order is the legacy-registry fallback. Interleaved subagent turns therefore cannot make the
50
+ parent's Stop artificially `stale_turn`. Hook payloads need no invented `activation_id` or
51
+ `turn_sequence` fields.
46
52
  - `Stop` accepts only a transcript-proven turn from the compatible active activation. Duplicates
47
53
  are no-ops; stale/superseded Stops neither publish memory nor overwrite a newer epoch's
48
54
  checkpoint.
49
55
  - `Stop` receives an absolute **45 s** deadline from hook entry. Reads check the clock between
50
56
  rollouts and on every chunk; reaching the limit returns `degraded` before the host timeout.
51
- - `SubagentStop` receives an absolute **15 s** deadline. Signals arriving within the **250 ms**
52
- window are coalesced: only the highest sequence recomposes/publishes, without losing the last
53
- child.
57
+ - `SubagentStop` receives an absolute **15 s** deadline and resolves the child rollout from Codex's
58
+ official `agent_transcript_path` field (`agentTranscriptPath` is also accepted);
59
+ `transcript_path` continues to identify the parent session. Before any write, `source.subagent`,
60
+ ID, canonical session, and `parent_thread_id` are validated; the parent must match a proven
61
+ session root. Signals arriving within the **250 ms** window are coalesced: only the highest
62
+ sequence recomposes/publishes, without losing the last child.
54
63
  - Observability is tri-state: `complete` publishes the full snapshot; `none` means zero proven by
55
64
  a causal Stop or stable offline scan; `degraded` preserves the previous snapshot and allowlisted
56
65
  diagnostics. An isolated `SubagentStop` never publishes `none`.
@@ -64,6 +73,10 @@ npx wendkeep import [options]
64
73
  - `import` reconciles observability even when no `wk-turn` is missing: a legacy schema, stale
65
74
  frontier, or unproven manifest triggers recomposition without duplicating iterations. A fresh
66
75
  checkpoint remains byte-identical; `degraded` is reported and does not change the note.
76
+ - `hook session-backfill` recovers missing `wk-turn` markers for the selected session. Without
77
+ `--write`, it only reports. On Codex, `missingTurns` contains only turns with `task_complete`;
78
+ open turns appear under `incompleteTurns` and are never written. `--write` applies only completed
79
+ candidates, and a second run is idempotent.
67
80
  - Exit `0` means consistent processing; non-zero reports invalid source/config/write instead of
68
81
  presenting silent partial success.
69
82
 
@@ -72,6 +85,8 @@ npx wendkeep import [options]
72
85
  ```bash
73
86
  npx wendkeep session list
74
87
  npx wendkeep session show 019abc-session-id
88
+ npx wendkeep hook session-backfill --session 019abc-session-id
89
+ npx wendkeep hook session-backfill --session 019abc-session-id --write
75
90
  npx wendkeep import --source codex --since 2026-07-01 --dry-run --json
76
91
  ```
77
92
 
@@ -98,6 +113,10 @@ without creating a new turn block.
98
113
  was found; the attempt remains observable but does not publish memory.
99
114
  - A late Stop reports `stale_turn`/`superseded`: the newer epoch and checkpoint are preserved; do
100
115
  not force the old payload to apply.
116
+ - `session-backfill` lists `incompleteTurns`: wait for that turn's `task_complete`/Stop and retry;
117
+ do not edit the note or force the partial turn. Once that turn receives `task_complete`, the next
118
+ run may treat it as eligible. Claude transcripts preserve their historical contract and do not
119
+ require this Codex event.
101
120
  - Fork duplicates: bound source/date and inspect `forked_from_id`/`source.subagent`.
102
121
  - Codex does not capture: approve hooks and start a new session after `sync`.
103
122
  - Contaminated cost: validate `session_id → session_file → transcript_path → provider`.
@@ -30,6 +30,7 @@ npx wendkeep hook <nome>
30
30
  npx wendkeep session list
31
31
  npx wendkeep session show <id>
32
32
  npx wendkeep session use <id>
33
+ npx wendkeep hook session-backfill --session <id> [--write]
33
34
  npx wendkeep import [opções]
34
35
  ```
35
36
 
@@ -38,18 +39,26 @@ npx wendkeep import [opções]
38
39
  - `wendkeep hook <name>` lê o payload do agente em stdin; nomes válidos aparecem em `--help`.
39
40
  - `SessionStart` abre uma activation, isto é, um epoch que continua ativo por vários `Stop`; só um
40
41
  novo `SessionStart` torna o epoch anterior superseded.
41
- - `UserPromptSubmit` avança o turno nativo da activation ativa. Se encontrar um registry legado
42
- com o epoch fechado, abre exatamente uma activation de recuperação; repetir o mesmo prompt não
43
- abre outra.
44
- - No Codex, `session_id`, o `turn_id` nativo e a ordem observada no transcript bastam para resolver
45
- o turno. O payload do hook não precisa inventar `activation_id` nem `turn_sequence`.
42
+ - `UserPromptSubmit` do agente principal avança o turno nativo da activation ativa. Um prompt de
43
+ rollout Codex com `source.subagent` apenas registra o path para observabilidade: não avança a
44
+ sequência, não entra em `turn_sequences` e não substitui o transcript principal. Se o prompt
45
+ principal encontra um registry legado com o epoch fechado, abre exatamente uma activation de
46
+ recuperação; repetir o mesmo prompt não abre outra.
47
+ - No Codex, `session_id` e o `turn_id` nativo bastam para resolver o turno. O Stop prova o ID no
48
+ transcript e prefere `SESSION_REGISTRY.turn_sequences[turn_id]`; a ordem local do transcript é
49
+ fallback para registry legado. Assim, turnos intercalados de subagents não tornam o Stop da mãe
50
+ artificialmente `stale_turn`. O payload não precisa inventar `activation_id` ou `turn_sequence`.
46
51
  - `Stop` aceita somente o turno comprovado pelo transcript e pela activation ativa compatível.
47
52
  Duplicatas são no-op; Stops stale/superseded não publicam memória nem sobrescrevem o checkpoint
48
53
  de um epoch mais novo.
49
54
  - `Stop` recebe deadline absoluto de **45 s** desde a entrada do hook. A leitura verifica o relógio
50
55
  entre rollouts e a cada chunk; ao atingir o limite, devolve `degraded` antes do timeout do host.
51
- - `SubagentStop` recebe deadline absoluto de **15 s**. Sinais que chegam na janela de **250 ms**
52
- são coalescidos: somente a maior sequência recompõe/publica, sem perder o último filho.
56
+ - `SubagentStop` recebe deadline absoluto de **15 s** e resolve o rollout filho pelo campo oficial
57
+ Codex `agent_transcript_path` (também aceita `agentTranscriptPath`); `transcript_path` continua
58
+ identificando a sessão-mãe. Antes de qualquer escrita, `source.subagent`, ID, sessão canônica e
59
+ `parent_thread_id` são validados; o pai deve corresponder a um root comprovado da sessão. Sinais
60
+ que chegam na janela de **250 ms** são coalescidos: somente a maior sequência
61
+ recompõe/publica, sem perder o último filho.
53
62
  - A observabilidade usa tri-state: `complete` publica o snapshot integral; `none` representa zero
54
63
  comprovado por Stop causal ou scan offline estável; `degraded` preserva o snapshot anterior e
55
64
  diagnostics allowlisted. `SubagentStop` isolado nunca publica `none`.
@@ -63,6 +72,10 @@ npx wendkeep import [opções]
63
72
  - `import` reconcilia a observabilidade mesmo quando nenhum `wk-turn` está ausente: schema legado,
64
73
  frontier stale ou manifest não comprovado disparam recomposição sem duplicar iterações. Um
65
74
  checkpoint fresco permanece byte-idêntico; `degraded` é reportado e não altera a nota.
75
+ - `hook session-backfill` recupera `wk-turn` ausente da sessão selecionada. Sem `--write`, apenas
76
+ relata. Em Codex, `missingTurns` contém somente turnos com `task_complete`; turnos ainda abertos
77
+ aparecem em `incompleteTurns` e nunca são gravados. `--write` aplica apenas os candidatos
78
+ concluídos e uma segunda execução é idempotente.
66
79
  - Exit `0` indica processamento consistente; exit não zero indica configuração, fonte ou escrita
67
80
  inválida sem transformar isso em sucesso parcial silencioso.
68
81
 
@@ -71,6 +84,8 @@ npx wendkeep import [opções]
71
84
  ```bash
72
85
  npx wendkeep session list
73
86
  npx wendkeep session show 019abc-session-id
87
+ npx wendkeep hook session-backfill --session 019abc-session-id
88
+ npx wendkeep hook session-backfill --session 019abc-session-id --write
74
89
  npx wendkeep import --source codex --since 2026-07-01 --dry-run --json
75
90
  ```
76
91
 
@@ -97,6 +112,10 @@ sem criar um novo bloco de turno.
97
112
  compatível foi encontrada; o attempt fica observável, mas não publica memória.
98
113
  - Stop atrasado aparece como `stale_turn`/`superseded`: o epoch mais novo e seu checkpoint são
99
114
  preservados; não force a reaplicação do payload antigo.
115
+ - `session-backfill` lista `incompleteTurns`: aguarde o `task_complete`/Stop desse turno e repita;
116
+ não edite a nota nem force o turno parcial. Quando o mesmo turno recebe `task_complete`, a
117
+ próxima execução passa a considerá-lo elegível. Transcripts Claude preservam o contrato
118
+ histórico e não exigem esse evento Codex.
100
119
  - Duplicatas de forks: limite por fonte/data e revise `forked_from_id`/`source.subagent`.
101
120
  - Codex não captura: aprove os hooks e reinicie a sessão após `sync`.
102
121
  - Custo contaminado: valide `session_id → session_file → transcript_path → provider`.
@@ -321,6 +321,13 @@ function nonNegativeSequence(value, fallback = null) {
321
321
  return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : fallback;
322
322
  }
323
323
 
324
+ export function resolveRegisteredTurnSequence(entry = {}, turnId = '', fallback = 0) {
325
+ const registered = turnId
326
+ ? nonNegativeSequence(entry?.turn_sequences?.[String(turnId)])
327
+ : null;
328
+ return registered === null ? nonNegativeSequence(fallback, 0) : registered;
329
+ }
330
+
324
331
  function cloneRegistry(registry = {}) {
325
332
  const sessions = Object.fromEntries(Object.entries(registry.sessions || {}).map(([id, entry]) => [
326
333
  id,
@@ -7,6 +7,7 @@ import {
7
7
  insertIteration,
8
8
  parseTranscript,
9
9
  } from './session-stop.mjs';
10
+ import { completedCodexTurnIdsContent } from '../packages/integrations/src/transcripts.mjs';
10
11
  import {
11
12
  getVaultBase,
12
13
  readControl,
@@ -63,6 +64,7 @@ export function backfillSessions({ vaultBase, write = false, limit = 0, session
63
64
  scanned: 0,
64
65
  candidates: 0,
65
66
  inserted: 0,
67
+ incomplete: 0,
66
68
  skipped: 0,
67
69
  missing: [],
68
70
  sessions: [],
@@ -85,13 +87,35 @@ export function backfillSessions({ vaultBase, write = false, limit = 0, session
85
87
  continue;
86
88
  }
87
89
 
90
+ const transcriptContent = readFileSync(entry.transcript_path, 'utf-8');
88
91
  const tx = parseTranscript(entry.transcript_path);
89
- const turns = tx.turns.filter((turn) => turn.turnId && turn.userPrompts.length);
92
+ const allTurns = tx.turns.filter((turn) => turn.turnId && turn.userPrompts.length);
93
+ const completedCodexTurns = tx.provider === 'codex'
94
+ ? completedCodexTurnIdsContent(transcriptContent)
95
+ : null;
96
+ const turns = completedCodexTurns
97
+ ? allTurns.filter((turn) => completedCodexTurns.has(turn.turnId))
98
+ : allTurns;
90
99
  const content = readFileSync(sessionPath, 'utf-8');
91
100
  const missingTurns = turns.filter((turn) => !hasTurnMarker(content, turn.turnId));
101
+ const incompleteTurns = completedCodexTurns
102
+ ? allTurns.filter((turn) => (
103
+ !completedCodexTurns.has(turn.turnId) && !hasTurnMarker(content, turn.turnId)
104
+ ))
105
+ : [];
106
+ report.incomplete += incompleteTurns.length;
92
107
 
93
108
  if (!missingTurns.length) {
94
109
  report.skipped += 1;
110
+ if (incompleteTurns.length) {
111
+ report.sessions.push({
112
+ session: entry.session_file,
113
+ transcript: entry.transcript_path,
114
+ missingTurns: [],
115
+ incompleteTurns: incompleteTurns.map((turn) => turn.turnId),
116
+ inserted: 0,
117
+ });
118
+ }
95
119
  continue;
96
120
  }
97
121
 
@@ -100,6 +124,7 @@ export function backfillSessions({ vaultBase, write = false, limit = 0, session
100
124
  session: entry.session_file,
101
125
  transcript: entry.transcript_path,
102
126
  missingTurns: missingTurns.map((turn) => turn.turnId),
127
+ incompleteTurns: incompleteTurns.map((turn) => turn.turnId),
103
128
  inserted: 0,
104
129
  };
105
130
 
@@ -32,6 +32,7 @@ import {
32
32
  yamlQuote,
33
33
  } from './obsidian-common.mjs';
34
34
  import { resolveSessionIdentity } from './session-identity.mjs';
35
+ import { readCodexRolloutMeta } from './codex-rollout-meta.mjs';
35
36
  import { mutateSessionNote } from './session-note-io.mjs';
36
37
 
37
38
  function sessionIdFromInput(input) {
@@ -54,6 +55,12 @@ function causalTurnPatch(input, now) {
54
55
  };
55
56
  }
56
57
 
58
+ function isCodexSubagentTranscript(identity) {
59
+ if (identity?.provider !== 'codex' || !identity.transcriptPath) return false;
60
+ const inspected = readCodexRolloutMeta(identity.transcriptPath);
61
+ return Boolean(inspected?.ok && inspected.meta?.source?.subagent);
62
+ }
63
+
57
64
  function buildSessionContent({ relPath, now, summary = 'session', sessionId = '', reason = 'Sessão criada automaticamente pelo hook UserPromptSubmit.' }) {
58
65
  const date = formatDate(now);
59
66
  const startedAt = formatLocalIso(now);
@@ -341,6 +348,21 @@ function main() {
341
348
  }
342
349
  const sessionId = identity.canonicalConversationId;
343
350
 
351
+ // UserPromptSubmit also fires inside Codex subagents. The child belongs to the parent's
352
+ // observability graph, but it is not a new main turn: registering it through causalTurnPatch
353
+ // permanently inflated the parent's sequence and made every later main Stop stale.
354
+ if (isCodexSubagentTranscript(identity)) {
355
+ const registered = readSessionRegistry(vaultBase).sessions?.[sessionId];
356
+ if (registered) {
357
+ upsertSessionRegistry(vaultBase, sessionId, {
358
+ transcript_paths: [identity.transcriptPath],
359
+ provider: identity.provider,
360
+ });
361
+ }
362
+ writeHookOutput({});
363
+ return;
364
+ }
365
+
344
366
  // Fast path: skip all writes if control file touched < 5 min ago and session matches
345
367
  try {
346
368
  const ctrlPath = controlPath(vaultBase);
@@ -61,6 +61,7 @@ import {
61
61
  hasTurnMarker,
62
62
  normalizeTurnMarkers,
63
63
  mutateSessionRegistry,
64
+ resolveRegisteredTurnSequence,
64
65
  resolveStopActivation,
65
66
  applyStopActivation,
66
67
  } from './obsidian-common.mjs';
@@ -1180,9 +1181,13 @@ export async function main({
1180
1181
  const turnId = turnIdentity.id;
1181
1182
  const now = finalizing ? new Date() : null;
1182
1183
  const endedAt = finalizing ? formatLocalIso(now) : '';
1183
- const stopTurnSequence = turnIdentity.order;
1184
1184
  const causalStop = finalizing
1185
1185
  ? mutateSessionRegistry(vaultBase, (registry) => {
1186
+ const stopTurnSequence = resolveRegisteredTurnSequence(
1187
+ registry.sessions?.[sessionId],
1188
+ turnId,
1189
+ turnIdentity.order,
1190
+ );
1186
1191
  const activationId = resolveStopActivation(registry, {
1187
1192
  session_id: sessionId,
1188
1193
  activation_id: input.activation_id || input.activationId || '',
@@ -1214,9 +1219,11 @@ export async function main({
1214
1219
  activation,
1215
1220
  stopDisposition: cas.stopDisposition,
1216
1221
  canPromoteMemory: cas.canPromoteMemory,
1222
+ turnSequence: stopTurnSequence,
1217
1223
  };
1218
1224
  })
1219
1225
  : null;
1226
+ const stopTurnSequence = causalStop?.turnSequence ?? turnIdentity.order;
1220
1227
  let memoryHandoff = null;
1221
1228
  let memoryAttempt = null;
1222
1229
  if (finalizing) {
@@ -82,6 +82,32 @@ function claudeRoots(entry) {
82
82
  return { state: 'complete', rootPaths: [...paths], descendantPaths: [], diagnostics: [] };
83
83
  }
84
84
 
85
+ export function subagentIdentityInput(input = {}) {
86
+ const agentTranscriptPath = input.agent_transcript_path || input.agentTranscriptPath || '';
87
+ if (!agentTranscriptPath) return input;
88
+ return {
89
+ ...input,
90
+ transcript_path: agentTranscriptPath,
91
+ transcriptPath: agentTranscriptPath,
92
+ };
93
+ }
94
+
95
+ function validatedCodexRootIds(entry, canonicalConversationId, { resolveRoots, readMeta }) {
96
+ const roots = resolveRoots(entry, { readMeta });
97
+ if (roots?.state !== 'complete' || !roots.rootPaths?.length) return null;
98
+
99
+ const ids = new Set();
100
+ for (const rootPath of roots.rootPaths) {
101
+ const result = readMeta(rootPath);
102
+ const meta = result?.meta;
103
+ const rootId = String(meta?.id || '');
104
+ if (!result?.ok || !rootId || meta?.source?.subagent) return null;
105
+ if (meta.session_id && meta.session_id !== canonicalConversationId) return null;
106
+ ids.add(rootId);
107
+ }
108
+ return ids;
109
+ }
110
+
85
111
  export async function refreshSubagents(vaultBase, input, {
86
112
  now = Date.now,
87
113
  hookStartedAt = now(),
@@ -100,7 +126,8 @@ export async function refreshSubagents(vaultBase, input, {
100
126
  } = {}) {
101
127
  const deadlineAt = hookStartedAt + deadlineMs;
102
128
  const provider = providerMeta(input.provider).id;
103
- const { identity, entry } = resolveEntry(vaultBase, input, provider);
129
+ const identityInput = subagentIdentityInput(input);
130
+ const { identity, entry } = resolveEntry(vaultBase, identityInput, provider);
104
131
  if (identity.state !== 'resolved') return false;
105
132
  const childTranscriptPath = identity.transcriptPath;
106
133
  const sessionRel = entry?.session_file || '';
@@ -118,6 +145,11 @@ export async function refreshSubagents(vaultBase, input, {
118
145
  || childMeta.meta.source?.subagent?.thread_spawn?.parent_thread_id
119
146
  || '',
120
147
  );
148
+ const rootIds = validatedCodexRootIds(entry, identity.canonicalConversationId, {
149
+ resolveRoots,
150
+ readMeta,
151
+ });
152
+ if (!childParentThreadId || !rootIds?.has(childParentThreadId)) return false;
121
153
  }
122
154
 
123
155
  const observed = causalSnapshot(entry);
@@ -168,7 +200,7 @@ export async function refreshSubagents(vaultBase, input, {
168
200
 
169
201
  try {
170
202
  if (now() >= deadlineAt) return true;
171
- const fresh = resolveEntry(vaultBase, input, provider);
203
+ const fresh = resolveEntry(vaultBase, identityInput, provider);
172
204
  if (fresh.identity?.state !== 'resolved'
173
205
  || fresh.identity.canonicalConversationId !== identity.canonicalConversationId
174
206
  || !fresh.entry?.session_file
@@ -197,7 +229,7 @@ export async function refreshSubagents(vaultBase, input, {
197
229
  const currentEntry = guardContext?.entry;
198
230
  const currentResolved = currentEntry
199
231
  ? { identity: { state: 'resolved', canonicalConversationId: identity.canonicalConversationId }, entry: currentEntry }
200
- : resolveEntry(vaultBase, input, provider);
232
+ : resolveEntry(vaultBase, identityInput, provider);
201
233
  if (currentResolved.identity?.state !== 'resolved'
202
234
  || currentResolved.identity.canonicalConversationId !== identity.canonicalConversationId
203
235
  || !currentResolved.entry) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.67.0",
3
+ "version": "0.67.1",
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.66.5"
73
+ "wendkeep": "^0.67.0"
74
74
  }
75
75
  }
@@ -117,7 +117,7 @@ Usage:
117
117
  wendkeep --help Show this help.
118
118
  `;
119
119
 
120
- function runHook(name) {
120
+ function runHook(name, args = []) {
121
121
  if (!name) {
122
122
  process.stderr.write('wendkeep hook: missing hook name\n');
123
123
  process.exit(2);
@@ -133,7 +133,7 @@ function runHook(name) {
133
133
  }
134
134
  // Spawn exactly as the agent would run `node <hook>.mjs`: stdio inherited so the
135
135
  // hook's stdin (agent JSON) and stdout (hookSpecificOutput) pass through untouched.
136
- const r = spawnSync(process.execPath, [file], { stdio: 'inherit' });
136
+ const r = spawnSync(process.execPath, [file, ...args], { stdio: 'inherit' });
137
137
  process.exit(r.status ?? 0);
138
138
  }
139
139
 
@@ -207,7 +207,7 @@ async function main(argv) {
207
207
  break;
208
208
  }
209
209
  case 'hook':
210
- runHook(rest[0]);
210
+ runHook(rest[0], rest.slice(1));
211
211
  break;
212
212
  case 'doctor': {
213
213
  const { runDoctor } = await import('../../../src/doctor.mjs');
@@ -170,6 +170,16 @@ function jsonLines(content) {
170
170
  }).filter(Boolean);
171
171
  }
172
172
 
173
+ export function completedCodexTurnIdsContent(content = '') {
174
+ const completed = new Set();
175
+ for (const event of jsonLines(content)) {
176
+ if (event.type !== 'event_msg' || event.payload?.type !== 'task_complete') continue;
177
+ const turnId = String(event.payload?.turn_id || event.turn_id || '').trim();
178
+ if (turnId) completed.add(turnId);
179
+ }
180
+ return completed;
181
+ }
182
+
173
183
  export function parseCodexTranscriptContent(content, options = {}) {
174
184
  const result = createResult('codex');
175
185
  const eventUserPrompts = [];
package/src/sync-defs.mjs CHANGED
@@ -64,10 +64,20 @@ This project uses the [wendkeep](https://github.com/rogersialves/wendkeep) harne
64
64
  in every profile: Vault, session, identity, memory, lessons, and persistence integrations.
65
65
  The persistent profile is selected explicitly; missing or invalid configuration uses **GOVERN as the default**.
66
66
 
67
- Before an implementation, the native LLM harness classifies the current request and may create a
68
- task-scoped lease with \`wendkeep profile route <FLOW|GUIDE|GOVERN|ASSURE> --session <id> --reason <text>\`.
69
- The lease expires when that request ends and the persistent session/project profile becomes
70
- effective again. **OFF is never selected adaptively**; only a human can persist \`profile use OFF\`.
67
+ Before every implementation, the native LLM harness **MUST run the routing gate**:
68
+ 1. Inspect \`wendkeep profile status\`.
69
+ 2. Classify the request and choose FLOW, GUIDE, GOVERN, or ASSURE from its scope and risk.
70
+ 3. Register the choice with
71
+ \`wendkeep profile route <FLOW|GUIDE|GOVERN|ASSURE> --session <id> --reason <text>\`.
72
+ 4. Re-check \`wendkeep profile status\` and follow the effective profile before editing.
73
+
74
+ The persistent profile is only the base restored after the task lease; it is not an immutable
75
+ decision for the current task. A base OFF does not block a temporary FLOW/GUIDE/GOVERN/ASSURE
76
+ lease, while a base GOVERN does not prevent choosing FLOW or GUIDE for bounded work. An explicit
77
+ user choice for the request takes precedence. If no causal session is available or routing fails,
78
+ do not invent lease state: follow the effective profile and use GOVERN only as the conservative
79
+ fallback when configuration is missing or invalid. The lease expires when the request ends.
80
+ **OFF is never selected adaptively**; only a human can persist \`profile use OFF\`.
71
81
 
72
82
  Route work by the effective profile:
73
83
  - **OFF** — Wend Runtime is disabled and governance belongs to the native LLM harness; Keep Core stays active.