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
@@ -8,29 +8,283 @@
8
8
  import { existsSync } from 'fs';
9
9
  import { join } from 'path';
10
10
  import { pathToFileURL } from 'url';
11
- import { readHookInput, writeHookOutput, getVaultBase, providerMeta } from './obsidian-common.mjs';
12
- import { updateSessionObservability } from './session-observability.mjs';
11
+ import {
12
+ getVaultBase,
13
+ mutateSessionRegistry,
14
+ providerMeta,
15
+ readHookInput,
16
+ writeHookOutput,
17
+ } from './obsidian-common.mjs';
18
+ import { materializeSessionObservability } from './session-observability.mjs';
13
19
  import { resolveSessionEntry } from './session-identity.mjs';
20
+ import { readCodexRolloutMeta } from './codex-rollout-meta.mjs';
21
+ import { resolveObservabilityRoots } from './session-observability-lifecycle.mjs';
22
+ import {
23
+ readObservabilityStore,
24
+ markObservabilityCheckpoint,
25
+ recordObservabilitySignal,
26
+ releaseObservabilityLease,
27
+ tryAcquireObservabilityLease,
28
+ } from './session-observability-store.mjs';
14
29
 
15
- export function refreshSubagents(vaultBase, input) {
16
- const { identity, entry } = resolveSessionEntry(vaultBase, input, providerMeta(input.provider).id);
30
+ const SUBAGENT_COALESCE_MS = 250;
31
+ const SUBAGENT_DEADLINE_MS = 15_000;
32
+
33
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
34
+
35
+ function causalSnapshot(entry) {
36
+ const activationId = String(entry?.active_activation_id || '');
37
+ const activation = entry?.activations?.[activationId] || {};
38
+ return {
39
+ activationId,
40
+ activationEpoch: Number(activation.epoch || entry?.activation_epoch || 0),
41
+ turnSequence: Number(entry?.last_turn_sequence || activation.last_turn_sequence || 0),
42
+ };
43
+ }
44
+
45
+ function sameCausalSnapshot(left, right) {
46
+ return left.activationId === right.activationId
47
+ && left.activationEpoch === right.activationEpoch
48
+ && left.turnSequence === right.turnSequence;
49
+ }
50
+
51
+ function defaultMaterialize(request) {
52
+ return materializeSessionObservability({
53
+ vaultBase: request.vaultBase,
54
+ sessionPath: request.sessionPath,
55
+ transcriptPath: request.transcriptPath,
56
+ entry: request.entry,
57
+ canonicalConversationId: request.canonicalConversationId,
58
+ frontier: request.frontier,
59
+ signals: request.signals,
60
+ cache: request.cache,
61
+ mode: 'live',
62
+ deadlineAt: request.deadlineAt,
63
+ now: request.now,
64
+ allowNone: request.allowNone,
65
+ readRuntimeFrontier: request.readRuntimeFrontier,
66
+ withPublicationGuard: request.withPublicationGuard,
67
+ writeRegistryCheckpoint: request.writeRegistryCheckpoint,
68
+ });
69
+ }
70
+
71
+ function claudeRoots(entry) {
72
+ const paths = new Set();
73
+ const add = (value) => {
74
+ if (typeof value === 'string' && value.trim()) paths.add(value.trim());
75
+ };
76
+ add(entry?.transcript_path);
77
+ for (const path of entry?.transcript_paths || []) add(path);
78
+ for (const activation of Object.values(entry?.activations || {})) {
79
+ add(activation?.transcript_path);
80
+ for (const path of activation?.transcript_paths || []) add(path);
81
+ }
82
+ return { state: 'complete', rootPaths: [...paths], descendantPaths: [], diagnostics: [] };
83
+ }
84
+
85
+ export async function refreshSubagents(vaultBase, input, {
86
+ now = Date.now,
87
+ hookStartedAt = now(),
88
+ sleep = wait,
89
+ coalesceMs = SUBAGENT_COALESCE_MS,
90
+ deadlineMs = SUBAGENT_DEADLINE_MS,
91
+ resolveEntry = resolveSessionEntry,
92
+ readMeta = readCodexRolloutMeta,
93
+ mutateRegistry = mutateSessionRegistry,
94
+ recordSignal = recordObservabilitySignal,
95
+ readStore = readObservabilityStore,
96
+ acquireLease = tryAcquireObservabilityLease,
97
+ releaseLease = releaseObservabilityLease,
98
+ resolveRoots = resolveObservabilityRoots,
99
+ materialize = defaultMaterialize,
100
+ } = {}) {
101
+ const deadlineAt = hookStartedAt + deadlineMs;
102
+ const provider = providerMeta(input.provider).id;
103
+ const { identity, entry } = resolveEntry(vaultBase, input, provider);
17
104
  if (identity.state !== 'resolved') return false;
18
- const transcriptPath = identity.transcriptPath;
105
+ const childTranscriptPath = identity.transcriptPath;
19
106
  const sessionRel = entry?.session_file || '';
20
107
  if (!sessionRel) return false;
21
- const sessionPath = join(vaultBase, sessionRel);
22
- if (!existsSync(sessionPath)) return false;
23
- updateSessionObservability({
24
- vaultBase, sessionPath, transcriptPath, caller: 'subagent-stop',
25
- canonicalConversationId: identity.canonicalConversationId,
108
+
109
+ let childParentThreadId = '';
110
+ if (identity.provider === 'codex') {
111
+ const childMeta = readMeta(childTranscriptPath);
112
+ if (!childMeta?.ok || !childMeta.meta?.source?.subagent) return false;
113
+ if (!childMeta.meta.id || childMeta.meta.id !== identity.transcriptId) return false;
114
+ if (childMeta.meta.session_id
115
+ && childMeta.meta.session_id !== identity.canonicalConversationId) return false;
116
+ childParentThreadId = String(
117
+ childMeta.meta.parent_thread_id
118
+ || childMeta.meta.source?.subagent?.thread_spawn?.parent_thread_id
119
+ || '',
120
+ );
121
+ }
122
+
123
+ const observed = causalSnapshot(entry);
124
+ const signal = mutateRegistry(vaultBase, (registry) => {
125
+ const current = registry.sessions?.[identity.canonicalConversationId];
126
+ if (!current || current.session_file !== sessionRel
127
+ || !sameCausalSnapshot(observed, causalSnapshot(current))) return null;
128
+ const recorded = recordSignal(vaultBase, identity.canonicalConversationId, {
129
+ rollout_id: identity.transcriptId,
130
+ transcript_path: childTranscriptPath,
131
+ parent_thread_id: childParentThreadId,
132
+ kind: 'started',
133
+ activation_id: observed.activationId,
134
+ activation_epoch: observed.activationEpoch,
135
+ turn_sequence: observed.turnSequence,
136
+ });
137
+ if (!recorded?.state) return recorded;
138
+ registry.sessions[identity.canonicalConversationId] = {
139
+ ...current,
140
+ observability_signal_sequence: recorded.sequence,
141
+ observability_checkpoint_sequence: Number(
142
+ recorded.state.observability_checkpoint_sequence
143
+ ?? current.observability_checkpoint_sequence
144
+ ?? 0,
145
+ ),
146
+ observability_dirty: Boolean(recorded.state.observability_dirty),
147
+ };
148
+ return recorded;
149
+ });
150
+ if (!signal?.state) return false;
151
+ if (!signal.state.observability_dirty) return true;
152
+
153
+ await sleep(coalesceMs);
154
+ if (now() >= deadlineAt) return true;
155
+
156
+ const latest = readStore(vaultBase, identity.canonicalConversationId);
157
+ if (!latest?.observability_dirty
158
+ || latest.observability_signal_sequence !== signal.sequence) return true;
159
+
160
+ const leaseNow = now();
161
+ if (leaseNow >= deadlineAt) return true;
162
+ const lease = acquireLease(vaultBase, identity.canonicalConversationId, {
163
+ signalSequence: signal.sequence,
164
+ now: leaseNow,
165
+ ttlMs: Math.max(1, deadlineAt - leaseNow),
26
166
  });
27
- return true;
167
+ if (!lease?.acquired) return true;
168
+
169
+ try {
170
+ if (now() >= deadlineAt) return true;
171
+ const fresh = resolveEntry(vaultBase, input, provider);
172
+ if (fresh.identity?.state !== 'resolved'
173
+ || fresh.identity.canonicalConversationId !== identity.canonicalConversationId
174
+ || !fresh.entry?.session_file
175
+ || !sameCausalSnapshot(observed, causalSnapshot(fresh.entry))) return true;
176
+
177
+ const sessionPath = join(vaultBase, fresh.entry.session_file);
178
+ if (!existsSync(sessionPath)) return true;
179
+ const roots = identity.provider === 'codex'
180
+ ? resolveRoots(fresh.entry)
181
+ : claudeRoots(fresh.entry);
182
+ if (roots?.state !== 'complete' || !roots.rootPaths?.length) return true;
183
+
184
+ const runtimeState = lease.state || latest;
185
+ const frontier = {
186
+ canonical_session_id: identity.canonicalConversationId,
187
+ activation_id: observed.activationId || 'legacy',
188
+ activation_epoch: observed.activationEpoch,
189
+ turn_sequence: observed.turnSequence,
190
+ signal_sequence: signal.sequence,
191
+ roots_stat_hash: 'pending',
192
+ graph_cursor: 'pending',
193
+ source_manifest_hash: 'pending',
194
+ };
195
+ const readRuntimeFrontier = (candidateFrontier, guardContext) => {
196
+ const currentRuntime = readStore(vaultBase, identity.canonicalConversationId);
197
+ const currentEntry = guardContext?.entry;
198
+ const currentResolved = currentEntry
199
+ ? { identity: { state: 'resolved', canonicalConversationId: identity.canonicalConversationId }, entry: currentEntry }
200
+ : resolveEntry(vaultBase, input, provider);
201
+ if (currentResolved.identity?.state !== 'resolved'
202
+ || currentResolved.identity.canonicalConversationId !== identity.canonicalConversationId
203
+ || !currentResolved.entry) {
204
+ return { ...candidateFrontier, canonical_session_id: 'unresolved' };
205
+ }
206
+ const currentCausal = causalSnapshot(currentResolved.entry);
207
+ return {
208
+ ...candidateFrontier,
209
+ activation_id: currentCausal.activationId || 'legacy',
210
+ activation_epoch: currentCausal.activationEpoch,
211
+ turn_sequence: currentCausal.turnSequence,
212
+ signal_sequence: Math.max(
213
+ Number(currentRuntime?.observability_signal_sequence || 0),
214
+ Number(currentResolved.entry.observability_signal_sequence || 0),
215
+ ),
216
+ };
217
+ };
218
+ const withPublicationGuard = (_candidateFrontier, publishGuarded) => (
219
+ mutateRegistry(vaultBase, (registry) => publishGuarded({
220
+ registry,
221
+ entry: registry.sessions?.[identity.canonicalConversationId] || null,
222
+ }))
223
+ );
224
+ const writeRegistryCheckpoint = ({
225
+ frontier: checkpointFrontier,
226
+ state,
227
+ diagnostics,
228
+ snapshot,
229
+ }, guardContext) => {
230
+ const registry = guardContext?.registry;
231
+ const current = registry?.sessions?.[identity.canonicalConversationId];
232
+ if (!current) return null;
233
+ const currentSignal = Number(current.observability_signal_sequence || checkpointFrontier.signal_sequence);
234
+ registry.sessions[identity.canonicalConversationId] = {
235
+ ...current,
236
+ observability_signal_sequence: currentSignal,
237
+ observability_checkpoint_sequence: checkpointFrontier.signal_sequence,
238
+ observability_dirty: currentSignal > checkpointFrontier.signal_sequence,
239
+ observability_checkpoint_frontier: checkpointFrontier,
240
+ subagents_observability_state: state,
241
+ subagents_diagnostics: diagnostics || [],
242
+ };
243
+ return markObservabilityCheckpoint(vaultBase, identity.canonicalConversationId, {
244
+ checkpointSequence: checkpointFrontier.signal_sequence,
245
+ frontier: checkpointFrontier,
246
+ sourceManifest: snapshot?.subagents?.sourceManifest,
247
+ graphCache: snapshot?.subagents?.cache,
248
+ diagnostics,
249
+ });
250
+ };
251
+
252
+ await Promise.resolve(materialize({
253
+ vaultBase,
254
+ sessionPath,
255
+ entry: fresh.entry,
256
+ rootPaths: roots.rootPaths,
257
+ transcriptPath: roots.rootPaths[0],
258
+ caller: 'subagent-stop',
259
+ canonicalConversationId: identity.canonicalConversationId,
260
+ activationId: observed.activationId,
261
+ activationEpoch: observed.activationEpoch,
262
+ turnSequence: observed.turnSequence,
263
+ signalSequence: signal.sequence,
264
+ deadlineAt,
265
+ allowNone: false,
266
+ frontier,
267
+ signals: runtimeState.signals || [],
268
+ cache: runtimeState.graph_cache || null,
269
+ now,
270
+ readRuntimeFrontier,
271
+ withPublicationGuard,
272
+ writeRegistryCheckpoint,
273
+ }));
274
+ return true;
275
+ } finally {
276
+ releaseLease(vaultBase, identity.canonicalConversationId, {
277
+ ownerToken: lease.ownerToken,
278
+ signalSequence: signal.sequence,
279
+ });
280
+ }
28
281
  }
29
282
 
30
283
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
31
284
  try {
285
+ const hookStartedAt = Date.now();
32
286
  const input = readHookInput();
33
- refreshSubagents(getVaultBase(input), input);
287
+ await refreshSubagents(getVaultBase(input), input, { hookStartedAt });
34
288
  writeHookOutput({});
35
289
  } catch (error) {
36
290
  process.stderr.write(`[wendkeep] subagent-stop falhou: ${error.message}\n`);
@@ -29,6 +29,49 @@ function walkAgentJsonl(dir) {
29
29
  return out;
30
30
  }
31
31
 
32
+ function inspectAgentJsonlDirectory(dir) {
33
+ let names;
34
+ try {
35
+ names = readdirSync(dir);
36
+ } catch (error) {
37
+ return { state: error?.code === 'ENOENT' ? 'absent' : 'error', files: [] };
38
+ }
39
+ const files = [];
40
+ for (const name of names) {
41
+ const path = join(dir, name);
42
+ let stat;
43
+ try {
44
+ stat = statSync(path);
45
+ } catch {
46
+ return { state: 'error', files: [] };
47
+ }
48
+ if (stat.isDirectory()) {
49
+ const nested = inspectAgentJsonlDirectory(path);
50
+ if (nested.state === 'error' || nested.state === 'absent') return { state: 'error', files: [] };
51
+ files.push(...nested.files);
52
+ } else if (name.startsWith('agent-') && name.endsWith('.jsonl')) {
53
+ files.push(path);
54
+ }
55
+ }
56
+ return { state: 'ok', files };
57
+ }
58
+
59
+ function validJsonl(path) {
60
+ let lines;
61
+ try {
62
+ lines = readFileSync(path, 'utf8').split(/\r?\n/).filter((line) => line.trim());
63
+ } catch {
64
+ return false;
65
+ }
66
+ if (!lines.length) return false;
67
+ try {
68
+ for (const line of lines) JSON.parse(line);
69
+ return true;
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
32
75
  // workflows/scripts/<name>-wf_<rid>.js -> { wf_<rid>: <name> }
33
76
  function workflowNameMap(sessionDir) {
34
77
  const map = {};
@@ -276,6 +319,28 @@ export function collectSubagentUsage(sessionDir) {
276
319
  };
277
320
  }
278
321
 
322
+ // Schema-2 callers need to distinguish proven absence from read/parse failure. Keep the
323
+ // legacy aggregate API above intact while exposing a fail-closed state for observability.
324
+ export function collectClaudeSubagentUsageState(sessionDir) {
325
+ const scan = inspectAgentJsonlDirectory(join(sessionDir, 'subagents'));
326
+ if (scan.state === 'absent' || (scan.state === 'ok' && scan.files.length === 0)) {
327
+ return { state: 'none', diagnostics: [] };
328
+ }
329
+ if (scan.state !== 'ok' || scan.files.some((path) => !validJsonl(path))) {
330
+ return {
331
+ state: 'degraded',
332
+ diagnostics: [{ code: 'CHILD_META_INVALID', count: 1 }],
333
+ };
334
+ }
335
+ const collected = collectSubagentUsage(sessionDir);
336
+ return collected
337
+ ? { ...collected, state: 'complete', diagnostics: [] }
338
+ : {
339
+ state: 'degraded',
340
+ diagnostics: [{ code: 'CHILD_META_INVALID', count: 1 }],
341
+ };
342
+ }
343
+
279
344
  function workflowLine(w) {
280
345
  const parts = [w.runId];
281
346
  if (w.status) parts.push(w.status);
@@ -510,16 +510,19 @@ function detectTranscriptFormat(lines) {
510
510
  return 'codex';
511
511
  }
512
512
 
513
- export function parseTokenUsageFromTranscript(transcriptPath) {
513
+ export function parseTokenUsageFromContent(content, { transcriptPath = '' } = {}) {
514
514
  const result = emptyParseResult(transcriptPath);
515
- if (!transcriptPath || !existsSync(transcriptPath)) return result;
516
-
517
- const lines = readFileSync(transcriptPath, 'utf-8').split('\n').filter(Boolean);
515
+ const lines = String(content || '').split('\n').filter(Boolean);
518
516
  return detectTranscriptFormat(lines) === 'claude'
519
517
  ? parseClaudeLines(lines, result)
520
518
  : parseCodexLines(lines, result);
521
519
  }
522
520
 
521
+ export function parseTokenUsageFromTranscript(transcriptPath) {
522
+ if (!transcriptPath || !existsSync(transcriptPath)) return emptyParseResult(transcriptPath);
523
+ return parseTokenUsageFromContent(readFileSync(transcriptPath, 'utf-8'), { transcriptPath });
524
+ }
525
+
523
526
  function modelCost(usage, model) {
524
527
  const normalized = normalizeModelName(model);
525
528
  const price = PRICE_REFERENCE[normalized];
@@ -935,6 +938,80 @@ export function collectSessionUsage({ sessionContent, transcriptPath }) {
935
938
  };
936
939
  }
937
940
 
941
+ function normalizedTranscriptKey(value) {
942
+ return String(value || '').replace(/\.jsonl?$/i, '').trim().toLowerCase();
943
+ }
944
+
945
+ // Rebuild the main bucket from every validated top-level rollout. Existing history is used
946
+ // only to preserve stable timestamps; an entry that is neither a known root nor a proven
947
+ // descendant makes the reconstruction fail closed instead of silently deleting a legitimate
948
+ // reopening.
949
+ export function collectSessionUsageForRoots({
950
+ sessionContent,
951
+ rootPaths = [],
952
+ descendantIds = [],
953
+ } = {}) {
954
+ const fmMatch = String(sessionContent || '').match(/^---\n([\s\S]*?)\n---/);
955
+ if (!fmMatch) return null;
956
+
957
+ const roots = [...new Set(rootPaths.filter(Boolean))].sort((a, b) => String(a).localeCompare(String(b)));
958
+ const rootKeys = new Set(roots.map((path) => normalizedTranscriptKey(transcriptIdFromPath(path))));
959
+ const descendantKeys = new Set(descendantIds.map(normalizedTranscriptKey).filter(Boolean));
960
+ const existingEntries = parseUsageHistory(fmMatch[1]);
961
+ const unresolved = existingEntries.filter((entry) => {
962
+ const key = normalizedTranscriptKey(entry.transcript_id);
963
+ return key && !rootKeys.has(key) && !descendantKeys.has(key);
964
+ });
965
+ if (unresolved.length) {
966
+ return {
967
+ state: 'degraded',
968
+ diagnostics: [{ code: 'MAIN_TRANSCRIPT_UNRESOLVED', count: unresolved.length }],
969
+ entries: existingEntries,
970
+ content: sessionContent,
971
+ };
972
+ }
973
+
974
+ const entries = [];
975
+ const summaries = [];
976
+ for (const transcriptPath of roots) {
977
+ if (!existsSync(transcriptPath)) {
978
+ return {
979
+ state: 'degraded',
980
+ diagnostics: [{ code: 'MAIN_TRANSCRIPT_UNRESOLVED', count: 1 }],
981
+ entries: existingEntries,
982
+ content: sessionContent,
983
+ };
984
+ }
985
+ const summary = summarizeTokenUsage(parseTokenUsageFromTranscript(transcriptPath));
986
+ if (!summary.calls) {
987
+ return {
988
+ state: 'degraded',
989
+ diagnostics: [{ code: 'MAIN_TRANSCRIPT_UNRESOLVED', count: 1 }],
990
+ entries: existingEntries,
991
+ content: sessionContent,
992
+ };
993
+ }
994
+ const transcriptId = transcriptIdFromPath(transcriptPath);
995
+ const current = entryFromSummary(summary, transcriptId);
996
+ const previous = existingEntries.find((entry) => normalizedTranscriptKey(entry.transcript_id) === normalizedTranscriptKey(transcriptId));
997
+ if (previous && sameUsageData(previous, current)) current.atualizado_em = previous.atualizado_em;
998
+ entries.push(current);
999
+ summaries.push(summary);
1000
+ }
1001
+
1002
+ const agg = aggregateEntries(entries);
1003
+ const content = upsertSessionFrontmatter(sessionContent, agg, entries);
1004
+ if (content === null) return null;
1005
+ return {
1006
+ state: 'complete',
1007
+ diagnostics: [],
1008
+ summaries,
1009
+ aggregate: agg,
1010
+ entries,
1011
+ content,
1012
+ };
1013
+ }
1014
+
938
1015
  export function updateSessionUsage({ vaultBase, sessionRel, sessionPath, transcriptPath, lockTimeoutMs }) {
939
1016
  if (!sessionPath || !existsSync(sessionPath)) return null;
940
1017
  let result = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.66.4",
3
+ "version": "0.67.0",
4
4
  "description": "Vault-first persistent memory for AI coding agents, with an optional profile-aware governance runtime: OFF, FLOW, GUIDE, GOVERN, or ASSURE. Local-first and agent-agnostic (Claude Code, Codex, Cursor…).",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -40,7 +40,7 @@
40
40
  "node": ">=18"
41
41
  },
42
42
  "scripts": {
43
- "check": "node --check bin/wendkeep.mjs && node --check packages/cli/src/index.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/flow-store.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/integrations/src/host-hooks.mjs && node --check packages/integrations/src/hook-envelope.mjs && node --check packages/integrations/src/prompt-content.mjs && node --check packages/integrations/src/transcript-usage.mjs && node --check packages/integrations/src/transcripts.mjs && node --check packages/integrations/src/session-identity.mjs && node --check packages/integrations/src/index.mjs && node --check packages/mcp/src/config.mjs && node --check packages/mcp/src/index.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/locale.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
43
+ "check": "node --check bin/wendkeep.mjs && node --check packages/cli/src/index.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/operating-profile-task-store.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/flow-store.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/integrations/src/host-hooks.mjs && node --check packages/integrations/src/hook-envelope.mjs && node --check packages/integrations/src/prompt-content.mjs && node --check packages/integrations/src/transcript-usage.mjs && node --check packages/integrations/src/transcripts.mjs && node --check packages/integrations/src/session-identity.mjs && node --check packages/integrations/src/index.mjs && node --check packages/mcp/src/config.mjs && node --check packages/mcp/src/index.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/locale.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
44
44
  "test": "node --test --test-concurrency=2",
45
45
  "release": "node scripts/release.mjs",
46
46
  "release:dry": "node scripts/release.mjs --dry-run",
@@ -70,6 +70,6 @@
70
70
  },
71
71
  "devDependencies": {
72
72
  "acorn": "^8.18.0",
73
- "wendkeep": "^0.57.2"
73
+ "wendkeep": "^0.66.5"
74
74
  }
75
75
  }
@@ -6,8 +6,16 @@ export const OPERATING_PROFILES = Object.freeze([
6
6
  'ASSURE',
7
7
  ]);
8
8
  export const DEFAULT_OPERATING_PROFILE = 'GOVERN';
9
+ export const ADAPTIVE_OPERATING_PROFILES = Object.freeze([
10
+ 'FLOW',
11
+ 'GUIDE',
12
+ 'GOVERN',
13
+ 'ASSURE',
14
+ ]);
9
15
 
10
16
  const PROFILE_SET = new Set(OPERATING_PROFILES);
17
+ const ADAPTIVE_PROFILE_SET = new Set(ADAPTIVE_OPERATING_PROFILES);
18
+ export const TASK_PROFILE_REASON_MAX_LENGTH = 500;
11
19
 
12
20
  function policy(profile, route, options) {
13
21
  return Object.freeze({
@@ -70,6 +78,125 @@ function canonicalProfile(value) {
70
78
  return value.trim().toUpperCase();
71
79
  }
72
80
 
81
+ function taskProfileError(code, message) {
82
+ const error = new Error(message);
83
+ error.code = code;
84
+ return error;
85
+ }
86
+
87
+ function taskProfile(value) {
88
+ const profile = canonicalProfile(value);
89
+ if (ADAPTIVE_PROFILE_SET.has(profile)) return profile;
90
+ throw taskProfileError(
91
+ 'WENDKEEP_TASK_PROFILE_INVALID',
92
+ `Perfil temporário inválido: ${typeof value === 'string' ? `"${value}"` : String(value)}. `
93
+ + `Use ${ADAPTIVE_OPERATING_PROFILES.join(', ')}; OFF exige seleção humana persistente.`,
94
+ );
95
+ }
96
+
97
+ function taskReason(value) {
98
+ const reason = typeof value === 'string' ? value.trim() : '';
99
+ if (reason && reason.length <= TASK_PROFILE_REASON_MAX_LENGTH) return reason;
100
+ throw taskProfileError(
101
+ 'WENDKEEP_TASK_PROFILE_REASON_INVALID',
102
+ `Motivo da rota temporária deve ter entre 1 e ${TASK_PROFILE_REASON_MAX_LENGTH} caracteres.`,
103
+ );
104
+ }
105
+
106
+ function taskSequence(value) {
107
+ const sequence = Number(value);
108
+ return Number.isSafeInteger(sequence) && sequence > 0 ? sequence : null;
109
+ }
110
+
111
+ function taskContextError() {
112
+ return taskProfileError(
113
+ 'WENDKEEP_TASK_PROFILE_CONTEXT_INVALID',
114
+ 'Rota temporária exige sessão, prompt causal, lease id e timestamp válidos.',
115
+ );
116
+ }
117
+
118
+ export function createTaskOperatingProfileLease({
119
+ profile,
120
+ reason,
121
+ sessionId,
122
+ turnId = '',
123
+ turnSequence,
124
+ leaseId,
125
+ issuedAt,
126
+ } = {}) {
127
+ const selected = taskProfile(profile);
128
+ const auditedReason = taskReason(reason);
129
+ const session = typeof sessionId === 'string' ? sessionId.trim() : '';
130
+ const requestTurnId = typeof turnId === 'string' ? turnId.trim() : '';
131
+ const sequence = taskSequence(turnSequence);
132
+ const id = typeof leaseId === 'string' ? leaseId.trim() : '';
133
+ const issued = typeof issuedAt === 'string' ? issuedAt.trim() : '';
134
+ if (!session || !requestTurnId || sequence === null || !id || !issued || !Number.isFinite(Date.parse(issued))) {
135
+ throw taskContextError();
136
+ }
137
+ return {
138
+ lease_id: id,
139
+ state: 'active',
140
+ profile: selected,
141
+ requested_by: 'llm-harness',
142
+ reason: auditedReason,
143
+ session_id: session,
144
+ request_turn_id: requestTurnId,
145
+ request_turn_sequence: sequence,
146
+ issued_at: issued,
147
+ expires_on: 'request-stop',
148
+ };
149
+ }
150
+
151
+ export function evaluateTaskOperatingProfileLease(lease, {
152
+ sessionId = '',
153
+ turnId = '',
154
+ turnSequence,
155
+ } = {}) {
156
+ if (lease === undefined || lease === null) return { state: 'absent' };
157
+ if (!lease || typeof lease !== 'object' || Array.isArray(lease)) return { state: 'invalid' };
158
+
159
+ let normalized;
160
+ try {
161
+ normalized = createTaskOperatingProfileLease({
162
+ profile: lease.profile,
163
+ reason: lease.reason,
164
+ sessionId: lease.session_id,
165
+ turnId: lease.request_turn_id,
166
+ turnSequence: lease.request_turn_sequence,
167
+ leaseId: lease.lease_id,
168
+ issuedAt: lease.issued_at,
169
+ });
170
+ } catch {
171
+ return {
172
+ state: 'invalid',
173
+ ...(typeof lease.lease_id === 'string' && lease.lease_id ? { lease_id: lease.lease_id } : {}),
174
+ };
175
+ }
176
+ if (lease.requested_by !== 'llm-harness' || lease.expires_on !== 'request-stop') {
177
+ return { state: 'invalid', lease_id: normalized.lease_id };
178
+ }
179
+ if (lease.state === 'consumed' || lease.state === 'expired') {
180
+ return { ...lease, ...normalized, state: lease.state };
181
+ }
182
+ if (lease.state !== 'active') return { state: 'invalid', lease_id: normalized.lease_id };
183
+
184
+ const currentSession = typeof sessionId === 'string' ? sessionId.trim() : '';
185
+ const currentTurnId = typeof turnId === 'string' ? turnId.trim() : '';
186
+ const currentSequence = taskSequence(turnSequence);
187
+ if (!currentSession || !currentTurnId || currentSequence === null) {
188
+ return { ...normalized, state: 'invalid' };
189
+ }
190
+ if (normalized.session_id !== currentSession) {
191
+ return { ...normalized, state: 'invalid' };
192
+ }
193
+ const turnIdMismatch = normalized.request_turn_id !== currentTurnId;
194
+ if (turnIdMismatch || normalized.request_turn_sequence !== currentSequence) {
195
+ return { ...normalized, state: 'expired' };
196
+ }
197
+ return normalized;
198
+ }
199
+
73
200
  export function normalizeOperatingProfile(value, { strict = false } = {}) {
74
201
  const normalized = canonicalProfile(value);
75
202
  if (PROFILE_SET.has(normalized)) return normalized;