wendkeep 0.66.4 → 0.67.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 (40) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.en.md +78 -5
  3. package/README.md +78 -5
  4. package/docs/en/commands/costs-and-observability.md +21 -7
  5. package/docs/en/commands/maintenance-and-diagnostics.md +13 -1
  6. package/docs/en/commands/operating-profiles.md +65 -10
  7. package/docs/en/commands/sessions-and-import.md +22 -1
  8. package/docs/en/commands/verify.md +5 -3
  9. package/docs/pt-BR/commands/costs-and-observability.md +21 -7
  10. package/docs/pt-BR/commands/maintenance-and-diagnostics.md +12 -1
  11. package/docs/pt-BR/commands/operating-profiles.md +66 -11
  12. package/docs/pt-BR/commands/sessions-and-import.md +20 -0
  13. package/docs/pt-BR/commands/verify.md +6 -3
  14. package/hooks/change-nag.mjs +8 -0
  15. package/hooks/codex-rollout-meta.mjs +112 -0
  16. package/hooks/codex-subagent-graph.mjs +903 -0
  17. package/hooks/harness-doctor.mjs +82 -1
  18. package/hooks/import-sessions.mjs +185 -50
  19. package/hooks/operating-profile-runtime.mjs +36 -2
  20. package/hooks/operating-profile-task-store.mjs +77 -0
  21. package/hooks/session-identity.mjs +40 -5
  22. package/hooks/session-observability-lifecycle.mjs +129 -0
  23. package/hooks/session-observability-state.mjs +241 -0
  24. package/hooks/session-observability-store.mjs +436 -0
  25. package/hooks/session-observability.mjs +647 -21
  26. package/hooks/session-stop.mjs +339 -11
  27. package/hooks/subagent-stop.mjs +266 -12
  28. package/hooks/subagent-usage.mjs +65 -0
  29. package/hooks/token-usage.mjs +81 -4
  30. package/package.json +3 -3
  31. package/packages/harness/src/operating-profile.mjs +127 -0
  32. package/packages/harness/src/sensors-core.mjs +41 -1
  33. package/packages/integrations/src/prompt-content.mjs +123 -0
  34. package/packages/integrations/src/transcripts.mjs +16 -10
  35. package/src/cost.mjs +40 -6
  36. package/src/doctor.mjs +4 -1
  37. package/src/profile.mjs +95 -17
  38. package/src/rebuild-costs.mjs +220 -34
  39. package/src/skills-seed.mjs +38 -2
  40. package/src/sync-defs.mjs +6 -1
@@ -8,8 +8,13 @@ import { addUsage, costBreakdown, emptyTokenUsage, normalizeClaudeUsage, normali
8
8
  import { buildBrainDigest, buildBrainIndex } from './brain-core.mjs';
9
9
  import { activeChangeLink, pruneChangeSentinels } from './change-core.mjs';
10
10
  import { getLocale } from './locale.mjs';
11
- import { updateSessionObservability } from './session-observability.mjs';
11
+ import { materializeSessionObservability } from './session-observability.mjs';
12
12
  import { resolveSessionEntry } from './session-identity.mjs';
13
+ import { resolveObservabilityRoots } from './session-observability-lifecycle.mjs';
14
+ import {
15
+ markObservabilityCheckpoint,
16
+ readObservabilityStore,
17
+ } from './session-observability-store.mjs';
13
18
  import { mutateSessionNote } from './session-note-io.mjs';
14
19
  import { applyDerivedSections, provenanceSessions } from './derived-sections.mjs';
15
20
  import { buildSessionMemoryEvents, collectLifecycleEvidence } from './memory-handoff.mjs';
@@ -28,6 +33,7 @@ import {
28
33
  parseTranscriptContent,
29
34
  resolveTurnIdentity,
30
35
  } from '../packages/integrations/src/transcripts.mjs';
36
+ import { sanitizeAssistantMessage } from '../packages/integrations/src/prompt-content.mjs';
31
37
  export { resolveTurnIdentity };
32
38
  import {
33
39
  ensureDir,
@@ -208,6 +214,13 @@ function escapeMarkdownBackticks(text) {
208
214
  return escaped;
209
215
  }
210
216
 
217
+ function escapeMarkdownHtmlTags(text) {
218
+ return String(text || '').replace(
219
+ /<(\/?[\p{L}][\p{L}\p{N}_.-]*)(?=[\s/>])([^<>\n]*)>/gu,
220
+ '&lt;$1$2&gt;',
221
+ );
222
+ }
223
+
211
224
  function compactText(text, max = 600) {
212
225
  const clean = redactSecrets(String(text || ''))
213
226
  .replace(/\r/g, '\n')
@@ -218,7 +231,8 @@ function compactText(text, max = 600) {
218
231
  const clipped = truncate(source, max);
219
232
  // Um corte no meio de código inline/fence pode casar com backticks da próxima
220
233
  // entrada gerada. Só snippets realmente truncados perdem a formatação incompleta.
221
- return compact.length > max ? escapeMarkdownBackticks(clipped) : clipped;
234
+ const markdownSafe = compact.length > max ? escapeMarkdownBackticks(clipped) : clipped;
235
+ return escapeMarkdownHtmlTags(markdownSafe);
222
236
  }
223
237
 
224
238
  function selectTurn(tx, turnId) {
@@ -230,6 +244,11 @@ function selectTurn(tx, turnId) {
230
244
 
231
245
  function formatConversation(turn) {
232
246
  const entries = (turn.conversation || [])
247
+ .map((entry) => (
248
+ entry.role === 'Assistente'
249
+ ? { ...entry, text: sanitizeAssistantMessage(entry.text) }
250
+ : entry
251
+ ))
233
252
  .filter((entry) => entry.text && !shouldIgnoreUserText(entry.text));
234
253
  if (!entries.length) return '- Nenhuma mensagem útil capturada no transcript.';
235
254
 
@@ -299,7 +318,9 @@ export function buildIterationBlock(tx, input) {
299
318
  const now = Number.isFinite(parsedDate.getTime()) ? parsedDate : new Date();
300
319
  const promptText = turn.userPrompts.at(-1) || tx.latestUserPrompt || '';
301
320
  const latestAssistant = turn.assistantMessages.at(-1) || tx.latestAssistantMessage || '';
302
- const heading = truncate(promptText.replace(/[\r\n#]+/g, ' ').replace(/\s+/g, ' ').trim() || 'Iteração', 80);
321
+ const heading = escapeMarkdownHtmlTags(
322
+ truncate(promptText.replace(/[\r\n#]+/g, ' ').replace(/\s+/g, ' ').trim() || 'Iteração', 80),
323
+ );
303
324
  const files = [...new Set([...(turn.consultedFiles || []), ...(turn.changedFiles || [])])];
304
325
  const model = turn.model || tx.model || '';
305
326
 
@@ -320,7 +341,7 @@ ${formatConversation(turn)}
320
341
 
321
342
  **Arquivos detectados no turno:** ${formatInlineList(files, 'Nenhum arquivo detectado automaticamente.')}
322
343
 
323
- **Estado ao final do turno:** ${compactText(latestAssistant || 'Checkpoint registrado automaticamente ao final do turno.', 900)}
344
+ **Estado ao final do turno:** ${compactText(sanitizeAssistantMessage(latestAssistant) || 'Checkpoint registrado automaticamente ao final do turno.', 900)}
324
345
  `;
325
346
  }
326
347
 
@@ -644,6 +665,104 @@ function replaceClosingSection(content, closing) {
644
665
  return `${content.slice(0, index).trimEnd()}\n\n${closing}\n`;
645
666
  }
646
667
 
668
+ const GENERATED_ITERATION_LINE_RULES = [
669
+ { pattern: /^(### \d{2}:\d{2} - )(.*)$/u, assistant: false },
670
+ { pattern: /^(\*\*Pedido:\*\* )(.*)$/u, assistant: false },
671
+ { pattern: /^(- \*\*Usuário:\*\* )(.*)$/u, assistant: false },
672
+ { pattern: /^(- \*\*Assistente:\*\* )(.*)$/u, assistant: true },
673
+ { pattern: /^(- \*\*Resumo:\*\* )(.*)$/u, assistant: true },
674
+ { pattern: /^(\*\*Estado ao final do turno:\*\* )(.*)$/u, assistant: true },
675
+ ];
676
+ const GENERATED_CLOSING_LINE_RULES = [
677
+ { pattern: /^(- \*\*Resumo final:\*\* )(.*)$/u, assistant: true },
678
+ ];
679
+
680
+ function generatedSessionLine(line, rules) {
681
+ for (const rule of rules) {
682
+ const match = rule.pattern.exec(line);
683
+ if (match) return { ...rule, prefix: match[1], value: match[2] };
684
+ }
685
+ return null;
686
+ }
687
+
688
+ function splitSessionMarkdownLines(source) {
689
+ const lines = [];
690
+ let cursor = 0;
691
+ while (cursor < source.length) {
692
+ const newline = source.indexOf('\n', cursor);
693
+ if (newline === -1) {
694
+ lines.push({ text: source.slice(cursor), eol: '' });
695
+ break;
696
+ }
697
+ const textEnd = source[newline - 1] === '\r' ? newline - 1 : newline;
698
+ lines.push({ text: source.slice(cursor, textEnd), eol: source.slice(textEnd, newline + 1) });
699
+ cursor = newline + 1;
700
+ }
701
+ return lines;
702
+ }
703
+
704
+ function generatedMetadataContinuation(line, mode = '') {
705
+ const clean = line.trim();
706
+ if (!clean) return null;
707
+ if (/^<\/?session\s*>/i.test(clean)) return mode;
708
+ if (/^<\/?(?:oai-mem-citation|citation_entries|rollout_ids)\b/i.test(clean)) {
709
+ const nested = [...clean.matchAll(/<(citation_entries|rollout_ids)\b[^>]*>/gi)].at(-1);
710
+ return nested ? nested[1].toLowerCase() : mode;
711
+ }
712
+ if (mode === 'citation_entries' && (
713
+ /\|note=\[[^\]]*\]\s*$/i.test(clean)
714
+ || /^[^\s<>]+:\d+(?:-\d+)?(?:\|[^\s].*)?$/i.test(clean)
715
+ )) return mode;
716
+ if (mode === 'rollout_ids' && /^(?:[0-9a-f]{8,}(?:-[0-9a-f-]+)*|019f-[A-Za-z0-9_-]+)$/i.test(clean)) {
717
+ return mode;
718
+ }
719
+ return null;
720
+ }
721
+
722
+ export function sanitizeGeneratedSessionMarkdown(content) {
723
+ const lines = splitSessionMarkdownLines(String(content || ''));
724
+ let section = '';
725
+ let output = '';
726
+
727
+ for (let index = 0; index < lines.length; index += 1) {
728
+ const line = lines[index];
729
+ if (/^## /u.test(line.text)) {
730
+ section = line.text === '## Iterações'
731
+ ? 'iterations'
732
+ : (line.text === '## Encerramento' ? 'closing' : '');
733
+ output += `${line.text}${line.eol}`;
734
+ continue;
735
+ }
736
+
737
+ const rules = section === 'iterations'
738
+ ? GENERATED_ITERATION_LINE_RULES
739
+ : (section === 'closing' ? GENERATED_CLOSING_LINE_RULES : []);
740
+ const generated = generatedSessionLine(line.text, rules);
741
+ if (!generated) {
742
+ output += `${line.text}${line.eol}`;
743
+ continue;
744
+ }
745
+
746
+ let value = generated.value;
747
+ let last = index;
748
+ let mode = '';
749
+ if (generated.assistant) {
750
+ for (let next = index + 1; next < lines.length; next += 1) {
751
+ const nextMode = generatedMetadataContinuation(lines[next].text, mode);
752
+ if (nextMode === null) break;
753
+ value += `${lines[last].eol}${lines[next].text}`;
754
+ last = next;
755
+ mode = nextMode;
756
+ }
757
+ value = sanitizeAssistantMessage(value);
758
+ }
759
+
760
+ output += `${generated.prefix}${escapeMarkdownHtmlTags(value)}${lines[last].eol}`;
761
+ index = last;
762
+ }
763
+ return output;
764
+ }
765
+
647
766
  export function finalizeSessionFile(sessionPath, tx, created, endedAt, vaultBase = '') {
648
767
  const pending = extractPending(tx.rawTextForDetection);
649
768
  const links = (items) => items.length ? items.map((rel) => ` - ${wikilinkFromRel(rel)}`).join('\n') : ' - Nenhuma';
@@ -668,7 +787,7 @@ ${formatPendingClosing(pending)}
668
787
  // As três seções derivadas saem do MESMO `created` que monta o Encerramento — antes
669
788
  // elas ficavam de fora deste write e a nota mentia no corpo (ver hooks/derived-sections.mjs).
670
789
  applyDerivedSections(
671
- replacePendingSection(updateFrontmatter(content, endedAt), pending),
790
+ replacePendingSection(updateFrontmatter(sanitizeGeneratedSessionMarkdown(content), endedAt), pending),
672
791
  created,
673
792
  ),
674
793
  closing,
@@ -676,8 +795,9 @@ ${formatPendingClosing(pending)}
676
795
  }
677
796
 
678
797
  export function sessionFinalSummary(tx) {
679
- return tx.latestAssistantMessage
680
- ? truncate(tx.latestAssistantMessage, 500)
798
+ const assistantSummary = sanitizeAssistantMessage(tx.latestAssistantMessage);
799
+ return assistantSummary
800
+ ? compactText(assistantSummary, 500)
681
801
  : `Sessão encerrada com ${tx.userPrompts.length} prompts e ${tx.tools.length} ferramentas registradas.`;
682
802
  }
683
803
 
@@ -794,7 +914,206 @@ export function shouldAbortStopAfterStaging(causalStop, memoryAttempt) {
794
914
  );
795
915
  }
796
916
 
797
- export function main({ stageMemory = stageStopMemoryAttempt } = {}) {
917
+ const STOP_OBSERVABILITY_DEADLINE_MS = 45_000;
918
+
919
+ function stopEntryCausalSnapshot(entry) {
920
+ const activationId = String(entry?.active_activation_id || '');
921
+ const activation = entry?.activations?.[activationId] || {};
922
+ return {
923
+ activationId,
924
+ activationEpoch: Number(activation.epoch || entry?.activation_epoch || 0),
925
+ turnSequence: Number(entry?.last_turn_sequence || activation.last_turn_sequence || 0),
926
+ };
927
+ }
928
+
929
+ function expectedStopCausalSnapshot(entry, causalStop, turnSequence) {
930
+ if (!causalStop) return stopEntryCausalSnapshot(entry);
931
+ return {
932
+ activationId: String(causalStop.activationId || ''),
933
+ activationEpoch: Number(causalStop.activation?.epoch || entry?.activation_epoch || 0),
934
+ turnSequence: Number(turnSequence || 0),
935
+ };
936
+ }
937
+
938
+ function sameStopCausalSnapshot(left, right) {
939
+ return left.activationId === right.activationId
940
+ && left.activationEpoch === right.activationEpoch
941
+ && left.turnSequence === right.turnSequence;
942
+ }
943
+
944
+ function stopClaudeRoots(entry) {
945
+ const paths = new Set();
946
+ const add = (value) => {
947
+ if (typeof value === 'string' && value.trim()) paths.add(value.trim());
948
+ };
949
+ add(entry?.transcript_path);
950
+ for (const path of entry?.transcript_paths || []) add(path);
951
+ for (const activation of Object.values(entry?.activations || {})) {
952
+ add(activation?.transcript_path);
953
+ for (const path of activation?.transcript_paths || []) add(path);
954
+ }
955
+ return { state: 'complete', rootPaths: [...paths], descendantPaths: [], diagnostics: [] };
956
+ }
957
+
958
+ function materializeStopObservability(request) {
959
+ return materializeSessionObservability({
960
+ vaultBase: request.vaultBase,
961
+ sessionPath: request.sessionPath,
962
+ transcriptPath: request.transcriptPath,
963
+ entry: request.entry,
964
+ canonicalConversationId: request.canonicalConversationId,
965
+ frontier: request.frontier,
966
+ signals: request.signals,
967
+ cache: request.cache,
968
+ mode: 'live',
969
+ deadlineAt: request.deadlineAt,
970
+ now: request.now,
971
+ allowNone: request.allowNone,
972
+ readRuntimeFrontier: request.readRuntimeFrontier,
973
+ withPublicationGuard: request.withPublicationGuard,
974
+ writeRegistryCheckpoint: request.writeRegistryCheckpoint,
975
+ });
976
+ }
977
+
978
+ export async function refreshStopObservability({
979
+ vaultBase,
980
+ input,
981
+ sessionPath,
982
+ sessionId,
983
+ entry,
984
+ causalStop,
985
+ turnSequence,
986
+ hookStartedAt,
987
+ expectedSignalSequence,
988
+ }, {
989
+ now = Date.now,
990
+ resolveEntry = resolveSessionEntry,
991
+ mutateRegistry = mutateSessionRegistry,
992
+ readStore = readObservabilityStore,
993
+ resolveRoots = resolveObservabilityRoots,
994
+ materialize = materializeStopObservability,
995
+ } = {}) {
996
+ if (causalStop && !causalStop.canPromoteMemory) return false;
997
+ const deadlineAt = hookStartedAt + STOP_OBSERVABILITY_DEADLINE_MS;
998
+ if (now() >= deadlineAt) return false;
999
+
1000
+ const expected = expectedStopCausalSnapshot(entry, causalStop, turnSequence);
1001
+ const fresh = resolveEntry(vaultBase, input, entry?.provider);
1002
+ if (fresh.identity?.state !== 'resolved'
1003
+ || fresh.identity.canonicalConversationId !== sessionId
1004
+ || !fresh.entry?.session_file
1005
+ || fresh.entry.session_file !== entry?.session_file
1006
+ || !sameStopCausalSnapshot(expected, stopEntryCausalSnapshot(fresh.entry))) return false;
1007
+
1008
+ const runtime = readStore(vaultBase, sessionId);
1009
+ const signalSequence = Number(runtime?.observability_signal_sequence || 0);
1010
+ if (expectedSignalSequence !== undefined
1011
+ && signalSequence !== Number(expectedSignalSequence)) return false;
1012
+ if (now() >= deadlineAt) return false;
1013
+
1014
+ const roots = fresh.identity.provider === 'codex'
1015
+ ? resolveRoots(fresh.entry)
1016
+ : stopClaudeRoots(fresh.entry);
1017
+ if (roots?.state !== 'complete' || !roots.rootPaths?.length) return false;
1018
+
1019
+ const frontier = {
1020
+ canonical_session_id: sessionId,
1021
+ activation_id: expected.activationId || 'legacy',
1022
+ activation_epoch: expected.activationEpoch,
1023
+ turn_sequence: expected.turnSequence,
1024
+ signal_sequence: signalSequence,
1025
+ roots_stat_hash: 'pending',
1026
+ graph_cursor: 'pending',
1027
+ source_manifest_hash: 'pending',
1028
+ };
1029
+ const readRuntimeFrontier = (candidateFrontier, guardContext) => {
1030
+ const currentEntry = guardContext?.entry;
1031
+ const current = currentEntry
1032
+ ? { identity: { state: 'resolved', canonicalConversationId: sessionId }, entry: currentEntry }
1033
+ : resolveEntry(vaultBase, input, entry?.provider);
1034
+ if (current.identity?.state !== 'resolved'
1035
+ || current.identity.canonicalConversationId !== sessionId
1036
+ || !current.entry) {
1037
+ return { ...candidateFrontier, canonical_session_id: 'unresolved' };
1038
+ }
1039
+ const currentCausal = stopEntryCausalSnapshot(current.entry);
1040
+ const currentRuntime = readStore(vaultBase, sessionId);
1041
+ return {
1042
+ ...candidateFrontier,
1043
+ activation_id: currentCausal.activationId || 'legacy',
1044
+ activation_epoch: currentCausal.activationEpoch,
1045
+ turn_sequence: currentCausal.turnSequence,
1046
+ signal_sequence: Math.max(
1047
+ Number(currentRuntime?.observability_signal_sequence || 0),
1048
+ Number(current.entry.observability_signal_sequence || 0),
1049
+ ),
1050
+ };
1051
+ };
1052
+ const withPublicationGuard = (_candidateFrontier, publishGuarded) => (
1053
+ mutateRegistry(vaultBase, (registry) => publishGuarded({
1054
+ registry,
1055
+ entry: registry.sessions?.[sessionId] || null,
1056
+ }))
1057
+ );
1058
+ const writeRegistryCheckpoint = ({
1059
+ frontier: checkpointFrontier,
1060
+ state,
1061
+ diagnostics,
1062
+ snapshot,
1063
+ }, guardContext) => {
1064
+ const registry = guardContext?.registry;
1065
+ const current = registry?.sessions?.[sessionId];
1066
+ if (!current) return null;
1067
+ const currentSignal = Number(current.observability_signal_sequence || checkpointFrontier.signal_sequence);
1068
+ registry.sessions[sessionId] = {
1069
+ ...current,
1070
+ observability_signal_sequence: currentSignal,
1071
+ observability_checkpoint_sequence: checkpointFrontier.signal_sequence,
1072
+ observability_dirty: currentSignal > checkpointFrontier.signal_sequence,
1073
+ observability_checkpoint_frontier: checkpointFrontier,
1074
+ subagents_observability_state: state,
1075
+ subagents_diagnostics: diagnostics || [],
1076
+ };
1077
+ return markObservabilityCheckpoint(vaultBase, sessionId, {
1078
+ checkpointSequence: checkpointFrontier.signal_sequence,
1079
+ frontier: checkpointFrontier,
1080
+ sourceManifest: snapshot?.subagents?.sourceManifest,
1081
+ graphCache: snapshot?.subagents?.cache,
1082
+ diagnostics,
1083
+ });
1084
+ };
1085
+
1086
+ const result = await Promise.resolve(materialize({
1087
+ vaultBase,
1088
+ sessionPath,
1089
+ entry: fresh.entry,
1090
+ rootPaths: roots.rootPaths,
1091
+ transcriptPath: roots.rootPaths[0],
1092
+ caller: 'stop',
1093
+ canonicalConversationId: sessionId,
1094
+ activationId: expected.activationId,
1095
+ activationEpoch: expected.activationEpoch,
1096
+ turnSequence: expected.turnSequence,
1097
+ signalSequence,
1098
+ deadlineAt,
1099
+ allowNone: true,
1100
+ frontier,
1101
+ signals: runtime?.signals || [],
1102
+ cache: runtime?.graph_cache || null,
1103
+ now,
1104
+ readRuntimeFrontier,
1105
+ withPublicationGuard,
1106
+ writeRegistryCheckpoint,
1107
+ }));
1108
+ return !['stale', 'conflict', 'degraded', 'missing'].includes(result?.status);
1109
+ }
1110
+
1111
+ export async function main({
1112
+ stageMemory = stageStopMemoryAttempt,
1113
+ clock = Date.now,
1114
+ refreshObservability = refreshStopObservability,
1115
+ } = {}) {
1116
+ const hookStartedAt = clock();
798
1117
  const input = readHookInput();
799
1118
  if (input.stop_hook_active) {
800
1119
  writeHookOutput({});
@@ -961,8 +1280,17 @@ export function main({ stageMemory = stageStopMemoryAttempt } = {}) {
961
1280
  }
962
1281
 
963
1282
  try {
964
- updateSessionObservability({
965
- vaultBase, sessionPath, transcriptPath, caller: 'stop', canonicalConversationId: sessionId,
1283
+ await refreshObservability({
1284
+ vaultBase,
1285
+ input,
1286
+ sessionPath,
1287
+ sessionId,
1288
+ entry,
1289
+ causalStop,
1290
+ turnSequence: stopTurnSequence,
1291
+ hookStartedAt,
1292
+ }, {
1293
+ now: clock,
966
1294
  });
967
1295
  } catch (error) {
968
1296
  process.stderr.write(`[wendkeep] Token usage falhou: ${error.message}\n`);
@@ -1062,7 +1390,7 @@ export function main({ stageMemory = stageStopMemoryAttempt } = {}) {
1062
1390
 
1063
1391
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
1064
1392
  try {
1065
- main();
1393
+ await main();
1066
1394
  } catch (error) {
1067
1395
  process.stderr.write(`[wendkeep] Stop falhou: ${error.message}\n`);
1068
1396
  // Same reasoning as the identity bail: stderr is discarded by Codex. Exit stays 0 —