wendkeep 0.66.4 → 0.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/README.en.md +78 -5
- package/README.md +78 -5
- package/docs/en/commands/costs-and-observability.md +21 -7
- package/docs/en/commands/maintenance-and-diagnostics.md +13 -1
- package/docs/en/commands/operating-profiles.md +65 -10
- package/docs/en/commands/sessions-and-import.md +22 -1
- package/docs/en/commands/verify.md +5 -3
- package/docs/pt-BR/commands/costs-and-observability.md +21 -7
- package/docs/pt-BR/commands/maintenance-and-diagnostics.md +12 -1
- package/docs/pt-BR/commands/operating-profiles.md +66 -11
- package/docs/pt-BR/commands/sessions-and-import.md +20 -0
- package/docs/pt-BR/commands/verify.md +6 -3
- package/hooks/change-nag.mjs +8 -0
- package/hooks/codex-rollout-meta.mjs +112 -0
- package/hooks/codex-subagent-graph.mjs +903 -0
- package/hooks/harness-doctor.mjs +82 -1
- package/hooks/import-sessions.mjs +185 -50
- package/hooks/operating-profile-runtime.mjs +36 -2
- package/hooks/operating-profile-task-store.mjs +77 -0
- package/hooks/session-identity.mjs +40 -5
- package/hooks/session-observability-lifecycle.mjs +129 -0
- package/hooks/session-observability-state.mjs +241 -0
- package/hooks/session-observability-store.mjs +436 -0
- package/hooks/session-observability.mjs +647 -21
- package/hooks/session-stop.mjs +339 -11
- package/hooks/subagent-stop.mjs +266 -12
- package/hooks/subagent-usage.mjs +65 -0
- package/hooks/token-usage.mjs +81 -4
- 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/cost.mjs +40 -6
- package/src/doctor.mjs +4 -1
- package/src/profile.mjs +95 -17
- package/src/rebuild-costs.mjs +220 -34
- package/src/skills-seed.mjs +38 -2
- package/src/sync-defs.mjs +6 -1
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
isBootstrapPrompt,
|
|
3
|
+
redactSecrets,
|
|
4
|
+
sanitizeAssistantMessage,
|
|
5
|
+
} from './prompt-content.mjs';
|
|
2
6
|
import {
|
|
3
7
|
addUsage,
|
|
4
8
|
emptyTokenUsage,
|
|
@@ -73,6 +77,14 @@ function addConversation(turn, role, value) {
|
|
|
73
77
|
}
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
function addAssistantMessage(result, turn, value) {
|
|
81
|
+
const text = sanitizeAssistantMessage(value);
|
|
82
|
+
if (!text) return;
|
|
83
|
+
addUnique(result.assistantMessages, text);
|
|
84
|
+
addUnique(turn.assistantMessages, text);
|
|
85
|
+
addConversation(turn, 'Assistente', text);
|
|
86
|
+
}
|
|
87
|
+
|
|
76
88
|
function normalizeRoot(value) {
|
|
77
89
|
return String(value || '').replace(/\\+/g, '/').replace(/\/+$/, '');
|
|
78
90
|
}
|
|
@@ -207,9 +219,7 @@ export function parseCodexTranscriptContent(content, options = {}) {
|
|
|
207
219
|
const text = event.payload.message || event.payload.text || '';
|
|
208
220
|
if (text) {
|
|
209
221
|
const turn = ensureTurn(event.payload.turn_id || result.latestTurnId, event.timestamp);
|
|
210
|
-
|
|
211
|
-
addUnique(turn.assistantMessages, text);
|
|
212
|
-
addConversation(turn, 'Assistente', text);
|
|
222
|
+
addAssistantMessage(result, turn, text);
|
|
213
223
|
}
|
|
214
224
|
continue;
|
|
215
225
|
}
|
|
@@ -234,9 +244,7 @@ export function parseCodexTranscriptContent(content, options = {}) {
|
|
|
234
244
|
addConversation(turn, 'Usuário', text);
|
|
235
245
|
}
|
|
236
246
|
if (payload.role === 'assistant') {
|
|
237
|
-
|
|
238
|
-
addUnique(turn.assistantMessages, text);
|
|
239
|
-
addConversation(turn, 'Assistente', text);
|
|
247
|
+
addAssistantMessage(result, turn, text);
|
|
240
248
|
}
|
|
241
249
|
continue;
|
|
242
250
|
}
|
|
@@ -356,9 +364,7 @@ export function parseClaudeTranscriptContent(content, options = {}) {
|
|
|
356
364
|
const blocks = Array.isArray(event.message?.content) ? event.message.content : [];
|
|
357
365
|
for (const block of blocks) {
|
|
358
366
|
if (block?.type === 'text' && block.text && block.text.trim()) {
|
|
359
|
-
|
|
360
|
-
addUnique(turn.assistantMessages, block.text);
|
|
361
|
-
addConversation(turn, 'Assistente', block.text);
|
|
367
|
+
addAssistantMessage(result, turn, block.text);
|
|
362
368
|
} else if (block?.type === 'tool_use') {
|
|
363
369
|
const name = block.name || 'tool_use';
|
|
364
370
|
addUnique(result.tools, name);
|
package/src/cost.mjs
CHANGED
|
@@ -196,19 +196,53 @@ function opt(argv, name) {
|
|
|
196
196
|
return eq ? eq.slice(name.length + 1) : undefined;
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
const REBUILD_LIMIT_FLAGS = [
|
|
200
|
+
['--max-graph-nodes', 'maxGraphNodes'],
|
|
201
|
+
['--max-fallback-days', 'maxFallbackDays'],
|
|
202
|
+
['--max-fallback-candidates', 'maxFallbackCandidates'],
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
export function parseRebuildOptions(argv = []) {
|
|
206
|
+
const session = opt(argv, '--session') || '';
|
|
207
|
+
const overrides = {};
|
|
208
|
+
for (const [flag, key] of REBUILD_LIMIT_FLAGS) {
|
|
209
|
+
const present = argv.includes(flag) || argv.some((arg) => arg.startsWith(`${flag}=`));
|
|
210
|
+
if (!present) continue;
|
|
211
|
+
const value = Number(opt(argv, flag));
|
|
212
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
213
|
+
return { ok: false, exitCode: 2, code: 'INVALID_LIMIT_OVERRIDE' };
|
|
214
|
+
}
|
|
215
|
+
overrides[key] = value;
|
|
216
|
+
}
|
|
217
|
+
if (Object.keys(overrides).length > 0 && !session) {
|
|
218
|
+
return { ok: false, exitCode: 2, code: 'TARGET_REQUIRED_FOR_LIMIT_OVERRIDE' };
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
ok: true,
|
|
222
|
+
options: {
|
|
223
|
+
apply: argv.includes('--apply'),
|
|
224
|
+
session,
|
|
225
|
+
limit: Number(opt(argv, '--limit')) || 0,
|
|
226
|
+
limits: { ...overrides },
|
|
227
|
+
overrides: { ...overrides },
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
199
232
|
export function runCost(argv) {
|
|
200
233
|
const vaultRaw = opt(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
|
|
201
234
|
if (!vaultRaw) { process.stderr.write('wendkeep cost: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
|
|
202
235
|
const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
|
|
203
236
|
if (!existsSync(vaultBase)) { process.stderr.write(`wendkeep cost: vault not found: ${vaultBase}\n`); process.exit(2); }
|
|
204
237
|
if (argv[0] === 'rebuild') {
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
238
|
+
const parsed = parseRebuildOptions(argv);
|
|
239
|
+
if (!parsed.ok) {
|
|
240
|
+
process.stderr.write(`wendkeep cost rebuild: ${parsed.code}\n`);
|
|
241
|
+
process.exit(parsed.exitCode);
|
|
242
|
+
}
|
|
243
|
+
const report = rebuildSessionCosts(vaultBase, parsed.options);
|
|
210
244
|
if (argv.includes('--json')) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
211
|
-
else process.stdout.write(`cost rebuild (${report.mode}): ${report.scanned} lidas · ${report.changed} alteradas · ${report.unchanged} iguais · ${report.
|
|
245
|
+
else process.stdout.write(`cost rebuild (${report.mode}): ${report.scanned} lidas · ${report.changed} alteradas · ${report.unchanged} iguais · ${report.degraded} degradadas · ${report.stale} stale · ${report.missing} sem fonte · ${report.errors} erros\n${report.mode === 'apply' ? 'Relatório: .brain/COST_REBUILD.json\n' : 'Nenhum arquivo foi alterado; use --apply para gravar.\n'}`);
|
|
212
246
|
process.exit(report.ok ? 0 : 1);
|
|
213
247
|
}
|
|
214
248
|
const agg = collectVaultCost(vaultBase, { since: opt(argv, '--since') });
|
package/src/doctor.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
5
|
import { dirname, join, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import { checkHarness, checkVaultLinks, checkSessionActivity, checkStackedFrontmatter, renderStackedFrontmatterLines, checkUnpricedModels, renderUnpricedModelLines, checkStaleDerivedSections, renderStaleDerivedSectionLines } from '../hooks/harness-doctor.mjs';
|
|
7
|
+
import { checkHarness, checkVaultLinks, checkSessionActivity, checkStackedFrontmatter, renderStackedFrontmatterLines, checkUnpricedModels, renderUnpricedModelLines, checkStaleDerivedSections, renderStaleDerivedSectionLines, checkSessionObservability, renderSessionObservabilityLines } from '../hooks/harness-doctor.mjs';
|
|
8
8
|
import { checkSyncDefs } from './sync-defs.mjs';
|
|
9
9
|
import { resolveProjectVault } from './project-vault.mjs';
|
|
10
10
|
|
|
@@ -79,6 +79,9 @@ export function runDoctor(argv) {
|
|
|
79
79
|
// 3d. Seções derivadas do corpo que ficaram para trás do Encerramento (notas pré-0.53.0).
|
|
80
80
|
process.stdout.write(`\n${renderStaleDerivedSectionLines(checkStaleDerivedSections(vaultBase)).join('\n')}\n`);
|
|
81
81
|
|
|
82
|
+
// 3e. Observabilidade materializada: schema vigente não basta sem frontier + manifest frescos.
|
|
83
|
+
process.stdout.write(`\n${renderSessionObservabilityLines(checkSessionObservability(vaultBase)).join('\n')}\n`);
|
|
84
|
+
|
|
82
85
|
// 4. Sessão: não mente "inativa" quando há atividade recente (workflow/subagente em background).
|
|
83
86
|
const act = checkSessionActivity(vaultBase);
|
|
84
87
|
if (act.lastSession) {
|
package/src/profile.mjs
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
import { mutateSessionRegistry, readSessionRegistry } from '../hooks/obsidian-common.mjs';
|
|
4
4
|
import {
|
|
5
5
|
DEFAULT_OPERATING_PROFILE,
|
|
6
|
+
evaluateTaskOperatingProfileLease,
|
|
6
7
|
normalizeOperatingProfile,
|
|
7
8
|
resolveOperatingProfile,
|
|
8
9
|
setOperatingProfile,
|
|
9
10
|
} from './operating-profile.mjs';
|
|
11
|
+
import { setSessionTaskOperatingProfile } from '../hooks/operating-profile-task-store.mjs';
|
|
10
12
|
import { resolve } from 'node:path';
|
|
11
13
|
import { findProjectBinding, resolveProjectVault, updateProjectBinding } from './project-vault.mjs';
|
|
12
14
|
|
|
@@ -14,12 +16,14 @@ export const PROFILE_HELP = `wendkeep profile <subcommand>
|
|
|
14
16
|
|
|
15
17
|
status [--session <id>]
|
|
16
18
|
use <OFF|FLOW|GUIDE|GOVERN|ASSURE> [--session <id>]
|
|
19
|
+
route <FLOW|GUIDE|GOVERN|ASSURE> --session <id> --reason <text>
|
|
17
20
|
|
|
18
|
-
Common options: --project <path> --vault <path> --session <id> --json
|
|
21
|
+
Common options: --project <path> --vault <path> --session <id> --json --reason <text>
|
|
22
|
+
route creates a task-scoped choice for the current request; it never selects OFF.
|
|
19
23
|
The Keep Core (Vault, session, and memory) remains active under every profile.
|
|
20
24
|
`;
|
|
21
25
|
|
|
22
|
-
const VALUE_OPTIONS = new Set(['--project', '--vault', '--session']);
|
|
26
|
+
const VALUE_OPTIONS = new Set(['--project', '--vault', '--session', '--reason']);
|
|
23
27
|
const FLAG_OPTIONS = new Set(['--json']);
|
|
24
28
|
|
|
25
29
|
function optionValue(argv, name) {
|
|
@@ -32,8 +36,8 @@ function commandArgs(argv) {
|
|
|
32
36
|
const values = [];
|
|
33
37
|
for (let index = 0; index < argv.length; index += 1) {
|
|
34
38
|
const value = argv[index];
|
|
35
|
-
if (['--project', '--vault', '--session'].includes(value)) { index += 1; continue; }
|
|
36
|
-
if (value.startsWith('--project=') || value.startsWith('--vault=') || value.startsWith('--session=')) continue;
|
|
39
|
+
if (['--project', '--vault', '--session', '--reason'].includes(value)) { index += 1; continue; }
|
|
40
|
+
if (value.startsWith('--project=') || value.startsWith('--vault=') || value.startsWith('--session=') || value.startsWith('--reason=')) continue;
|
|
37
41
|
if (value === '--json') continue;
|
|
38
42
|
values.push(value);
|
|
39
43
|
}
|
|
@@ -76,8 +80,15 @@ function canonicalPath(value) {
|
|
|
76
80
|
function output(payload, json) {
|
|
77
81
|
if (json) process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
78
82
|
else {
|
|
79
|
-
const scope = payload.scope === '
|
|
80
|
-
|
|
83
|
+
const scope = payload.scope === 'task'
|
|
84
|
+
? `task ${payload.session_id}`
|
|
85
|
+
: payload.scope === 'session' ? `session ${payload.session_id}` : 'project';
|
|
86
|
+
const details = [scope, payload.source];
|
|
87
|
+
if (payload.session_id && payload.base_profile && payload.task_lease?.state) {
|
|
88
|
+
details.push(`base=${payload.base_profile}/${payload.base_source}`);
|
|
89
|
+
details.push(`lease=${payload.task_lease.state}`);
|
|
90
|
+
}
|
|
91
|
+
process.stdout.write(`${payload.profile} (${details.join('; ')})\n`);
|
|
81
92
|
}
|
|
82
93
|
if (payload.binding_error) {
|
|
83
94
|
const code = payload.binding_error.code || 'WENDKEEP_VAULT_CONFIG_INVALID';
|
|
@@ -129,10 +140,7 @@ export function setSessionOperatingProfile(vaultBase, sessionId, profile, { now
|
|
|
129
140
|
});
|
|
130
141
|
}
|
|
131
142
|
|
|
132
|
-
function
|
|
133
|
-
const sessions = readSessionRegistry(vaultBase).sessions || {};
|
|
134
|
-
if (!Object.hasOwn(sessions, sessionId)) throw new Error(`sessão não encontrada: ${sessionId}`);
|
|
135
|
-
const entry = sessions[sessionId];
|
|
143
|
+
function sessionBaseProfile(entry, projectResolved) {
|
|
136
144
|
if (Object.hasOwn(entry, 'operating_profile')) {
|
|
137
145
|
try {
|
|
138
146
|
return {
|
|
@@ -149,6 +157,44 @@ function sessionProfile(vaultBase, sessionId, projectResolved) {
|
|
|
149
157
|
return { profile: projectResolved.profile, source: projectResolved.source };
|
|
150
158
|
}
|
|
151
159
|
|
|
160
|
+
function sessionProfile(vaultBase, sessionId, projectResolved) {
|
|
161
|
+
const sessions = readSessionRegistry(vaultBase).sessions || {};
|
|
162
|
+
if (!Object.hasOwn(sessions, sessionId)) throw new Error(`sessão não encontrada: ${sessionId}`);
|
|
163
|
+
return sessionBaseProfile(sessions[sessionId], projectResolved);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function sessionProfileStatus(vaultBase, sessionId, projectResolved) {
|
|
167
|
+
const sessions = readSessionRegistry(vaultBase).sessions || {};
|
|
168
|
+
if (!Object.hasOwn(sessions, sessionId)) throw new Error(`sessão não encontrada: ${sessionId}`);
|
|
169
|
+
const entry = sessions[sessionId];
|
|
170
|
+
const base = sessionBaseProfile(entry, projectResolved);
|
|
171
|
+
const taskLease = evaluateTaskOperatingProfileLease(entry.operating_profile_task, {
|
|
172
|
+
sessionId,
|
|
173
|
+
turnId: entry.last_prompt_turn_id || '',
|
|
174
|
+
turnSequence: entry.last_turn_sequence,
|
|
175
|
+
});
|
|
176
|
+
return {
|
|
177
|
+
profile: taskLease.state === 'active' ? taskLease.profile : base.profile,
|
|
178
|
+
source: taskLease.state === 'active' ? 'task-lease' : base.source,
|
|
179
|
+
scope: taskLease.state === 'active' ? 'task' : 'session',
|
|
180
|
+
baseProfile: base.profile,
|
|
181
|
+
baseSource: base.source,
|
|
182
|
+
taskLease,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function sessionOutputPayload(effective, sessionId) {
|
|
187
|
+
return {
|
|
188
|
+
profile: effective.profile,
|
|
189
|
+
source: effective.source,
|
|
190
|
+
scope: effective.scope,
|
|
191
|
+
session_id: sessionId,
|
|
192
|
+
base_profile: effective.baseProfile,
|
|
193
|
+
base_source: effective.baseSource,
|
|
194
|
+
task_lease: effective.taskLease,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
152
198
|
export function runProfile(argv = []) {
|
|
153
199
|
try { validateArgv(argv); }
|
|
154
200
|
catch (error) { return fail(error.message); }
|
|
@@ -156,6 +202,8 @@ export function runProfile(argv = []) {
|
|
|
156
202
|
const sub = args[0] || 'status';
|
|
157
203
|
const json = argv.includes('--json');
|
|
158
204
|
const sessionId = optionValue(argv, '--session') || '';
|
|
205
|
+
const reason = optionValue(argv, '--reason');
|
|
206
|
+
if (sub !== 'route' && reason) return fail('--reason só é aceito por profile route');
|
|
159
207
|
|
|
160
208
|
let state;
|
|
161
209
|
try { state = context(argv); }
|
|
@@ -166,20 +214,50 @@ export function runProfile(argv = []) {
|
|
|
166
214
|
if (args.length > 1) return fail(`${sub} não aceita argumentos posicionais adicionais`);
|
|
167
215
|
try {
|
|
168
216
|
const effective = sessionId
|
|
169
|
-
?
|
|
170
|
-
: { profile: projectResolved.profile, source: projectResolved.source };
|
|
217
|
+
? sessionProfileStatus(state.vaultBase, sessionId, projectResolved)
|
|
218
|
+
: { profile: projectResolved.profile, source: projectResolved.source, scope: 'project' };
|
|
171
219
|
output({
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
220
|
+
...(sessionId ? sessionOutputPayload(effective, sessionId) : {
|
|
221
|
+
profile: effective.profile,
|
|
222
|
+
source: effective.source,
|
|
223
|
+
scope: 'project',
|
|
224
|
+
session_id: null,
|
|
225
|
+
}),
|
|
176
226
|
...(state.resolved.bindingError ? { binding_error: state.resolved.bindingError } : {}),
|
|
177
227
|
}, json);
|
|
178
228
|
return 0;
|
|
179
229
|
} catch (error) { return fail(error.message); }
|
|
180
230
|
}
|
|
181
231
|
|
|
182
|
-
if (sub
|
|
232
|
+
if (sub === 'route') {
|
|
233
|
+
if (args.length !== 2) return fail('route requer exatamente um perfil');
|
|
234
|
+
if (!sessionId) return fail('route requer --session <id>');
|
|
235
|
+
if (!reason) return fail('route requer --reason <text>');
|
|
236
|
+
try {
|
|
237
|
+
const base = sessionProfile(state.vaultBase, sessionId, projectResolved);
|
|
238
|
+
const lease = setSessionTaskOperatingProfile(
|
|
239
|
+
state.vaultBase,
|
|
240
|
+
sessionId,
|
|
241
|
+
args[1],
|
|
242
|
+
{ reason },
|
|
243
|
+
);
|
|
244
|
+
output({
|
|
245
|
+
profile: lease.profile,
|
|
246
|
+
source: 'task-lease',
|
|
247
|
+
scope: 'task',
|
|
248
|
+
session_id: sessionId,
|
|
249
|
+
base_profile: base.profile,
|
|
250
|
+
base_source: base.source,
|
|
251
|
+
task_lease: lease,
|
|
252
|
+
...(state.resolved.bindingError ? { binding_error: state.resolved.bindingError } : {}),
|
|
253
|
+
}, json);
|
|
254
|
+
return 0;
|
|
255
|
+
} catch (error) { return fail(error.message); }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (sub !== 'use' && sub !== 'set') {
|
|
259
|
+
return fail('use status | use <OFF|FLOW|GUIDE|GOVERN|ASSURE> | route <FLOW|GUIDE|GOVERN|ASSURE>');
|
|
260
|
+
}
|
|
183
261
|
if (args.length !== 2) return fail(`${sub} requer exatamente um perfil`);
|
|
184
262
|
let profile;
|
|
185
263
|
try { profile = normalizeOperatingProfile(args[1], { strict: true }); }
|