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.
- package/CHANGELOG.md +52 -0
- package/README.en.md +76 -3
- package/README.md +76 -3
- package/docs/en/commands/operating-profiles.md +65 -10
- package/docs/en/commands/sessions-and-import.md +34 -9
- package/docs/en/commands/verify.md +5 -3
- package/docs/pt-BR/commands/operating-profiles.md +66 -11
- package/docs/pt-BR/commands/sessions-and-import.md +33 -8
- package/docs/pt-BR/commands/verify.md +6 -3
- package/hooks/change-nag.mjs +8 -0
- package/hooks/obsidian-common.mjs +7 -0
- package/hooks/operating-profile-runtime.mjs +36 -2
- package/hooks/operating-profile-task-store.mjs +77 -0
- package/hooks/session-backfill.mjs +26 -1
- package/hooks/session-ensure.mjs +22 -0
- package/hooks/session-stop.mjs +129 -7
- package/hooks/subagent-stop.mjs +35 -3
- package/package.json +3 -3
- package/packages/cli/src/index.mjs +3 -3
- package/packages/harness/src/operating-profile.mjs +127 -0
- package/packages/harness/src/sensors-core.mjs +41 -1
- package/packages/integrations/src/prompt-content.mjs +123 -0
- package/packages/integrations/src/transcripts.mjs +26 -10
- package/src/profile.mjs +95 -17
- package/src/skills-seed.mjs +38 -2
- package/src/sync-defs.mjs +16 -1
package/hooks/session-stop.mjs
CHANGED
|
@@ -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
|
+
'<$1$2>',
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
685
|
-
|
|
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) {
|
package/hooks/subagent-stop.mjs
CHANGED
|
@@ -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
|
|
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,
|
|
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,
|
|
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.
|
|
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.
|
|
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, [], {
|
|
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
|
+
}
|