wendkeep 0.66.5 → 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.
@@ -3,6 +3,7 @@ import { resolve } from 'node:path';
3
3
 
4
4
  import {
5
5
  DEFAULT_OPERATING_PROFILE,
6
+ evaluateTaskOperatingProfileLease,
6
7
  normalizeOperatingProfile,
7
8
  operatingProfilePolicy,
8
9
  resolveOperatingProfile,
@@ -68,6 +69,22 @@ function sessionOverride(entry) {
68
69
  }
69
70
  }
70
71
 
72
+ function nonNegativeSequence(value, fallback = null) {
73
+ const parsed = Number(value);
74
+ return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : fallback;
75
+ }
76
+
77
+ function taskLeaseContext(entry, input, sessionId) {
78
+ return {
79
+ sessionId,
80
+ turnId: input?.turn_id || input?.turnId || entry?.last_prompt_turn_id || '',
81
+ turnSequence: nonNegativeSequence(
82
+ input?.turn_sequence ?? input?.turnSequence,
83
+ nonNegativeSequence(entry?.last_turn_sequence),
84
+ ),
85
+ };
86
+ }
87
+
71
88
  function canonicalPath(value) {
72
89
  const path = resolve(value).replaceAll('\\', '/');
73
90
  return process.platform === 'win32' ? path.toLowerCase() : path;
@@ -105,7 +122,8 @@ function matchingProjectBinding(vaultResolution, input) {
105
122
  }
106
123
  }
107
124
 
108
- // Resolution precedence for hooks: explicit session override -> project binding -> GOVERN.
125
+ // Resolution precedence for hooks: active request lease -> explicit session override
126
+ // -> project binding -> GOVERN.
109
127
  // Binding corruption is never interpreted as OFF: an authoritative Vault keeps the Keep Core
110
128
  // alive under GOVERN and carries a visible diagnostic to each entrypoint.
111
129
  export function resolveHookOperatingProfile({
@@ -134,7 +152,20 @@ export function resolveHookOperatingProfile({
134
152
  const entry = identity.state === 'resolved'
135
153
  ? readSessionRegistry(vaultResolution.base).sessions?.[identity.canonicalConversationId] || null
136
154
  : null;
137
- const selected = sessionOverride(entry) || project;
155
+ const base = sessionOverride(entry) || project;
156
+ const taskLease = evaluateTaskOperatingProfileLease(
157
+ entry?.operating_profile_task,
158
+ taskLeaseContext(entry, input, identity.canonicalConversationId || ''),
159
+ );
160
+ const selected = taskLease.state === 'active'
161
+ ? {
162
+ profile: taskLease.profile,
163
+ source: 'task-lease',
164
+ valid: true,
165
+ configured: true,
166
+ raw: taskLease.profile,
167
+ }
168
+ : base;
138
169
  return {
139
170
  ...selected,
140
171
  policy: operatingProfilePolicy(selected.profile),
@@ -142,6 +173,9 @@ export function resolveHookOperatingProfile({
142
173
  projectRoot: vaultResolution.projectRoot,
143
174
  identity,
144
175
  entry,
176
+ baseProfile: base.profile,
177
+ baseSource: base.source,
178
+ taskLease,
145
179
  resolution: vaultResolution,
146
180
  ...(bindingError ? { bindingError } : {}),
147
181
  };
@@ -0,0 +1,77 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ import {
4
+ createTaskOperatingProfileLease,
5
+ } from '../src/operating-profile.mjs';
6
+ import { mutateSessionRegistry } from './obsidian-common.mjs';
7
+
8
+ function isoTimestamp(now) {
9
+ if (typeof now === 'string') return now;
10
+ if (now instanceof Date) return now.toISOString();
11
+ return new Date().toISOString();
12
+ }
13
+
14
+ function missingSessionError(sessionId) {
15
+ const error = new Error(`sessão não encontrada: ${sessionId}`);
16
+ error.code = 'WENDKEEP_SESSION_NOT_FOUND';
17
+ return error;
18
+ }
19
+
20
+ export function setSessionTaskOperatingProfile(vaultBase, sessionId, profile, {
21
+ reason,
22
+ leaseId = randomUUID(),
23
+ now,
24
+ } = {}) {
25
+ const issuedAt = isoTimestamp(now);
26
+ return mutateSessionRegistry(vaultBase, (registry) => {
27
+ const sessions = registry.sessions || (registry.sessions = {});
28
+ if (!Object.hasOwn(sessions, sessionId)) throw missingSessionError(sessionId);
29
+ const current = sessions[sessionId];
30
+ const turnId = typeof current.last_prompt_turn_id === 'string'
31
+ ? current.last_prompt_turn_id.trim()
32
+ : '';
33
+ const hasRegisteredTurn = Boolean(
34
+ turnId
35
+ && current.turn_sequences
36
+ && Object.hasOwn(current.turn_sequences, turnId)
37
+ && current.turn_sequences[turnId] === current.last_turn_sequence
38
+ );
39
+ const lease = createTaskOperatingProfileLease({
40
+ profile,
41
+ reason,
42
+ sessionId,
43
+ turnId,
44
+ turnSequence: hasRegisteredTurn ? current.last_turn_sequence : undefined,
45
+ leaseId,
46
+ issuedAt,
47
+ });
48
+ sessions[sessionId] = {
49
+ ...current,
50
+ operating_profile_task: lease,
51
+ updated_at: issuedAt,
52
+ };
53
+ return lease;
54
+ });
55
+ }
56
+
57
+ export function consumeSessionTaskOperatingProfile(vaultBase, sessionId, leaseId, {
58
+ now,
59
+ } = {}) {
60
+ if (!sessionId || !leaseId) return false;
61
+ const consumedAt = isoTimestamp(now);
62
+ return mutateSessionRegistry(vaultBase, (registry) => {
63
+ const current = registry.sessions?.[sessionId];
64
+ const lease = current?.operating_profile_task;
65
+ if (!lease || lease.state !== 'active' || lease.lease_id !== leaseId) return false;
66
+ registry.sessions[sessionId] = {
67
+ ...current,
68
+ operating_profile_task: {
69
+ ...lease,
70
+ state: 'consumed',
71
+ consumed_at: consumedAt,
72
+ },
73
+ updated_at: consumedAt,
74
+ };
75
+ return true;
76
+ });
77
+ }
@@ -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,
@@ -213,6 +214,13 @@ function escapeMarkdownBackticks(text) {
213
214
  return escaped;
214
215
  }
215
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
+
216
224
  function compactText(text, max = 600) {
217
225
  const clean = redactSecrets(String(text || ''))
218
226
  .replace(/\r/g, '\n')
@@ -223,7 +231,8 @@ function compactText(text, max = 600) {
223
231
  const clipped = truncate(source, max);
224
232
  // Um corte no meio de código inline/fence pode casar com backticks da próxima
225
233
  // entrada gerada. Só snippets realmente truncados perdem a formatação incompleta.
226
- return compact.length > max ? escapeMarkdownBackticks(clipped) : clipped;
234
+ const markdownSafe = compact.length > max ? escapeMarkdownBackticks(clipped) : clipped;
235
+ return escapeMarkdownHtmlTags(markdownSafe);
227
236
  }
228
237
 
229
238
  function selectTurn(tx, turnId) {
@@ -235,6 +244,11 @@ function selectTurn(tx, turnId) {
235
244
 
236
245
  function formatConversation(turn) {
237
246
  const entries = (turn.conversation || [])
247
+ .map((entry) => (
248
+ entry.role === 'Assistente'
249
+ ? { ...entry, text: sanitizeAssistantMessage(entry.text) }
250
+ : entry
251
+ ))
238
252
  .filter((entry) => entry.text && !shouldIgnoreUserText(entry.text));
239
253
  if (!entries.length) return '- Nenhuma mensagem útil capturada no transcript.';
240
254
 
@@ -304,7 +318,9 @@ export function buildIterationBlock(tx, input) {
304
318
  const now = Number.isFinite(parsedDate.getTime()) ? parsedDate : new Date();
305
319
  const promptText = turn.userPrompts.at(-1) || tx.latestUserPrompt || '';
306
320
  const latestAssistant = turn.assistantMessages.at(-1) || tx.latestAssistantMessage || '';
307
- 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
+ );
308
324
  const files = [...new Set([...(turn.consultedFiles || []), ...(turn.changedFiles || [])])];
309
325
  const model = turn.model || tx.model || '';
310
326
 
@@ -325,7 +341,7 @@ ${formatConversation(turn)}
325
341
 
326
342
  **Arquivos detectados no turno:** ${formatInlineList(files, 'Nenhum arquivo detectado automaticamente.')}
327
343
 
328
- **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)}
329
345
  `;
330
346
  }
331
347
 
@@ -649,6 +665,104 @@ function replaceClosingSection(content, closing) {
649
665
  return `${content.slice(0, index).trimEnd()}\n\n${closing}\n`;
650
666
  }
651
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
+
652
766
  export function finalizeSessionFile(sessionPath, tx, created, endedAt, vaultBase = '') {
653
767
  const pending = extractPending(tx.rawTextForDetection);
654
768
  const links = (items) => items.length ? items.map((rel) => ` - ${wikilinkFromRel(rel)}`).join('\n') : ' - Nenhuma';
@@ -673,7 +787,7 @@ ${formatPendingClosing(pending)}
673
787
  // As três seções derivadas saem do MESMO `created` que monta o Encerramento — antes
674
788
  // elas ficavam de fora deste write e a nota mentia no corpo (ver hooks/derived-sections.mjs).
675
789
  applyDerivedSections(
676
- replacePendingSection(updateFrontmatter(content, endedAt), pending),
790
+ replacePendingSection(updateFrontmatter(sanitizeGeneratedSessionMarkdown(content), endedAt), pending),
677
791
  created,
678
792
  ),
679
793
  closing,
@@ -681,8 +795,9 @@ ${formatPendingClosing(pending)}
681
795
  }
682
796
 
683
797
  export function sessionFinalSummary(tx) {
684
- return tx.latestAssistantMessage
685
- ? truncate(tx.latestAssistantMessage, 500)
798
+ const assistantSummary = sanitizeAssistantMessage(tx.latestAssistantMessage);
799
+ return assistantSummary
800
+ ? compactText(assistantSummary, 500)
686
801
  : `Sessão encerrada com ${tx.userPrompts.length} prompts e ${tx.tools.length} ferramentas registradas.`;
687
802
  }
688
803
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.66.5",
3
+ "version": "0.67.0",
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.66.5"
74
74
  }
75
75
  }
@@ -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.