wendkeep 0.66.5 → 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.
@@ -33,6 +33,7 @@ import {
33
33
  parseTranscriptContent,
34
34
  resolveTurnIdentity,
35
35
  } from '../packages/integrations/src/transcripts.mjs';
36
+ import { sanitizeAssistantMessage } from '../packages/integrations/src/prompt-content.mjs';
36
37
  export { resolveTurnIdentity };
37
38
  import {
38
39
  ensureDir,
@@ -60,6 +61,7 @@ import {
60
61
  hasTurnMarker,
61
62
  normalizeTurnMarkers,
62
63
  mutateSessionRegistry,
64
+ resolveRegisteredTurnSequence,
63
65
  resolveStopActivation,
64
66
  applyStopActivation,
65
67
  } from './obsidian-common.mjs';
@@ -213,6 +215,13 @@ function escapeMarkdownBackticks(text) {
213
215
  return escaped;
214
216
  }
215
217
 
218
+ function escapeMarkdownHtmlTags(text) {
219
+ return String(text || '').replace(
220
+ /<(\/?[\p{L}][\p{L}\p{N}_.-]*)(?=[\s/>])([^<>\n]*)>/gu,
221
+ '&lt;$1$2&gt;',
222
+ );
223
+ }
224
+
216
225
  function compactText(text, max = 600) {
217
226
  const clean = redactSecrets(String(text || ''))
218
227
  .replace(/\r/g, '\n')
@@ -223,7 +232,8 @@ function compactText(text, max = 600) {
223
232
  const clipped = truncate(source, max);
224
233
  // Um corte no meio de código inline/fence pode casar com backticks da próxima
225
234
  // entrada gerada. Só snippets realmente truncados perdem a formatação incompleta.
226
- return compact.length > max ? escapeMarkdownBackticks(clipped) : clipped;
235
+ const markdownSafe = compact.length > max ? escapeMarkdownBackticks(clipped) : clipped;
236
+ return escapeMarkdownHtmlTags(markdownSafe);
227
237
  }
228
238
 
229
239
  function selectTurn(tx, turnId) {
@@ -235,6 +245,11 @@ function selectTurn(tx, turnId) {
235
245
 
236
246
  function formatConversation(turn) {
237
247
  const entries = (turn.conversation || [])
248
+ .map((entry) => (
249
+ entry.role === 'Assistente'
250
+ ? { ...entry, text: sanitizeAssistantMessage(entry.text) }
251
+ : entry
252
+ ))
238
253
  .filter((entry) => entry.text && !shouldIgnoreUserText(entry.text));
239
254
  if (!entries.length) return '- Nenhuma mensagem útil capturada no transcript.';
240
255
 
@@ -304,7 +319,9 @@ export function buildIterationBlock(tx, input) {
304
319
  const now = Number.isFinite(parsedDate.getTime()) ? parsedDate : new Date();
305
320
  const promptText = turn.userPrompts.at(-1) || tx.latestUserPrompt || '';
306
321
  const latestAssistant = turn.assistantMessages.at(-1) || tx.latestAssistantMessage || '';
307
- const heading = truncate(promptText.replace(/[\r\n#]+/g, ' ').replace(/\s+/g, ' ').trim() || 'Iteração', 80);
322
+ const heading = escapeMarkdownHtmlTags(
323
+ truncate(promptText.replace(/[\r\n#]+/g, ' ').replace(/\s+/g, ' ').trim() || 'Iteração', 80),
324
+ );
308
325
  const files = [...new Set([...(turn.consultedFiles || []), ...(turn.changedFiles || [])])];
309
326
  const model = turn.model || tx.model || '';
310
327
 
@@ -325,7 +342,7 @@ ${formatConversation(turn)}
325
342
 
326
343
  **Arquivos detectados no turno:** ${formatInlineList(files, 'Nenhum arquivo detectado automaticamente.')}
327
344
 
328
- **Estado ao final do turno:** ${compactText(latestAssistant || 'Checkpoint registrado automaticamente ao final do turno.', 900)}
345
+ **Estado ao final do turno:** ${compactText(sanitizeAssistantMessage(latestAssistant) || 'Checkpoint registrado automaticamente ao final do turno.', 900)}
329
346
  `;
330
347
  }
331
348
 
@@ -649,6 +666,104 @@ function replaceClosingSection(content, closing) {
649
666
  return `${content.slice(0, index).trimEnd()}\n\n${closing}\n`;
650
667
  }
651
668
 
669
+ const GENERATED_ITERATION_LINE_RULES = [
670
+ { pattern: /^(### \d{2}:\d{2} - )(.*)$/u, assistant: false },
671
+ { pattern: /^(\*\*Pedido:\*\* )(.*)$/u, assistant: false },
672
+ { pattern: /^(- \*\*Usuário:\*\* )(.*)$/u, assistant: false },
673
+ { pattern: /^(- \*\*Assistente:\*\* )(.*)$/u, assistant: true },
674
+ { pattern: /^(- \*\*Resumo:\*\* )(.*)$/u, assistant: true },
675
+ { pattern: /^(\*\*Estado ao final do turno:\*\* )(.*)$/u, assistant: true },
676
+ ];
677
+ const GENERATED_CLOSING_LINE_RULES = [
678
+ { pattern: /^(- \*\*Resumo final:\*\* )(.*)$/u, assistant: true },
679
+ ];
680
+
681
+ function generatedSessionLine(line, rules) {
682
+ for (const rule of rules) {
683
+ const match = rule.pattern.exec(line);
684
+ if (match) return { ...rule, prefix: match[1], value: match[2] };
685
+ }
686
+ return null;
687
+ }
688
+
689
+ function splitSessionMarkdownLines(source) {
690
+ const lines = [];
691
+ let cursor = 0;
692
+ while (cursor < source.length) {
693
+ const newline = source.indexOf('\n', cursor);
694
+ if (newline === -1) {
695
+ lines.push({ text: source.slice(cursor), eol: '' });
696
+ break;
697
+ }
698
+ const textEnd = source[newline - 1] === '\r' ? newline - 1 : newline;
699
+ lines.push({ text: source.slice(cursor, textEnd), eol: source.slice(textEnd, newline + 1) });
700
+ cursor = newline + 1;
701
+ }
702
+ return lines;
703
+ }
704
+
705
+ function generatedMetadataContinuation(line, mode = '') {
706
+ const clean = line.trim();
707
+ if (!clean) return null;
708
+ if (/^<\/?session\s*>/i.test(clean)) return mode;
709
+ if (/^<\/?(?:oai-mem-citation|citation_entries|rollout_ids)\b/i.test(clean)) {
710
+ const nested = [...clean.matchAll(/<(citation_entries|rollout_ids)\b[^>]*>/gi)].at(-1);
711
+ return nested ? nested[1].toLowerCase() : mode;
712
+ }
713
+ if (mode === 'citation_entries' && (
714
+ /\|note=\[[^\]]*\]\s*$/i.test(clean)
715
+ || /^[^\s<>]+:\d+(?:-\d+)?(?:\|[^\s].*)?$/i.test(clean)
716
+ )) return mode;
717
+ if (mode === 'rollout_ids' && /^(?:[0-9a-f]{8,}(?:-[0-9a-f-]+)*|019f-[A-Za-z0-9_-]+)$/i.test(clean)) {
718
+ return mode;
719
+ }
720
+ return null;
721
+ }
722
+
723
+ export function sanitizeGeneratedSessionMarkdown(content) {
724
+ const lines = splitSessionMarkdownLines(String(content || ''));
725
+ let section = '';
726
+ let output = '';
727
+
728
+ for (let index = 0; index < lines.length; index += 1) {
729
+ const line = lines[index];
730
+ if (/^## /u.test(line.text)) {
731
+ section = line.text === '## Iterações'
732
+ ? 'iterations'
733
+ : (line.text === '## Encerramento' ? 'closing' : '');
734
+ output += `${line.text}${line.eol}`;
735
+ continue;
736
+ }
737
+
738
+ const rules = section === 'iterations'
739
+ ? GENERATED_ITERATION_LINE_RULES
740
+ : (section === 'closing' ? GENERATED_CLOSING_LINE_RULES : []);
741
+ const generated = generatedSessionLine(line.text, rules);
742
+ if (!generated) {
743
+ output += `${line.text}${line.eol}`;
744
+ continue;
745
+ }
746
+
747
+ let value = generated.value;
748
+ let last = index;
749
+ let mode = '';
750
+ if (generated.assistant) {
751
+ for (let next = index + 1; next < lines.length; next += 1) {
752
+ const nextMode = generatedMetadataContinuation(lines[next].text, mode);
753
+ if (nextMode === null) break;
754
+ value += `${lines[last].eol}${lines[next].text}`;
755
+ last = next;
756
+ mode = nextMode;
757
+ }
758
+ value = sanitizeAssistantMessage(value);
759
+ }
760
+
761
+ output += `${generated.prefix}${escapeMarkdownHtmlTags(value)}${lines[last].eol}`;
762
+ index = last;
763
+ }
764
+ return output;
765
+ }
766
+
652
767
  export function finalizeSessionFile(sessionPath, tx, created, endedAt, vaultBase = '') {
653
768
  const pending = extractPending(tx.rawTextForDetection);
654
769
  const links = (items) => items.length ? items.map((rel) => ` - ${wikilinkFromRel(rel)}`).join('\n') : ' - Nenhuma';
@@ -673,7 +788,7 @@ ${formatPendingClosing(pending)}
673
788
  // As três seções derivadas saem do MESMO `created` que monta o Encerramento — antes
674
789
  // elas ficavam de fora deste write e a nota mentia no corpo (ver hooks/derived-sections.mjs).
675
790
  applyDerivedSections(
676
- replacePendingSection(updateFrontmatter(content, endedAt), pending),
791
+ replacePendingSection(updateFrontmatter(sanitizeGeneratedSessionMarkdown(content), endedAt), pending),
677
792
  created,
678
793
  ),
679
794
  closing,
@@ -681,8 +796,9 @@ ${formatPendingClosing(pending)}
681
796
  }
682
797
 
683
798
  export function sessionFinalSummary(tx) {
684
- return tx.latestAssistantMessage
685
- ? truncate(tx.latestAssistantMessage, 500)
799
+ const assistantSummary = sanitizeAssistantMessage(tx.latestAssistantMessage);
800
+ return assistantSummary
801
+ ? compactText(assistantSummary, 500)
686
802
  : `Sessão encerrada com ${tx.userPrompts.length} prompts e ${tx.tools.length} ferramentas registradas.`;
687
803
  }
688
804
 
@@ -1065,9 +1181,13 @@ export async function main({
1065
1181
  const turnId = turnIdentity.id;
1066
1182
  const now = finalizing ? new Date() : null;
1067
1183
  const endedAt = finalizing ? formatLocalIso(now) : '';
1068
- const stopTurnSequence = turnIdentity.order;
1069
1184
  const causalStop = finalizing
1070
1185
  ? mutateSessionRegistry(vaultBase, (registry) => {
1186
+ const stopTurnSequence = resolveRegisteredTurnSequence(
1187
+ registry.sessions?.[sessionId],
1188
+ turnId,
1189
+ turnIdentity.order,
1190
+ );
1071
1191
  const activationId = resolveStopActivation(registry, {
1072
1192
  session_id: sessionId,
1073
1193
  activation_id: input.activation_id || input.activationId || '',
@@ -1099,9 +1219,11 @@ export async function main({
1099
1219
  activation,
1100
1220
  stopDisposition: cas.stopDisposition,
1101
1221
  canPromoteMemory: cas.canPromoteMemory,
1222
+ turnSequence: stopTurnSequence,
1102
1223
  };
1103
1224
  })
1104
1225
  : null;
1226
+ const stopTurnSequence = causalStop?.turnSequence ?? turnIdentity.order;
1105
1227
  let memoryHandoff = null;
1106
1228
  let memoryAttempt = null;
1107
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.66.5",
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": [
@@ -40,7 +40,7 @@
40
40
  "node": ">=18"
41
41
  },
42
42
  "scripts": {
43
- "check": "node --check bin/wendkeep.mjs && node --check packages/cli/src/index.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/flow-store.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/integrations/src/host-hooks.mjs && node --check packages/integrations/src/hook-envelope.mjs && node --check packages/integrations/src/prompt-content.mjs && node --check packages/integrations/src/transcript-usage.mjs && node --check packages/integrations/src/transcripts.mjs && node --check packages/integrations/src/session-identity.mjs && node --check packages/integrations/src/index.mjs && node --check packages/mcp/src/config.mjs && node --check packages/mcp/src/index.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/locale.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
43
+ "check": "node --check bin/wendkeep.mjs && node --check packages/cli/src/index.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/operating-profile-task-store.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/flow-store.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/integrations/src/host-hooks.mjs && node --check packages/integrations/src/hook-envelope.mjs && node --check packages/integrations/src/prompt-content.mjs && node --check packages/integrations/src/transcript-usage.mjs && node --check packages/integrations/src/transcripts.mjs && node --check packages/integrations/src/session-identity.mjs && node --check packages/integrations/src/index.mjs && node --check packages/mcp/src/config.mjs && node --check packages/mcp/src/index.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/locale.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
44
44
  "test": "node --test --test-concurrency=2",
45
45
  "release": "node scripts/release.mjs",
46
46
  "release:dry": "node scripts/release.mjs --dry-run",
@@ -70,6 +70,6 @@
70
70
  },
71
71
  "devDependencies": {
72
72
  "acorn": "^8.18.0",
73
- "wendkeep": "^0.57.2"
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');
@@ -6,8 +6,16 @@ export const OPERATING_PROFILES = Object.freeze([
6
6
  'ASSURE',
7
7
  ]);
8
8
  export const DEFAULT_OPERATING_PROFILE = 'GOVERN';
9
+ export const ADAPTIVE_OPERATING_PROFILES = Object.freeze([
10
+ 'FLOW',
11
+ 'GUIDE',
12
+ 'GOVERN',
13
+ 'ASSURE',
14
+ ]);
9
15
 
10
16
  const PROFILE_SET = new Set(OPERATING_PROFILES);
17
+ const ADAPTIVE_PROFILE_SET = new Set(ADAPTIVE_OPERATING_PROFILES);
18
+ export const TASK_PROFILE_REASON_MAX_LENGTH = 500;
11
19
 
12
20
  function policy(profile, route, options) {
13
21
  return Object.freeze({
@@ -70,6 +78,125 @@ function canonicalProfile(value) {
70
78
  return value.trim().toUpperCase();
71
79
  }
72
80
 
81
+ function taskProfileError(code, message) {
82
+ const error = new Error(message);
83
+ error.code = code;
84
+ return error;
85
+ }
86
+
87
+ function taskProfile(value) {
88
+ const profile = canonicalProfile(value);
89
+ if (ADAPTIVE_PROFILE_SET.has(profile)) return profile;
90
+ throw taskProfileError(
91
+ 'WENDKEEP_TASK_PROFILE_INVALID',
92
+ `Perfil temporário inválido: ${typeof value === 'string' ? `"${value}"` : String(value)}. `
93
+ + `Use ${ADAPTIVE_OPERATING_PROFILES.join(', ')}; OFF exige seleção humana persistente.`,
94
+ );
95
+ }
96
+
97
+ function taskReason(value) {
98
+ const reason = typeof value === 'string' ? value.trim() : '';
99
+ if (reason && reason.length <= TASK_PROFILE_REASON_MAX_LENGTH) return reason;
100
+ throw taskProfileError(
101
+ 'WENDKEEP_TASK_PROFILE_REASON_INVALID',
102
+ `Motivo da rota temporária deve ter entre 1 e ${TASK_PROFILE_REASON_MAX_LENGTH} caracteres.`,
103
+ );
104
+ }
105
+
106
+ function taskSequence(value) {
107
+ const sequence = Number(value);
108
+ return Number.isSafeInteger(sequence) && sequence > 0 ? sequence : null;
109
+ }
110
+
111
+ function taskContextError() {
112
+ return taskProfileError(
113
+ 'WENDKEEP_TASK_PROFILE_CONTEXT_INVALID',
114
+ 'Rota temporária exige sessão, prompt causal, lease id e timestamp válidos.',
115
+ );
116
+ }
117
+
118
+ export function createTaskOperatingProfileLease({
119
+ profile,
120
+ reason,
121
+ sessionId,
122
+ turnId = '',
123
+ turnSequence,
124
+ leaseId,
125
+ issuedAt,
126
+ } = {}) {
127
+ const selected = taskProfile(profile);
128
+ const auditedReason = taskReason(reason);
129
+ const session = typeof sessionId === 'string' ? sessionId.trim() : '';
130
+ const requestTurnId = typeof turnId === 'string' ? turnId.trim() : '';
131
+ const sequence = taskSequence(turnSequence);
132
+ const id = typeof leaseId === 'string' ? leaseId.trim() : '';
133
+ const issued = typeof issuedAt === 'string' ? issuedAt.trim() : '';
134
+ if (!session || !requestTurnId || sequence === null || !id || !issued || !Number.isFinite(Date.parse(issued))) {
135
+ throw taskContextError();
136
+ }
137
+ return {
138
+ lease_id: id,
139
+ state: 'active',
140
+ profile: selected,
141
+ requested_by: 'llm-harness',
142
+ reason: auditedReason,
143
+ session_id: session,
144
+ request_turn_id: requestTurnId,
145
+ request_turn_sequence: sequence,
146
+ issued_at: issued,
147
+ expires_on: 'request-stop',
148
+ };
149
+ }
150
+
151
+ export function evaluateTaskOperatingProfileLease(lease, {
152
+ sessionId = '',
153
+ turnId = '',
154
+ turnSequence,
155
+ } = {}) {
156
+ if (lease === undefined || lease === null) return { state: 'absent' };
157
+ if (!lease || typeof lease !== 'object' || Array.isArray(lease)) return { state: 'invalid' };
158
+
159
+ let normalized;
160
+ try {
161
+ normalized = createTaskOperatingProfileLease({
162
+ profile: lease.profile,
163
+ reason: lease.reason,
164
+ sessionId: lease.session_id,
165
+ turnId: lease.request_turn_id,
166
+ turnSequence: lease.request_turn_sequence,
167
+ leaseId: lease.lease_id,
168
+ issuedAt: lease.issued_at,
169
+ });
170
+ } catch {
171
+ return {
172
+ state: 'invalid',
173
+ ...(typeof lease.lease_id === 'string' && lease.lease_id ? { lease_id: lease.lease_id } : {}),
174
+ };
175
+ }
176
+ if (lease.requested_by !== 'llm-harness' || lease.expires_on !== 'request-stop') {
177
+ return { state: 'invalid', lease_id: normalized.lease_id };
178
+ }
179
+ if (lease.state === 'consumed' || lease.state === 'expired') {
180
+ return { ...lease, ...normalized, state: lease.state };
181
+ }
182
+ if (lease.state !== 'active') return { state: 'invalid', lease_id: normalized.lease_id };
183
+
184
+ const currentSession = typeof sessionId === 'string' ? sessionId.trim() : '';
185
+ const currentTurnId = typeof turnId === 'string' ? turnId.trim() : '';
186
+ const currentSequence = taskSequence(turnSequence);
187
+ if (!currentSession || !currentTurnId || currentSequence === null) {
188
+ return { ...normalized, state: 'invalid' };
189
+ }
190
+ if (normalized.session_id !== currentSession) {
191
+ return { ...normalized, state: 'invalid' };
192
+ }
193
+ const turnIdMismatch = normalized.request_turn_id !== currentTurnId;
194
+ if (turnIdMismatch || normalized.request_turn_sequence !== currentSequence) {
195
+ return { ...normalized, state: 'expired' };
196
+ }
197
+ return normalized;
198
+ }
199
+
73
200
  export function normalizeOperatingProfile(value, { strict = false } = {}) {
74
201
  const normalized = canonicalProfile(value);
75
202
  if (PROFILE_SET.has(normalized)) return normalized;
@@ -6,6 +6,38 @@ import { existsSync, readFileSync } from 'node:fs';
6
6
  import { dirname, join, resolve } from 'node:path';
7
7
 
8
8
  export const SENSOR_VAULT_ENV = 'WENDKEEP_SENSOR_VAULT';
9
+ const SENSOR_OUTPUT_MAX_BUFFER = 8 * 1024 * 1024;
10
+ const SENSOR_DIAGNOSTIC_MAX_LENGTH = 2000;
11
+
12
+ function sanitizeSensorDiagnostic(value) {
13
+ return String(value || '')
14
+ .replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '')
15
+ .replace(/\b(gh[pousr]_[A-Za-z0-9_]{12,})\b/g, '[REDACTED_SECRET]')
16
+ .replace(/\b(sk-[A-Za-z0-9_-]{12,})\b/g, '[REDACTED_SECRET]')
17
+ .replace(/\b(whsec_[A-Za-z0-9_/-]{8,})\b/g, '[REDACTED_SECRET]')
18
+ .replace(/\b(xox[baprs]-[A-Za-z0-9-]{12,})\b/g, '[REDACTED_SECRET]')
19
+ .replace(/\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|API_KEY)[A-Z0-9_]*)\s*[:=]\s*["']?[^"'\s]+/gi, '$1=[REDACTED_SECRET]')
20
+ .replace(/:\/\/([^:\s/@]+):([^@\s/]+)@/g, '://[REDACTED_SECRET]@')
21
+ .replace(/\r/g, '')
22
+ .trim();
23
+ }
24
+
25
+ function sensorFailureNote(result = {}) {
26
+ const status = result.status ?? 'null';
27
+ const header = [
28
+ `exit=${status}`,
29
+ ...(result.signal ? [`signal=${result.signal}`] : []),
30
+ ].join(' ');
31
+ const detail = sanitizeSensorDiagnostic([
32
+ result.error?.message,
33
+ result.stdout,
34
+ result.stderr,
35
+ ].filter(Boolean).join('\n'));
36
+ if (!detail) return header;
37
+ const room = SENSOR_DIAGNOSTIC_MAX_LENGTH - header.length - 1;
38
+ const bounded = detail.length > room ? `…${detail.slice(-(room - 1))}` : detail;
39
+ return `${header}\n${bounded}`;
40
+ }
9
41
 
10
42
  export function sensorProcessEnv(vaultBase, inherited = process.env) {
11
43
  return {
@@ -59,8 +91,16 @@ export function runSensors(sensors, ids, { spawn = spawnSync, cwd, env, now } =
59
91
  for (const id of ids) {
60
92
  const s = byId[id];
61
93
  if (!s) { evidence.push({ id, status: 'red', ts, severity: 'critical', note: 'sensor não definido' }); continue; }
62
- const r = spawn(s.command, [], { cwd, shell: true, stdio: 'ignore', ...(env ? { env } : {}) });
94
+ const r = spawn(s.command, [], {
95
+ cwd,
96
+ shell: true,
97
+ encoding: 'utf8',
98
+ maxBuffer: SENSOR_OUTPUT_MAX_BUFFER,
99
+ stdio: ['ignore', 'pipe', 'pipe'],
100
+ ...(env ? { env } : {}),
101
+ });
63
102
  const entry = { id, status: (r.status ?? 1) === 0 ? 'green' : 'red', ts, severity: s.severity || 'critical' };
103
+ if (entry.status === 'red') entry.note = sensorFailureNote(r);
64
104
  if (s.type === 'mutation' && s.report) {
65
105
  // Delegated mutation (Wave B): read the tool's mutation-testing-elements report and
66
106
  // attach surviving mutants so verify can turn them into fix tasks.
@@ -18,3 +18,126 @@ export function redactSecrets(text) {
18
18
  .replace(/\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|API_KEY)[A-Z0-9_]*)\s*[:=]\s*["']?[^"'\s]+/gi, '$1=[REDACTED_SECRET]')
19
19
  .replace(/:\/\/([^:\s/@]+):([^@\s/]+)@/g, '://[REDACTED_SECRET]@');
20
20
  }
21
+
22
+ function metadataPayloadLine(name, line) {
23
+ if (name === 'citation_entries') {
24
+ return /\|note=\[[^\]]*\]\s*$/i.test(line)
25
+ || /^[^\s<>]+:\d+(?:-\d+)?(?:\|[^\s].*)?$/i.test(line);
26
+ }
27
+ if (name === 'rollout_ids') {
28
+ return /^(?:[0-9a-f]{8,}(?:-[0-9a-f-]+)*|019f-[A-Za-z0-9_-]+)$/i.test(line);
29
+ }
30
+ return false;
31
+ }
32
+
33
+ function consumeTruncatedMetadata(source, name, tagEnd) {
34
+ let cursor = tagEnd;
35
+ let mode = name;
36
+ let openingLine = true;
37
+
38
+ while (cursor < source.length) {
39
+ const newline = source.indexOf('\n', cursor);
40
+ const lineEnd = newline === -1 ? source.length : newline;
41
+ const line = source.slice(cursor, lineEnd).replace(/\r$/, '');
42
+ const clean = line.trim();
43
+
44
+ if (!clean) {
45
+ if (!openingLine) return cursor;
46
+ } else if (/^<\/?(?:oai-mem-citation|citation_entries|rollout_ids)\b/i.test(clean)) {
47
+ const nested = [...clean.matchAll(/<(citation_entries|rollout_ids)\b[^>]*>/gi)].at(-1);
48
+ if (nested) mode = nested[1].toLowerCase();
49
+ } else if (!(openingLine && name !== 'oai-mem-citation') && !metadataPayloadLine(mode, clean)) {
50
+ return cursor;
51
+ }
52
+
53
+ if (newline === -1) return source.length;
54
+ cursor = newline + 1;
55
+ openingLine = false;
56
+ }
57
+ return cursor;
58
+ }
59
+
60
+ function openingPayloadLooksStructural(source, opening) {
61
+ const name = opening[1].toLowerCase();
62
+ const tagEnd = opening.index + opening[0].length;
63
+ const rest = source.slice(tagEnd);
64
+ if (new RegExp(`<\/${name}\\s*>`, 'i').test(rest)) return true;
65
+
66
+ if (name === 'oai-mem-citation') {
67
+ const child = /^[\t\r\n ]*<(citation_entries|rollout_ids)\b[^>]*>/i.exec(rest);
68
+ if (!child) return !rest.trim();
69
+ const childRest = rest.slice(child[0].length);
70
+ const lineEnd = childRest.search(/\r?\n/u);
71
+ const sameLine = childRest.slice(0, lineEnd === -1 ? childRest.length : lineEnd);
72
+ if (!sameLine.trim()) return true;
73
+ if (!/^[\t ]/u.test(childRest)) return true;
74
+ if (/^<\/?(?:oai-mem-citation|citation_entries|rollout_ids)\b/i.test(sameLine.trim())) return true;
75
+ return metadataPayloadLine(child[1].toLowerCase(), sameLine.trim());
76
+ }
77
+
78
+ const lineEnd = rest.search(/\r?\n/u);
79
+ const sameLine = rest.slice(0, lineEnd === -1 ? rest.length : lineEnd);
80
+ if (!sameLine.trim()) return true;
81
+ if (!/^[\t ]/u.test(rest)) return true;
82
+ if (/^<\/?(?:oai-mem-citation|citation_entries|rollout_ids)\b/i.test(sameLine.trim())) return true;
83
+ return metadataPayloadLine(name, sameLine.trim());
84
+ }
85
+
86
+ function metadataStart(source, opening) {
87
+ const tagStart = opening.index;
88
+ const before = source.slice(0, tagStart);
89
+ const adjacentSession = /<\/session>[\t\r\n ]*$/i.exec(before);
90
+ if (adjacentSession) return adjacentSession.index;
91
+
92
+ if (!openingPayloadLooksStructural(source, opening)) return -1;
93
+
94
+ const lineStart = before.lastIndexOf('\n') + 1;
95
+ if (!before.slice(lineStart).trim()) return tagStart;
96
+
97
+ const name = opening[1].toLowerCase();
98
+ if (name === 'oai-mem-citation'
99
+ && tagStart > 0
100
+ && !/\s/u.test(source[tagStart - 1])) {
101
+ return tagStart;
102
+ }
103
+ return -1;
104
+ }
105
+
106
+ function findAssistantMetadataRemoval(source) {
107
+ const openings = source.matchAll(/<(oai-mem-citation|citation_entries|rollout_ids)\b[^>]*>/gi);
108
+ for (const opening of openings) {
109
+ const start = metadataStart(source, opening);
110
+ if (start < 0) continue;
111
+
112
+ const name = opening[1].toLowerCase();
113
+ const tagEnd = opening.index + opening[0].length;
114
+ const closing = new RegExp(`<\/${name}\\s*>`, 'i').exec(source.slice(tagEnd));
115
+ const end = closing
116
+ ? tagEnd + closing.index + closing[0].length
117
+ : consumeTruncatedMetadata(source, name, tagEnd);
118
+ return { start, end };
119
+ }
120
+ return null;
121
+ }
122
+
123
+ function removeAssistantMetadata(source, removal) {
124
+ const before = source.slice(0, removal.start).trimEnd();
125
+ const after = source.slice(removal.end).trimStart();
126
+ if (!before) return after;
127
+ if (!after) return before;
128
+ return `${before}\n${after}`;
129
+ }
130
+
131
+ export function sanitizeAssistantMessage(text) {
132
+ let source = String(text || '');
133
+ if (!source) return '';
134
+
135
+ while (source) {
136
+ const removal = findAssistantMetadataRemoval(source);
137
+ if (!removal) break;
138
+ const next = removeAssistantMetadata(source, removal);
139
+ if (next.length >= source.length) break;
140
+ source = next;
141
+ }
142
+ return source;
143
+ }