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.
- package/CHANGELOG.md +28 -0
- package/README.en.md +75 -2
- package/README.md +75 -2
- package/docs/en/commands/operating-profiles.md +65 -10
- package/docs/en/commands/sessions-and-import.md +7 -1
- 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 +7 -1
- package/docs/pt-BR/commands/verify.md +6 -3
- package/hooks/change-nag.mjs +8 -0
- package/hooks/operating-profile-runtime.mjs +36 -2
- package/hooks/operating-profile-task-store.mjs +77 -0
- package/hooks/session-stop.mjs +121 -6
- package/package.json +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 +16 -10
- package/src/profile.mjs +95 -17
- package/src/skills-seed.mjs +38 -2
- package/src/sync-defs.mjs +6 -1
|
@@ -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:
|
|
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
|
|
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
|
+
}
|
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,
|
|
@@ -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
|
+
'<$1$2>',
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
685
|
-
|
|
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.
|
|
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.
|
|
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, [], {
|
|
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.
|