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.
Files changed (40) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.en.md +78 -5
  3. package/README.md +78 -5
  4. package/docs/en/commands/costs-and-observability.md +21 -7
  5. package/docs/en/commands/maintenance-and-diagnostics.md +13 -1
  6. package/docs/en/commands/operating-profiles.md +65 -10
  7. package/docs/en/commands/sessions-and-import.md +22 -1
  8. package/docs/en/commands/verify.md +5 -3
  9. package/docs/pt-BR/commands/costs-and-observability.md +21 -7
  10. package/docs/pt-BR/commands/maintenance-and-diagnostics.md +12 -1
  11. package/docs/pt-BR/commands/operating-profiles.md +66 -11
  12. package/docs/pt-BR/commands/sessions-and-import.md +20 -0
  13. package/docs/pt-BR/commands/verify.md +6 -3
  14. package/hooks/change-nag.mjs +8 -0
  15. package/hooks/codex-rollout-meta.mjs +112 -0
  16. package/hooks/codex-subagent-graph.mjs +903 -0
  17. package/hooks/harness-doctor.mjs +82 -1
  18. package/hooks/import-sessions.mjs +185 -50
  19. package/hooks/operating-profile-runtime.mjs +36 -2
  20. package/hooks/operating-profile-task-store.mjs +77 -0
  21. package/hooks/session-identity.mjs +40 -5
  22. package/hooks/session-observability-lifecycle.mjs +129 -0
  23. package/hooks/session-observability-state.mjs +241 -0
  24. package/hooks/session-observability-store.mjs +436 -0
  25. package/hooks/session-observability.mjs +647 -21
  26. package/hooks/session-stop.mjs +339 -11
  27. package/hooks/subagent-stop.mjs +266 -12
  28. package/hooks/subagent-usage.mjs +65 -0
  29. package/hooks/token-usage.mjs +81 -4
  30. package/package.json +3 -3
  31. package/packages/harness/src/operating-profile.mjs +127 -0
  32. package/packages/harness/src/sensors-core.mjs +41 -1
  33. package/packages/integrations/src/prompt-content.mjs +123 -0
  34. package/packages/integrations/src/transcripts.mjs +16 -10
  35. package/src/cost.mjs +40 -6
  36. package/src/doctor.mjs +4 -1
  37. package/src/profile.mjs +95 -17
  38. package/src/rebuild-costs.mjs +220 -34
  39. package/src/skills-seed.mjs +38 -2
  40. package/src/sync-defs.mjs +6 -1
@@ -1,45 +1,231 @@
1
- // Deterministic cost reconstruction for historical sessions.
2
- // Registry is authoritative: session_file <-> transcript_path. Dry-run restores every note.
3
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
4
- import { join } from 'node:path';
1
+ // Deterministic, causal reconstruction for historical session observability.
2
+ // Dry-run is a pure composition pass; apply delegates all note mutation to the CAS publisher.
3
+ import { createHash } from 'node:crypto';
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
5
+ import { dirname, join } from 'node:path';
5
6
  import { readSessionRegistry } from '../hooks/obsidian-common.mjs';
6
- import { updateSessionObservability } from '../hooks/session-observability.mjs';
7
+ import * as sessionObservability from '../hooks/session-observability.mjs';
8
+ import {
9
+ mutateObservabilityStore,
10
+ readObservabilityStore,
11
+ } from '../hooks/session-observability-store.mjs';
12
+ import { sanitizeObservabilityDiagnostics } from '../hooks/session-observability-state.mjs';
7
13
  import { assertVaultPathSafe } from '../hooks/vault-path-safety.mjs';
8
14
 
9
- export function rebuildSessionCosts(vaultBase, { apply = false, session = '', limit = 0 } = {}) {
10
- const registry = readSessionRegistry(vaultBase);
11
- const report = { version: 1, generatedAt: new Date().toISOString(), mode: apply ? 'apply' : 'dry-run', scanned: 0, changed: 0, unchanged: 0, missing: [], errors: [], sessions: [] };
12
- const entries = Object.entries(registry.sessions || {}).map(([sessionId, value]) => ({ sessionId, ...value }))
13
- .filter((e) => e.session_file)
14
- .filter((e) => !session || e.sessionId === session || e.session_file === session);
15
- for (const entry of entries) {
15
+ function sortedEntries(registry, target) {
16
+ return Object.entries(registry?.sessions || {})
17
+ .map(([sessionId, value]) => ({ sessionId, ...value }))
18
+ .filter((entry) => entry.session_file)
19
+ .filter((entry) => !target || entry.sessionId === target || entry.session_file === target)
20
+ .sort((a, b) => a.sessionId.localeCompare(b.sessionId));
21
+ }
22
+
23
+ function transcriptCandidates(entry) {
24
+ const paths = new Set();
25
+ if (entry.transcript_path) paths.add(entry.transcript_path);
26
+ for (const path of entry.transcript_paths || []) if (path) paths.add(path);
27
+ const activations = Array.isArray(entry.activations)
28
+ ? entry.activations
29
+ : Object.values(entry.activations || {});
30
+ for (const activation of activations) {
31
+ if (activation?.transcript_path) paths.add(activation.transcript_path);
32
+ for (const path of activation?.transcript_paths || []) if (path) paths.add(path);
33
+ }
34
+ return [...paths];
35
+ }
36
+
37
+ function candidateContent(candidate, fallback) {
38
+ return typeof candidate?.content === 'string' ? candidate.content : fallback;
39
+ }
40
+
41
+ function candidateHash(candidate, fallback) {
42
+ return createHash('sha256').update(candidateContent(candidate, fallback)).digest('hex');
43
+ }
44
+
45
+ function safeDiagnostics(input, fallback = []) {
46
+ try {
47
+ return sanitizeObservabilityDiagnostics(input || fallback);
48
+ } catch {
49
+ return sanitizeObservabilityDiagnostics(fallback);
50
+ }
51
+ }
52
+
53
+ export function semanticRebuildReport(report) {
54
+ const {
55
+ generatedAt: _generatedAt,
56
+ changed = 0,
57
+ unchanged = 0,
58
+ sessions = [],
59
+ ...semantic
60
+ } = report || {};
61
+ return {
62
+ ...semantic,
63
+ converged: Number(changed || 0) + Number(unchanged || 0),
64
+ sessions: sessions.map((entry) => ({
65
+ ...entry,
66
+ status: entry.status === 'published' || entry.status === 'unchanged'
67
+ ? 'converged'
68
+ : entry.status,
69
+ })),
70
+ };
71
+ }
72
+
73
+ export function writeRebuildReportIfChanged(reportPath, report) {
74
+ if (existsSync(reportPath)) {
75
+ try {
76
+ const previous = JSON.parse(readFileSync(reportPath, 'utf8'));
77
+ if (JSON.stringify(semanticRebuildReport(previous))
78
+ === JSON.stringify(semanticRebuildReport(report))) return false;
79
+ } catch {
80
+ // Invalid prior reports are replaced by the sanitized current schema.
81
+ }
82
+ }
83
+ mkdirSync(dirname(reportPath), { recursive: true });
84
+ writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
85
+ return true;
86
+ }
87
+
88
+ function markDirtyDefault(vaultBase, sessionId, diagnostics) {
89
+ mutateObservabilityStore(vaultBase, sessionId, (state) => ({
90
+ ...state,
91
+ observability_dirty: true,
92
+ diagnostics: safeDiagnostics(diagnostics, [{ code: 'STALE_FRONTIER', count: 1 }]),
93
+ }));
94
+ }
95
+
96
+ export function rebuildSessionCosts(
97
+ vaultBase,
98
+ {
99
+ apply = false,
100
+ session = '',
101
+ limit = 0,
102
+ limits = {},
103
+ overrides = {},
104
+ } = {},
105
+ effects = {},
106
+ ) {
107
+ const readRegistry = effects.readRegistry || readSessionRegistry;
108
+ const compose = effects.compose || sessionObservability.composeSessionObservability;
109
+ const publish = effects.publish || sessionObservability.publishSessionObservability;
110
+ const readStore = effects.readStore || readObservabilityStore;
111
+ const markDirty = effects.markDirty || markDirtyDefault;
112
+ const writeReport = effects.writeReport || writeRebuildReportIfChanged;
113
+ const now = effects.now || (() => new Date().toISOString());
114
+ if (typeof compose !== 'function') throw new TypeError('composeSessionObservability indisponível');
115
+ if (apply && typeof publish !== 'function') throw new TypeError('publishSessionObservability indisponível');
116
+
117
+ const registry = readRegistry(vaultBase);
118
+ const report = {
119
+ version: 2,
120
+ generatedAt: now(),
121
+ mode: apply ? 'apply' : 'dry-run',
122
+ targeted: Boolean(session),
123
+ overrides: { ...overrides },
124
+ scanned: 0,
125
+ changed: 0,
126
+ unchanged: 0,
127
+ degraded: 0,
128
+ stale: 0,
129
+ missing: 0,
130
+ errors: 0,
131
+ ok: true,
132
+ sessions: [],
133
+ };
134
+
135
+ for (const entry of sortedEntries(registry, session)) {
16
136
  if (limit && report.scanned >= limit) break;
17
137
  report.scanned += 1;
18
- const checkedNote = assertVaultPathSafe(vaultBase, join(vaultBase, entry.session_file), {
19
- expectedType: 'file', label: 'nota de sessão do rebuild de custos',
20
- });
21
- const note = checkedNote.target;
22
- if (!entry.transcript_path || !checkedNote.exists || !existsSync(entry.transcript_path)) {
23
- report.missing.push({ sessionId: entry.sessionId, session: entry.session_file, note: checkedNote.exists, transcript: !!entry.transcript_path && existsSync(entry.transcript_path), transcriptPath: entry.transcript_path || '' });
24
- continue;
25
- }
26
- const before = readFileSync(note, 'utf8');
138
+ let note;
27
139
  try {
28
- updateSessionObservability({
29
- vaultBase, sessionPath: note, transcriptPath: entry.transcript_path,
30
- caller: 'cost-rebuild', canonicalConversationId: entry.sessionId,
140
+ const checked = assertVaultPathSafe(vaultBase, join(vaultBase, entry.session_file), {
141
+ expectedType: 'file', label: 'nota de sessão do rebuild de custos',
31
142
  });
32
- const after = readFileSync(note, 'utf8');
33
- const changed = before !== after;
34
- if (changed) report.changed += 1; else report.unchanged += 1;
35
- report.sessions.push({ sessionId: entry.sessionId, session: entry.session_file, transcript: entry.transcript_path, changed });
36
- if (!apply && changed) writeFileSync(note, before, 'utf8');
37
- } catch (error) {
38
- if (!apply) writeFileSync(note, before, 'utf8');
39
- report.errors.push({ sessionId: entry.sessionId, session: entry.session_file, error: error.message });
143
+ const hasTranscript = transcriptCandidates(entry).some((path) => existsSync(path));
144
+ if (!checked.exists || !hasTranscript) {
145
+ report.missing += 1;
146
+ report.sessions.push({
147
+ sessionId: entry.sessionId, status: 'missing', diagnostics: [],
148
+ });
149
+ continue;
150
+ }
151
+ note = checked.target;
152
+ const before = readFileSync(note, 'utf8');
153
+ const runtimeState = readStore(vaultBase, entry.sessionId);
154
+ const candidate = compose({
155
+ vaultBase,
156
+ sessionContent: before,
157
+ sessionEntry: entry,
158
+ canonicalConversationId: entry.sessionId,
159
+ caller: 'cost-rebuild',
160
+ mode: 'offline',
161
+ limits,
162
+ runtimeState,
163
+ });
164
+ const diagnostics = safeDiagnostics(candidate?.diagnostics);
165
+ const contentHash = candidateHash(candidate, before);
166
+ if (candidate?.state === 'degraded') {
167
+ report.degraded += 1;
168
+ report.sessions.push({ sessionId: entry.sessionId, status: 'degraded', diagnostics });
169
+ if (apply) markDirty(vaultBase, entry.sessionId, diagnostics);
170
+ continue;
171
+ }
172
+ if (candidate?.state !== 'complete' && candidate?.state !== 'none') {
173
+ report.degraded += 1;
174
+ const invalidDiagnostics = [{ code: 'PARENT_META_INVALID', count: 1 }];
175
+ report.sessions.push({
176
+ sessionId: entry.sessionId, status: 'degraded', diagnostics: invalidDiagnostics,
177
+ });
178
+ if (apply) markDirty(vaultBase, entry.sessionId, invalidDiagnostics);
179
+ continue;
180
+ }
181
+
182
+ if (!apply) {
183
+ const changed = candidateContent(candidate, before) !== before;
184
+ if (changed) report.changed += 1;
185
+ else report.unchanged += 1;
186
+ report.sessions.push({
187
+ sessionId: entry.sessionId,
188
+ status: changed ? 'would-change' : 'unchanged',
189
+ candidateHash: contentHash,
190
+ diagnostics,
191
+ });
192
+ continue;
193
+ }
194
+
195
+ const outcome = publish({
196
+ vaultBase,
197
+ sessionPath: note,
198
+ canonicalConversationId: entry.sessionId,
199
+ candidate,
200
+ caller: 'cost-rebuild',
201
+ mode: 'offline',
202
+ allowSourceRefresh: true,
203
+ }) || { status: 'degraded' };
204
+ if (outcome.status === 'published') report.changed += 1;
205
+ else if (outcome.status === 'unchanged') report.unchanged += 1;
206
+ else if (outcome.status === 'stale' || outcome.status === 'conflict') {
207
+ report.stale += 1;
208
+ markDirty(vaultBase, entry.sessionId, [{ code: 'STALE_FRONTIER', count: 1 }]);
209
+ } else {
210
+ report.degraded += 1;
211
+ markDirty(vaultBase, entry.sessionId, [{ code: 'PARENT_META_INVALID', count: 1 }]);
212
+ }
213
+ report.sessions.push({
214
+ sessionId: entry.sessionId,
215
+ status: outcome.status || 'degraded',
216
+ candidateHash: contentHash,
217
+ diagnostics: safeDiagnostics(outcome.diagnostics, diagnostics),
218
+ });
219
+ } catch {
220
+ report.errors += 1;
221
+ const diagnostics = [{ code: 'PARENT_META_INVALID', count: 1 }];
222
+ report.sessions.push({ sessionId: entry.sessionId, status: 'degraded', diagnostics });
223
+ if (apply) markDirty(vaultBase, entry.sessionId, diagnostics);
40
224
  }
41
225
  }
42
- report.ok = report.errors.length === 0 && report.missing.length === 0;
43
- if (apply) writeFileSync(join(vaultBase, '.brain', 'COST_REBUILD.json'), `${JSON.stringify(report, null, 2)}\n`, 'utf8');
226
+
227
+ report.ok = report.degraded === 0 && report.stale === 0
228
+ && report.missing === 0 && report.errors === 0;
229
+ if (apply) writeReport(join(vaultBase, '.brain', 'COST_REBUILD.json'), report);
44
230
  return report;
45
231
  }
@@ -17,6 +17,24 @@ Use ao começar implementação, correção ou refatoração. **Keep Core perman
17
17
  em todos os perfis: Vault, sessão, identidade, memória, lessons e persistência. Na ausência de
18
18
  configuração válida, **GOVERN é o padrão** compatível.
19
19
 
20
+ ## Seleção temporária por solicitação
21
+
22
+ Antes de editar, classifique a implementação atual e registre a escolha auditável com
23
+ \`wendkeep profile route <FLOW|GUIDE|GOVERN|ASSURE> --session <id> --reason <texto>\`.
24
+ A lease vale somente para a solicitação atual; ao encerrá-la, o perfil persistente da
25
+ sessão/projeto volta a valer. **OFF nunca é uma escolha automática da LLM**: somente uma pessoa
26
+ pode persistir \`profile use OFF\` explicitamente.
27
+
28
+ - **FLOW:** ajuste local, reversível e de escopo fechado, sem contrato/spec, segurança,
29
+ dependência, CI/release ou policy.
30
+ - **GUIDE:** mudança compacta de comportamento que precisa de change/spec, sem revisão formal.
31
+ - **GOVERN:** escolha conservadora em caso de dúvida ou risco e para superfícies sensíveis.
32
+ - **ASSURE:** GOVERN quando confirmação explícita e handoff fazem parte do contrato.
33
+
34
+ O harness da LLM faz essa classificação semântica; o Wend Runtime valida e aplica a lease.
35
+ Se não houver uma sessão causal identificada ou o comando falhar, não fabrique estado: use o
36
+ perfil efetivo já injetado e trate \`GOVERN\` como fallback conservador quando ele for o padrão.
37
+
20
38
  <HARD-GATE>
21
39
  Antes de editar, leia o **perfil efetivo** injetado pelo WendKeep e siga somente sua rota:
22
40
  - \`OFF\`: não imponha processo Wend; a governança pertence ao **harness nativo da LLM**.
@@ -268,6 +286,24 @@ Use this when starting an implementation, fix, or refactor. **Keep Core is alway
268
286
  in every profile: Vault, session, identity, memory, lessons, and persistence. With no valid
269
287
  configuration, **GOVERN is the default** for compatibility.
270
288
 
289
+ ## Temporary selection per request
290
+
291
+ Before editing, classify the current implementation and record the auditable choice with
292
+ \`wendkeep profile route <FLOW|GUIDE|GOVERN|ASSURE> --session <id> --reason <text>\`.
293
+ The lease applies only to the current request; after it ends, the persistent session/project
294
+ profile becomes effective again. **OFF is never an automatic LLM choice**: only a human can
295
+ persist it explicitly through \`profile use OFF\`.
296
+
297
+ - **FLOW:** a local, reversible, bounded adjustment with no contract/spec, security, dependency,
298
+ CI/release, or policy impact.
299
+ - **GUIDE:** a compact behavior change that needs a change/spec but no formal review.
300
+ - **GOVERN:** the conservative choice when uncertain or risky, and for sensitive surfaces.
301
+ - **ASSURE:** GOVERN when explicit confirmation and handoff are part of the contract.
302
+
303
+ The LLM harness owns semantic classification; Wend Runtime validates and applies the lease. If
304
+ there is no causally identified session or the command fails, do not fabricate state: use the
305
+ already injected effective profile, with \`GOVERN\` as the conservative configured fallback.
306
+
271
307
  <HARD-GATE>
272
308
  Before editing, read the injected **effective profile** and follow only its route:
273
309
  - \`OFF\`: impose no Wend process; governance belongs to the **native LLM harness**.
@@ -619,7 +655,7 @@ size of the problem>
619
655
  // usuário ("implementa X", "corrige Y"), não com abstrações ("mudança não-trivial"). Gatilhos
620
656
  // concretos + instrução imperativa = a skill dispara sozinha (paridade Superpowers).
621
657
  const WK_SKILLS_PT = [
622
- skill('wk-workflow', 'Use quando o usuário pedir para implementar, criar, corrigir, refatorar, adicionar ou alterar código: leia o perfil efetivo e roteie OFF/FLOW/GUIDE/GOVERN/ASSURE ANTES de editar. Keep Core permanece ativo; GOVERN é o padrão compatível.', WORKFLOW),
658
+ skill('wk-workflow', 'Use quando o usuário pedir para implementar, criar, corrigir, refatorar, adicionar ou alterar código: classifique e registre a rota temporária FLOW/GUIDE/GOVERN/ASSURE ANTES de editar, então siga o perfil efetivo. Keep Core permanece ativo; OFF nunca é automático.', WORKFLOW),
623
659
  skill('wk-tdd', 'Use ao implementar qualquer comportamento — Red/Green/Refactor com testes que discriminam (derivados do spec, litmus não-raso, adequação).', TDD),
624
660
  skill('wk-debugging', 'Use quando algo falha, quebra, dá erro ou regride — depuração sistemática por hipótese antes de corrigir.', DEBUGGING),
625
661
  skill('wk-brainstorming', 'Use quando a ideia ainda é vaga ou o usuário quer discutir/planejar uma feature (inclusive em plan mode) — vira design aprovado, com closure gate e tabela out-of-scope, antes de código.', BRAINSTORMING, [{ name: 'design-template.md', content: DESIGN_TEMPLATE_PT }]),
@@ -628,7 +664,7 @@ const WK_SKILLS_PT = [
628
664
  ];
629
665
 
630
666
  const WK_SKILLS_EN = [
631
- skill('wk-workflow', 'Use when the user asks to implement, create, fix, refactor, add, or change code: read the effective profile and route OFF/FLOW/GUIDE/GOVERN/ASSURE BEFORE editing. Keep Core stays active; GOVERN is the compatible default.', WORKFLOW_EN),
667
+ skill('wk-workflow', 'Use when the user asks to implement, create, fix, refactor, add, or change code: classify and record the temporary FLOW/GUIDE/GOVERN/ASSURE route BEFORE editing, then follow the effective profile. Keep Core stays active; OFF is never automatic.', WORKFLOW_EN),
632
668
  skill('wk-tdd', 'Use when implementing any behaviour — Red/Green/Refactor with tests that discriminate (spec-derived, non-shallow litmus, adequacy).', TDD_EN),
633
669
  skill('wk-debugging', 'Use when something fails, breaks, errors or regresses — systematic hypothesis-driven debugging before fixing.', DEBUGGING_EN),
634
670
  skill('wk-brainstorming', 'Use when the idea is still vague or the user wants to discuss/plan a feature (plan mode included) — turns it into an approved design, with a closure gate and out-of-scope table, before code.', BRAINSTORMING_EN, [{ name: 'design-template.md', content: DESIGN_TEMPLATE_EN }]),
package/src/sync-defs.mjs CHANGED
@@ -62,7 +62,12 @@ function renderAgentsSection(skills, sourceHash = '') {
62
62
 
63
63
  This project uses the [wendkeep](https://github.com/rogersialves/wendkeep) harness. **Keep Core is always active**
64
64
  in every profile: Vault, session, identity, memory, lessons, and persistence integrations.
65
- The effective profile is selected explicitly; missing or invalid configuration uses **GOVERN as the default**.
65
+ The persistent profile is selected explicitly; missing or invalid configuration uses **GOVERN as the default**.
66
+
67
+ Before an implementation, the native LLM harness classifies the current request and may create a
68
+ task-scoped lease with \`wendkeep profile route <FLOW|GUIDE|GOVERN|ASSURE> --session <id> --reason <text>\`.
69
+ The lease expires when that request ends and the persistent session/project profile becomes
70
+ effective again. **OFF is never selected adaptively**; only a human can persist \`profile use OFF\`.
66
71
 
67
72
  Route work by the effective profile:
68
73
  - **OFF** — Wend Runtime is disabled and governance belongs to the native LLM harness; Keep Core stays active.