wendkeep 0.28.1 → 0.29.2
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 +42 -0
- package/bin/wendkeep.mjs +1 -0
- package/hooks/decision-capture.mjs +53 -0
- package/hooks/import-sessions.mjs +33 -1
- package/hooks/linked-notes.mjs +9 -0
- package/hooks/obsidian-common.mjs +21 -0
- package/hooks/session-backfill.mjs +4 -2
- package/hooks/session-ensure.mjs +1 -1
- package/hooks/session-start.mjs +3 -3
- package/hooks/session-stop.mjs +16 -11
- package/hooks/token-usage.mjs +1 -1
- package/hooks/vault-health.mjs +4 -4
- package/package.json +1 -1
- package/src/import.mjs +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,48 @@ All notable changes to **wendkeep** are documented here. Format based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project follows
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.29.2] — 2026-07-09
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **Iteration turn marker renamed `codex-turn` → `wk-turn`** (provider-neutral). A **Claude**
|
|
11
|
+
session's iterations carried `<!-- codex-turn: … -->` — a legacy name from when this was a
|
|
12
|
+
Codex-only tool, confusing in the note source. The marker is a dedup key, so the change is
|
|
13
|
+
backward-compatible: `hasTurnMarker` still recognizes the legacy name, and `insertIteration`
|
|
14
|
+
**self-migrates** any `codex-turn` → `wk-turn` on the next write (backfill re-processes older
|
|
15
|
+
notes). Shared helpers `turnMarker` / `hasTurnMarker` / `normalizeTurnMarkers` in obsidian-common;
|
|
16
|
+
`vault-health` recognizes both.
|
|
17
|
+
- Note-visible fallback text "Checkpoint registrado pelo hook Stop do **Codex**" → provider-neutral.
|
|
18
|
+
- Stderr log prefix `[codex-obsidian]` → `[wendkeep]` across the hooks.
|
|
19
|
+
|
|
20
|
+
## [0.29.1] — 2026-07-09
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- **`wendkeep import --rescan-decisions`** — re-scan **already-imported/captured** transcripts for
|
|
24
|
+
prose decisions only (no session re-import). For sessions imported before 0.29.0 whose rollouts
|
|
25
|
+
carry options-in-prose choices that were never captured. Walks the registry
|
|
26
|
+
(`session_file` + `transcript_path`), runs the same conservative extraction, dedupes by filename
|
|
27
|
+
— re-running is a no-op. `--limit N` / `--json` supported. New `rescanDecisions()` export.
|
|
28
|
+
|
|
29
|
+
## [0.29.0] — 2026-07-09
|
|
30
|
+
|
|
31
|
+
Codex decision parity — agnostic prose-decision capture.
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
- **Prose-decision capture** (`extractProseDecisions` / `captureProseDecisions` in
|
|
35
|
+
`hooks/decision-capture.mjs`, wired inside `createLinkedNotes`): Codex has no
|
|
36
|
+
`AskUserQuestion`-style tool — the agent asks in **prose**. A conservative pattern (assistant
|
|
37
|
+
message with ≥2 enumerated options ending in a question + a SHORT user reply) now produces the
|
|
38
|
+
**same decision note** the Claude hook writes (options + the user's choice, in `04-Decisões/`,
|
|
39
|
+
wikilinked to the session). One integration point covers **live Stop, `import` and backfill,
|
|
40
|
+
for every provider**. Validated on 144 real Codex rollouts: 6 genuine decisions extracted, no
|
|
41
|
+
visible false positives.
|
|
42
|
+
|
|
43
|
+
### Notes (investigated, decided against)
|
|
44
|
+
- **Codex subagent telemetry**: real rollouts contain **no** subagent/parallel structure — nothing
|
|
45
|
+
to map; documented as not applicable.
|
|
46
|
+
- **Codex structured events** (`thread_goal_updated`, `task_complete`): goal payload ≈ the initial
|
|
47
|
+
prompt; task events are turn markers already parsed. No extra capture worth the noise.
|
|
48
|
+
|
|
7
49
|
## [0.28.1] — 2026-07-09
|
|
8
50
|
|
|
9
51
|
Startup-contention fixes — root-caused from a real VSCode startup log where the memory injection
|
package/bin/wendkeep.mjs
CHANGED
|
@@ -54,6 +54,7 @@ Usage:
|
|
|
54
54
|
wendkeep import [opts] Backfill: import this project's past Claude + Codex sessions into
|
|
55
55
|
the vault (deduped by session_id). --source all|claude|codex (default
|
|
56
56
|
all) · --stamp-ids (backfill session_id in existing notes) ·
|
|
57
|
+
--rescan-decisions (capture prose decisions from already-imported transcripts) ·
|
|
57
58
|
--from <dir> · --codex-from <dir> · --since <date> · --limit N ·
|
|
58
59
|
--dry-run · --json.
|
|
59
60
|
wendkeep verify [--deep] [--change s] Run a change's task sensors + record evidence (the gate);
|
|
@@ -68,6 +68,59 @@ ${blocks}
|
|
|
68
68
|
`;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// --- agnostic prose decisions (Codex parity) ---------------------------------
|
|
72
|
+
// Codex has no AskUserQuestion-style tool: the agent asks in PROSE and the user answers in the
|
|
73
|
+
// next message. Conservative extraction (validated on real rollouts): an assistant message with
|
|
74
|
+
// >=2 enumerated options that ends in a question, followed by a SHORT user reply (a choice, not a
|
|
75
|
+
// new instruction). Works over the turn conversations both parsers already build — so it covers
|
|
76
|
+
// Claude and Codex, live and import, without depending on any hook event.
|
|
77
|
+
export function extractProseDecisions(tx) {
|
|
78
|
+
const flat = [];
|
|
79
|
+
for (const t of tx?.turns || []) for (const c of t.conversation || []) flat.push(c);
|
|
80
|
+
const out = [];
|
|
81
|
+
for (let i = 0; i < flat.length - 1; i++) {
|
|
82
|
+
const q = flat[i]; const a = flat[i + 1];
|
|
83
|
+
if (q.role !== 'Assistente' || a.role !== 'Usuário') continue;
|
|
84
|
+
const text = String(q.text || '');
|
|
85
|
+
const answer = String(a.text || '').trim();
|
|
86
|
+
if (!answer || answer.length > 200) continue; // long reply = new instruction, not a choice
|
|
87
|
+
// question: the message's last non-empty lines must end with '?'
|
|
88
|
+
const lines = text.trim().split('\n').map((l) => l.trim()).filter(Boolean);
|
|
89
|
+
const lastLine = lines[lines.length - 1] || '';
|
|
90
|
+
if (!/\?\s*$/.test(lastLine)) continue;
|
|
91
|
+
// options: numbered/lettered lines, or bulleted bold labels
|
|
92
|
+
let options = [...text.matchAll(/^\s*(?:\d+[\).]|[a-cA-C][\)])\s+(.{3,140})$/gm)].map((m) => m[1].trim());
|
|
93
|
+
if (options.length < 2) options = [...text.matchAll(/^\s*[-*]\s+\*\*(.{2,90}?)\*\*/gm)].map((m) => m[1].trim());
|
|
94
|
+
if (options.length < 2) continue;
|
|
95
|
+
const question = lines.filter((l) => /\?\s*$/.test(l)).pop() || lastLine;
|
|
96
|
+
out.push({ question: question.slice(0, 200), options: options.slice(0, 6), answer });
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Write one decision note per extracted prose decision (same shape as the hook capture).
|
|
102
|
+
// Deduped by filename (day + question slug). Returns the vault-relative paths written.
|
|
103
|
+
export function captureProseDecisions(vaultBase, { tx, dateStr, sessionRel, provider, localeId }) {
|
|
104
|
+
const written = [];
|
|
105
|
+
const decisions = extractProseDecisions(tx);
|
|
106
|
+
if (!decisions.length) return written;
|
|
107
|
+
const loc = getLocale(vaultBase);
|
|
108
|
+
const dir = join(vaultBase, monthFolderRelFromDateStr(loc.folders.decisions, dateStr, vaultBase));
|
|
109
|
+
for (const d of decisions) {
|
|
110
|
+
ensureDir(dir);
|
|
111
|
+
const filePath = join(dir, `${dateStr}-escolha-${slugify(d.question, 'decisao', 50)}.md`);
|
|
112
|
+
if (existsSync(filePath)) continue;
|
|
113
|
+
writeFileSync(filePath, buildDecisionCaptureNote({
|
|
114
|
+
questions: [{ question: d.question, multiSelect: false, options: d.options.map((label) => ({ label, description: '' })) }],
|
|
115
|
+
answers: { [d.question]: d.answer },
|
|
116
|
+
dateStr, startedAt: `${dateStr}T00:00:00`, sessionRel,
|
|
117
|
+
provider: provider || providerMeta(tx?.provider), localeId: localeId || loc.id,
|
|
118
|
+
}), 'utf-8');
|
|
119
|
+
written.push(toVaultRelative(vaultBase, filePath));
|
|
120
|
+
}
|
|
121
|
+
return written;
|
|
122
|
+
}
|
|
123
|
+
|
|
71
124
|
export function captureDecision(vaultBase, input) {
|
|
72
125
|
const toolIn = input.tool_input || input.toolInput || {};
|
|
73
126
|
const questions = Array.isArray(toolIn.questions) ? toolIn.questions : [];
|
|
@@ -18,8 +18,9 @@ import { buildSessionContent, allocateSessionPath } from './session-start.mjs';
|
|
|
18
18
|
import { createLinkedNotes } from './linked-notes.mjs';
|
|
19
19
|
import { updateSessionUsage } from './token-usage.mjs';
|
|
20
20
|
import { upsertSubagentUsage } from './subagent-usage.mjs';
|
|
21
|
-
import { readSessionRegistry, upsertSessionRegistry, formatLocalIso, formatDate } from './obsidian-common.mjs';
|
|
21
|
+
import { readSessionRegistry, upsertSessionRegistry, formatLocalIso, formatDate, providerMeta } from './obsidian-common.mjs';
|
|
22
22
|
import { getLocale } from './locale.mjs';
|
|
23
|
+
import { captureProseDecisions } from './decision-capture.mjs';
|
|
23
24
|
|
|
24
25
|
// Claude encodes a project's absolute path as its `.claude/projects` dir name by replacing each
|
|
25
26
|
// path separator and the drive colon with '-'. `C:\GitHub\WendKeep` -> `C--GitHub-WendKeep`.
|
|
@@ -237,6 +238,37 @@ export function importSession(vaultBase, txPath, opts = {}) {
|
|
|
237
238
|
return { sessionId, relPath, turns: turns.length };
|
|
238
239
|
}
|
|
239
240
|
|
|
241
|
+
// Re-scan ALREADY-imported/captured transcripts for prose decisions only (no session re-import).
|
|
242
|
+
// For sessions imported before 0.29.0, whose transcripts carry options-in-prose choices that were
|
|
243
|
+
// never captured. Walks the registry (session_file + transcript_path), parses each transcript and
|
|
244
|
+
// runs captureProseDecisions — filename-deduped, so re-running is a no-op. Fail-soft per session.
|
|
245
|
+
export function rescanDecisions(vaultBase, { limit = 0 } = {}) {
|
|
246
|
+
const sessions = readSessionRegistry(vaultBase).sessions || {};
|
|
247
|
+
const report = { scanned: 0, decisions: 0, errors: [], sessions: [] };
|
|
248
|
+
let done = 0;
|
|
249
|
+
for (const [sessionId, entry] of Object.entries(sessions)) {
|
|
250
|
+
if (!entry?.transcript_path || !entry.session_file) continue;
|
|
251
|
+
if (!existsSync(entry.transcript_path)) continue;
|
|
252
|
+
if (limit && done >= limit) break;
|
|
253
|
+
report.scanned += 1;
|
|
254
|
+
done += 1;
|
|
255
|
+
try {
|
|
256
|
+
const tx = parseTranscript(entry.transcript_path);
|
|
257
|
+
const dateStr = String(entry.started_at || entry.ended_at || '').slice(0, 10) || formatDate(new Date());
|
|
258
|
+
const written = captureProseDecisions(vaultBase, {
|
|
259
|
+
tx, dateStr, sessionRel: entry.session_file, provider: providerMeta(tx.provider),
|
|
260
|
+
});
|
|
261
|
+
if (written.length) {
|
|
262
|
+
report.decisions += written.length;
|
|
263
|
+
report.sessions.push({ sessionId, notes: written });
|
|
264
|
+
}
|
|
265
|
+
} catch (error) {
|
|
266
|
+
report.errors.push({ sessionId, error: error.message });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return report;
|
|
270
|
+
}
|
|
271
|
+
|
|
240
272
|
// Stamp a missing/empty `session_id` into a note's frontmatter (only within the frontmatter
|
|
241
273
|
// block, right after `provider:`). Leaves a note that already has a non-empty id untouched.
|
|
242
274
|
function injectSessionIdFrontmatter(content, sessionId) {
|
package/hooks/linked-notes.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
wikilinkFromRel,
|
|
14
14
|
} from './obsidian-common.mjs';
|
|
15
15
|
import { getLocale } from './locale.mjs';
|
|
16
|
+
import { captureProseDecisions } from './decision-capture.mjs';
|
|
16
17
|
|
|
17
18
|
function yamlQuote(value) {
|
|
18
19
|
return `"${String(value || '').replaceAll('"', '\\"')}"`;
|
|
@@ -568,6 +569,14 @@ export function createLinkedNotes(vaultBase, dateStr, sessionRel, tx, options =
|
|
|
568
569
|
}
|
|
569
570
|
}
|
|
570
571
|
|
|
572
|
+
// Agnostic prose decisions (Codex parity): options-in-prose + short answer -> decision note.
|
|
573
|
+
// One integration point covers live Stop, import and backfill, for every provider. Fail-quiet.
|
|
574
|
+
try {
|
|
575
|
+
for (const rel of captureProseDecisions(vaultBase, { tx, dateStr, sessionRel, provider, localeId: loc.id })) {
|
|
576
|
+
linked.decisions.push(rel);
|
|
577
|
+
}
|
|
578
|
+
} catch { /* prose capture é bônus — nunca derruba a captura principal */ }
|
|
579
|
+
|
|
571
580
|
const learnings = extractLearningDetails(tx, bugDetails);
|
|
572
581
|
if (learnings) {
|
|
573
582
|
const vaultLearningKeys = collectLearningKeys(vaultBase); // vault-wide dedup
|
|
@@ -557,6 +557,27 @@ export function wikilinkFromRel(relPath) {
|
|
|
557
557
|
return `[[${relPath.replace(/\.md$/i, '').replaceAll('\\', '/')}]]`;
|
|
558
558
|
}
|
|
559
559
|
|
|
560
|
+
// Per-iteration dedup marker (an invisible HTML comment). Provider-neutral name `wk-turn`; the
|
|
561
|
+
// old `codex-turn` (legacy, from when this was a Codex-only tool) is still RECOGNIZED so notes
|
|
562
|
+
// written by older versions keep deduping, and normalizeTurnMarkers migrates them on the next write.
|
|
563
|
+
export const TURN_MARKER = 'wk-turn';
|
|
564
|
+
export const LEGACY_TURN_MARKERS = ['codex-turn'];
|
|
565
|
+
|
|
566
|
+
export function turnMarker(id) {
|
|
567
|
+
return `<!-- ${TURN_MARKER}: ${id} -->`;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export function hasTurnMarker(content, id) {
|
|
571
|
+
return [TURN_MARKER, ...LEGACY_TURN_MARKERS].some((m) => String(content || '').includes(`<!-- ${m}: ${id} -->`));
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Rewrite any legacy turn markers in a note to the current name (self-healing migration).
|
|
575
|
+
export function normalizeTurnMarkers(content) {
|
|
576
|
+
let c = String(content || '');
|
|
577
|
+
for (const m of LEGACY_TURN_MARKERS) c = c.replaceAll(`<!-- ${m}: `, `<!-- ${TURN_MARKER}: `);
|
|
578
|
+
return c;
|
|
579
|
+
}
|
|
580
|
+
|
|
560
581
|
export function listMarkdownFiles(dir) {
|
|
561
582
|
try {
|
|
562
583
|
return readdirSync(dir).filter((f) => f.endsWith('.md'));
|
|
@@ -36,7 +36,9 @@ function parseArgs(argv) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
function hasTurnMarker(content, turnId) {
|
|
39
|
-
|
|
39
|
+
// recognize both the current `wk-turn` and the legacy `codex-turn`
|
|
40
|
+
return content.includes(`<!-- wk-turn: ${turnId} -->`)
|
|
41
|
+
|| content.includes(`<!-- codex-turn: ${turnId} -->`);
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
function sessionEntries(vaultBase, args) {
|
|
@@ -135,7 +137,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
|
|
135
137
|
try {
|
|
136
138
|
main();
|
|
137
139
|
} catch (error) {
|
|
138
|
-
process.stderr.write(`[
|
|
140
|
+
process.stderr.write(`[wendkeep] Backfill falhou: ${error.message}\n`);
|
|
139
141
|
process.exitCode = 1;
|
|
140
142
|
}
|
|
141
143
|
}
|
package/hooks/session-ensure.mjs
CHANGED
|
@@ -387,6 +387,6 @@ function main() {
|
|
|
387
387
|
try {
|
|
388
388
|
main();
|
|
389
389
|
} catch (error) {
|
|
390
|
-
process.stderr.write(`[
|
|
390
|
+
process.stderr.write(`[wendkeep] UserPromptSubmit falhou: ${error.message}\n`);
|
|
391
391
|
writeHookOutput({});
|
|
392
392
|
}
|
package/hooks/session-start.mjs
CHANGED
|
@@ -161,7 +161,7 @@ function main() {
|
|
|
161
161
|
try {
|
|
162
162
|
sweepStaleSessionsFile(vaultBase, now, undefined, input.transcript_path || input.transcriptPath || '');
|
|
163
163
|
} catch (error) {
|
|
164
|
-
process.stderr.write(`[
|
|
164
|
+
process.stderr.write(`[wendkeep] sweep de sessões falhou: ${error.message}\n`);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
// Reuso da nota apontada pelo CURRENT_SESSION só quando é a MESMA conversa
|
|
@@ -309,9 +309,9 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
|
|
309
309
|
try {
|
|
310
310
|
main();
|
|
311
311
|
} catch (error) {
|
|
312
|
-
process.stderr.write(`[
|
|
312
|
+
process.stderr.write(`[wendkeep] SessionStart falhou: ${error.message}\n`);
|
|
313
313
|
writeHookOutput({
|
|
314
|
-
systemMessage: `[
|
|
314
|
+
systemMessage: `[wendkeep] Não foi possível criar a sessão Obsidian: ${error.message}`,
|
|
315
315
|
});
|
|
316
316
|
}
|
|
317
317
|
}
|
package/hooks/session-stop.mjs
CHANGED
|
@@ -30,6 +30,9 @@ import {
|
|
|
30
30
|
wikilinkFromRel,
|
|
31
31
|
writeControl,
|
|
32
32
|
writeHookOutput,
|
|
33
|
+
turnMarker,
|
|
34
|
+
hasTurnMarker,
|
|
35
|
+
normalizeTurnMarkers,
|
|
33
36
|
} from './obsidian-common.mjs';
|
|
34
37
|
|
|
35
38
|
function extractContentText(content) {
|
|
@@ -596,7 +599,7 @@ export function buildIterationBlock(tx, input) {
|
|
|
596
599
|
// inclui recortes da conversa do turno, sem despejar outputs brutos.
|
|
597
600
|
return `
|
|
598
601
|
### ${formatTimeForHeading(now)} - ${heading}
|
|
599
|
-
|
|
602
|
+
${turnMarker(turnId)}
|
|
600
603
|
|
|
601
604
|
**Pedido:** ${compactText(promptText, 1000)}
|
|
602
605
|
|
|
@@ -609,7 +612,7 @@ ${formatConversation(turn)}
|
|
|
609
612
|
|
|
610
613
|
**Arquivos detectados no turno:** ${formatInlineList(files, 'Nenhum arquivo detectado automaticamente.')}
|
|
611
614
|
|
|
612
|
-
**Estado ao final do turno:** ${compactText(latestAssistant || 'Checkpoint registrado
|
|
615
|
+
**Estado ao final do turno:** ${compactText(latestAssistant || 'Checkpoint registrado automaticamente ao final do turno.', 900)}
|
|
613
616
|
`;
|
|
614
617
|
}
|
|
615
618
|
|
|
@@ -751,17 +754,19 @@ function relocateOrphanIterations(content) {
|
|
|
751
754
|
}
|
|
752
755
|
|
|
753
756
|
export function insertIteration(sessionPath, block, turnId, tx) {
|
|
754
|
-
|
|
755
|
-
|
|
757
|
+
const original = readFileSync(sessionPath, 'utf-8');
|
|
758
|
+
// Self-heal: migrate any legacy `codex-turn` markers to the neutral name on this write.
|
|
759
|
+
let content = normalizeTurnMarkers(original);
|
|
760
|
+
if (hasTurnMarker(content, turnId)) {
|
|
756
761
|
// Turno já registrado: ainda assim repara órfãos e seções dedicadas.
|
|
757
762
|
const repaired = applyDedicatedSections(relocateOrphanIterations(content), tx);
|
|
758
|
-
if (repaired !==
|
|
763
|
+
if (repaired !== original) writeFileSync(sessionPath, repaired, 'utf-8');
|
|
759
764
|
return false;
|
|
760
765
|
}
|
|
761
766
|
content = relocateOrphanIterations(content);
|
|
762
767
|
content = insertIntoIteracoes(content, block);
|
|
763
768
|
content = applyDedicatedSections(content, tx);
|
|
764
|
-
writeFileSync(sessionPath, content, 'utf-8');
|
|
769
|
+
if (content !== original) writeFileSync(sessionPath, content, 'utf-8');
|
|
765
770
|
return true;
|
|
766
771
|
}
|
|
767
772
|
|
|
@@ -1037,7 +1042,7 @@ function main() {
|
|
|
1037
1042
|
try {
|
|
1038
1043
|
applyLinearLinks(sessionPath, tx, vaultBase, sessionRel);
|
|
1039
1044
|
} catch (error) {
|
|
1040
|
-
process.stderr.write(`[
|
|
1045
|
+
process.stderr.write(`[wendkeep] Linear link falhou: ${error.message}\n`);
|
|
1041
1046
|
}
|
|
1042
1047
|
|
|
1043
1048
|
try {
|
|
@@ -1048,7 +1053,7 @@ function main() {
|
|
|
1048
1053
|
transcriptPath,
|
|
1049
1054
|
});
|
|
1050
1055
|
} catch (error) {
|
|
1051
|
-
process.stderr.write(`[
|
|
1056
|
+
process.stderr.write(`[wendkeep] Token usage falhou: ${error.message}\n`);
|
|
1052
1057
|
}
|
|
1053
1058
|
|
|
1054
1059
|
// Subagent/workflow telemetry (0.10.0): fold sibling subagent transcripts into the note.
|
|
@@ -1056,7 +1061,7 @@ function main() {
|
|
|
1056
1061
|
try {
|
|
1057
1062
|
upsertSubagentUsage(sessionPath, transcriptPath);
|
|
1058
1063
|
} catch (error) {
|
|
1059
|
-
process.stderr.write(`[
|
|
1064
|
+
process.stderr.write(`[wendkeep] Subagent usage falhou: ${error.message}\n`);
|
|
1060
1065
|
}
|
|
1061
1066
|
|
|
1062
1067
|
if (!shouldFinalizeSession()) {
|
|
@@ -1127,7 +1132,7 @@ function main() {
|
|
|
1127
1132
|
const rows = buildBrainIndex(vaultBase);
|
|
1128
1133
|
buildBrainDigest(vaultBase, rows);
|
|
1129
1134
|
} catch (error) {
|
|
1130
|
-
process.stderr.write(`[
|
|
1135
|
+
process.stderr.write(`[wendkeep] brain index/digest falhou: ${error.message}\n`);
|
|
1131
1136
|
}
|
|
1132
1137
|
|
|
1133
1138
|
pingObsidianVault(input.obsidian_api_key);
|
|
@@ -1138,7 +1143,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
|
|
1138
1143
|
try {
|
|
1139
1144
|
main();
|
|
1140
1145
|
} catch (error) {
|
|
1141
|
-
process.stderr.write(`[
|
|
1146
|
+
process.stderr.write(`[wendkeep] Stop falhou: ${error.message}\n`);
|
|
1142
1147
|
writeHookOutput({});
|
|
1143
1148
|
}
|
|
1144
1149
|
}
|
package/hooks/token-usage.mjs
CHANGED
|
@@ -966,7 +966,7 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
|
966
966
|
try {
|
|
967
967
|
runCli();
|
|
968
968
|
} catch (error) {
|
|
969
|
-
process.stderr.write(`[
|
|
969
|
+
process.stderr.write(`[wendkeep] Token usage falhou: ${error.message}\n`);
|
|
970
970
|
process.exitCode = 1;
|
|
971
971
|
}
|
|
972
972
|
}
|
package/hooks/vault-health.mjs
CHANGED
|
@@ -39,7 +39,7 @@ function parseArgs(argv) {
|
|
|
39
39
|
function findDuplicateTurnMarkers(content) {
|
|
40
40
|
const seen = new Set();
|
|
41
41
|
const duplicated = new Set();
|
|
42
|
-
const regex = /<!-- codex-turn: ([^>]+) -->/g;
|
|
42
|
+
const regex = /<!-- (?:wk-turn|codex-turn): ([^>]+) -->/g;
|
|
43
43
|
let match;
|
|
44
44
|
while ((match = regex.exec(content)) !== null) {
|
|
45
45
|
const turnId = match[1].trim();
|
|
@@ -104,10 +104,10 @@ function checkSession({ vaultBase, sessionRel, control, registry }) {
|
|
|
104
104
|
|
|
105
105
|
const content = readFileSync(sessionPath, 'utf-8');
|
|
106
106
|
const duplicates = findDuplicateTurnMarkers(content);
|
|
107
|
-
metrics.turnMarkers = (content.match(/<!-- codex-turn:/g) || []).length;
|
|
107
|
+
metrics.turnMarkers = (content.match(/<!-- (?:wk-turn|codex-turn):/g) || []).length;
|
|
108
108
|
metrics.duplicateTurnMarkers = duplicates.length;
|
|
109
109
|
|
|
110
|
-
if (duplicates.length) failures.push(`Marcadores
|
|
110
|
+
if (duplicates.length) failures.push(`Marcadores de turno duplicados: ${duplicates.join(', ')}`);
|
|
111
111
|
if (hasHeadingAfterClosing(content)) failures.push('Há headings/iterações após ## Encerramento.');
|
|
112
112
|
if (!usageSectionIsPlaced(content)) failures.push('## Uso de tokens e custos está fora da posição esperada.');
|
|
113
113
|
if (hasDefaultPending(content)) warnings.push('Pendências ainda contém placeholders padrão.');
|
|
@@ -197,7 +197,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
|
|
197
197
|
try {
|
|
198
198
|
main();
|
|
199
199
|
} catch (error) {
|
|
200
|
-
process.stderr.write(`[
|
|
200
|
+
process.stderr.write(`[wendkeep] Vault health falhou: ${error.message}\n`);
|
|
201
201
|
process.exitCode = 1;
|
|
202
202
|
}
|
|
203
203
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.2",
|
|
4
4
|
"description": "A persistent-memory harness for AI coding agents on your Obsidian vault: turn-by-turn session capture plus a native, zero-dependency spec→change→verify→archive loop (sensor-gated, independent verdict, mutation discrimination). Local-first, agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/import.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// by session_id). One command backfills your whole history: cost, subagents, iterations.
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
5
|
import { isAbsolute, resolve } from 'node:path';
|
|
6
|
-
import { runImport, stampSessionIds } from '../hooks/import-sessions.mjs';
|
|
6
|
+
import { runImport, stampSessionIds, rescanDecisions } from '../hooks/import-sessions.mjs';
|
|
7
7
|
|
|
8
8
|
function opt(argv, name) {
|
|
9
9
|
const i = argv.indexOf(name);
|
|
@@ -18,6 +18,16 @@ export function runImportCli(argv) {
|
|
|
18
18
|
const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
|
|
19
19
|
if (!existsSync(vaultBase)) { process.stderr.write(`wendkeep import: vault not found: ${vaultBase}\n`); process.exit(2); }
|
|
20
20
|
|
|
21
|
+
// Repair mode: re-scan already-imported transcripts for prose decisions only, then stop.
|
|
22
|
+
if (argv.includes('--rescan-decisions')) {
|
|
23
|
+
const r = rescanDecisions(vaultBase, { limit: Number(opt(argv, '--limit')) || 0 });
|
|
24
|
+
if (argv.includes('--json')) { process.stdout.write(`${JSON.stringify(r, null, 2)}\n`); process.exit(0); }
|
|
25
|
+
process.stdout.write(`${r.scanned} transcript(s) varridos · ${r.decisions} decisão(ões) capturadas\n`);
|
|
26
|
+
for (const s of r.sessions) for (const n of s.notes) process.stdout.write(` → ${n}\n`);
|
|
27
|
+
if (r.errors.length) for (const e of r.errors) process.stderr.write(` erro ${e.sessionId}: ${e.error}\n`);
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
// Repair mode: backfill session_id into existing notes from the registry, then stop.
|
|
22
32
|
if (argv.includes('--stamp-ids')) {
|
|
23
33
|
const r = stampSessionIds(vaultBase);
|