wendkeep 0.26.0 → 0.28.1

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 CHANGED
@@ -4,6 +4,65 @@ 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.28.1] — 2026-07-09
8
+
9
+ Startup-contention fixes — root-caused from a real VSCode startup log where the memory injection
10
+ silently dropped and MCPs timed out.
11
+
12
+ ### Fixed
13
+ - **`brain-inject` timeout 15 → 45s.** The hook is healthy (~2.5s direct, ~4s via npx warm), but
14
+ Windows session startup runs several `npx` cold-starts at once (a sibling MCP took **26s** in the
15
+ log) and 15s silently killed the CORE+DIGEST injection for the whole session.
16
+ - **context-mode double-registration eliminated.** Its plugin ships its **own** MCP server; wiring
17
+ an `.mcp.json` entry too registered it twice — two concurrent `npx context-mode` cold-starts,
18
+ both timing out. The companion is now **plugin-only** (on non-Claude agents add the MCP manually:
19
+ `npx -y context-mode`).
20
+ - **`MCP_TIMEOUT=60000` default** added to the settings `env` by init (only when absent — a user
21
+ value is never clobbered), giving npx-launched stdio MCPs (wendkeep-vault included) headroom over
22
+ Claude Code's 30s default.
23
+
24
+ ### Upgrade
25
+ - Existing installs: re-run `wendkeep init` (now recognizes your vault) to pick up the timeout +
26
+ `MCP_TIMEOUT`; remove a duplicated `context-mode` entry from `.mcp.json`/`enabledMcpjsonServers`
27
+ by hand if present.
28
+
29
+ ## [0.28.0] — 2026-07-09
30
+
31
+ Three new hooks: decisions, subagents, plan progress.
32
+
33
+ ### Added
34
+ - **Decision capture** (`PostToolUse` / `AskUserQuestion` → `hooks/decision-capture.mjs`): when the
35
+ agent asks the user to choose between options, the decision is recorded in `04-Decisões/` — the
36
+ question, **every** option (label + description), the user's choice (✅), and a wikilink to the
37
+ session. Explicit, high-signal decisions get full traceability in the graph. Shape validated
38
+ against real transcripts.
39
+ - **Live subagent telemetry** (`SubagentStop` → `hooks/subagent-stop.mjs`): refreshes the session's
40
+ subagent/workflow cost notes the moment each subagent finishes (reuses `upsertSubagentUsage`), so
41
+ a session that never reaches `Stop` still has its telemetry. *Model choice stays the harness's
42
+ job — wendkeep observes, it does not impose a routing rule.*
43
+ - **Plan progress log** (`TaskCompleted` → `hooks/task-log.mjs`): when a task is marked complete,
44
+ appends it to a durable `## Progresso do plano` section in the active session note (before
45
+ `## Encerramento`, so reopen can't strip it). A progress trail, not a fuzzy map to `tarefas.md`.
46
+
47
+ All three are wired by `wendkeep init`, are fail-open, and localize (pt-BR / en). `--force`-free —
48
+ they only read + append.
49
+
50
+ ## [0.27.0] — 2026-07-08
51
+
52
+ ### Fixed
53
+ - **Re-running `wendkeep init` no longer re-asks for the vault (or language) — and can't split your
54
+ data.** On a project already set up, init now reads the registered vault from
55
+ `.claude/settings.json` (`OBSIDIAN_VAULT_PATH`) and the locked locale from the vault's
56
+ `.brain/config.json`, reuses both, and skips the prompts. Previously a re-run (e.g. after
57
+ `npm i -D wendkeep@latest`) offered the *derived* default (`.<project>-vault`); accepting it — or
58
+ mistyping the name — created a **second, divergent vault**. `--vault` / `--locale` still override.
59
+ New exported `detectRegisteredVault()` / `readVaultLocale()`. `src/init.mjs`.
60
+
61
+ ### Note
62
+ - You do **not** need `wendkeep init` for a routine update: the hooks live in the package
63
+ (`settings.json` calls `npx wendkeep hook …`), so `npm i -D wendkeep@latest` updates them.
64
+ Re-run `init` only when a release adds new wiring (the CHANGELOG says so); it's idempotent.
65
+
7
66
  ## [0.26.0] — 2026-07-08
8
67
 
9
68
  ### Fixed
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ // PostToolUse hook (matcher: AskUserQuestion). When the agent asks the user to choose between
3
+ // options, this records the decision — the question, EVERY option (label + description), and the
4
+ // user's choice — as a note in 04-Decisões, wikilinked to the session. Explicit, high-signal
5
+ // decisions get full traceability in the graph (better than heuristic extraction). Fail-open.
6
+ import { existsSync, writeFileSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { pathToFileURL } from 'url';
9
+ import {
10
+ readHookInput, writeHookOutput, getVaultBase, providerMeta, ensureDir, formatDate,
11
+ formatLocalIso, monthFolderRelFromDateStr, slugify, findActiveSessionByTranscript,
12
+ wikilinkFromRel, readControl, toVaultRelative,
13
+ } from './obsidian-common.mjs';
14
+ import { getLocale } from './locale.mjs';
15
+
16
+ // The AskUserQuestion tool_output reads: `... "Question"="chosen labels" "Q2"="..."`.
17
+ export function parseAnswers(output) {
18
+ const map = {};
19
+ const re = /"([^"]+)"\s*=\s*"([^"]*)"/g;
20
+ let m;
21
+ while ((m = re.exec(String(output || '')))) map[m[1].trim()] = m[2].trim();
22
+ return map;
23
+ }
24
+
25
+ const clean = (s) => String(s || '').replace(/\r/g, '').replace(/\n{3,}/g, '\n\n').trim();
26
+
27
+ // Render the decision note from an AskUserQuestion tool call.
28
+ export function buildDecisionCaptureNote({ questions, answers, dateStr, startedAt, sessionRel, provider, localeId }) {
29
+ const en = localeId === 'en';
30
+ const src = sessionRel ? `\n - "${wikilinkFromRel(sessionRel)}"` : ' []';
31
+ const title = clean(questions[0]?.question || (en ? 'User decision' : 'Decisão do usuário')).slice(0, 90);
32
+
33
+ const blocks = questions.map((q) => {
34
+ const chosen = (answers[clean(q.question)] || '').split(',').map((s) => s.trim()).filter(Boolean);
35
+ const isChosen = (label) => chosen.some((c) => c === label.trim() || c.includes(label.trim()) || label.trim().includes(c));
36
+ const opts = (q.options || [])
37
+ .map((o) => `| ${isChosen(o.label) ? '✅' : ''} | ${clean(o.label)} | ${clean(o.description).slice(0, 200)} |`)
38
+ .join('\n');
39
+ return `### ${clean(q.question)}
40
+ ${q.multiSelect ? (en ? '_(multiple choice)_' : '_(múltipla escolha)_') : ''}
41
+
42
+ | | ${en ? 'Option' : 'Opção'} | ${en ? 'Description' : 'Descrição'} |
43
+ |---|---|---|
44
+ ${opts || `| | — | — |`}
45
+
46
+ **${en ? 'Chosen' : 'Escolhido'}:** ${chosen.length ? chosen.map((c) => `\`${c}\``).join(', ') : (en ? '(not recorded)' : '(não registrado)')}`;
47
+ }).join('\n\n');
48
+
49
+ return `---
50
+ type: decision
51
+ subtype: user-choice
52
+ date: ${dateStr}
53
+ started_at: ${startedAt}
54
+ provider: ${provider.id}
55
+ cssclasses:
56
+ - topic-decision
57
+ tags:
58
+ - decisao
59
+ - escolha-usuario
60
+ source:${src}
61
+ ---
62
+
63
+ # ${title}
64
+
65
+ > ${en ? 'Decision captured from an interactive question (options + the user\'s choice).' : 'Decisão capturada de uma pergunta interativa (opções + a escolha do usuário).'}
66
+
67
+ ${blocks}
68
+ `;
69
+ }
70
+
71
+ export function captureDecision(vaultBase, input) {
72
+ const toolIn = input.tool_input || input.toolInput || {};
73
+ const questions = Array.isArray(toolIn.questions) ? toolIn.questions : [];
74
+ if (!questions.length) return null;
75
+
76
+ const answers = parseAnswers(input.tool_output ?? input.toolOutput ?? input.tool_response ?? '');
77
+ const loc = getLocale(vaultBase);
78
+ const now = new Date();
79
+ const dateStr = formatDate(now);
80
+ const provider = providerMeta(input.provider);
81
+
82
+ const matched = input.transcript_path ? findActiveSessionByTranscript(vaultBase, input.transcript_path) : null;
83
+ const sessionRel = matched?.session_file || readControl(vaultBase).session_file || '';
84
+
85
+ const dir = join(vaultBase, monthFolderRelFromDateStr(loc.folders.decisions, dateStr, vaultBase));
86
+ ensureDir(dir);
87
+ const slug = slugify(questions[0]?.question || 'decisao', 'decisao', 50);
88
+ const filePath = join(dir, `${dateStr}-escolha-${slug}.md`);
89
+ if (existsSync(filePath)) return { rel: toVaultRelative(vaultBase, filePath), skipped: true };
90
+
91
+ writeFileSync(filePath, buildDecisionCaptureNote({
92
+ questions, answers, dateStr, startedAt: formatLocalIso(now), sessionRel, provider, localeId: loc.id,
93
+ }), 'utf-8');
94
+ return { rel: toVaultRelative(vaultBase, filePath), skipped: false };
95
+ }
96
+
97
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
98
+ try {
99
+ const input = readHookInput();
100
+ if ((input.tool_name || input.toolName) === 'AskUserQuestion') {
101
+ captureDecision(getVaultBase(input), input);
102
+ }
103
+ writeHookOutput({});
104
+ } catch (error) {
105
+ process.stderr.write(`[wendkeep] decision-capture falhou: ${error.message}\n`);
106
+ writeHookOutput({});
107
+ }
108
+ }
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ // SubagentStop hook: refresh this session's subagent/workflow telemetry the MOMENT a subagent
3
+ // finishes — not only at the main Stop. Resilience: a session that never reaches Stop (crash,
4
+ // window closed) still gets its subagent cost notes. Reuses the same upsertSubagentUsage the Stop
5
+ // hook runs, so the output is identical; it just runs earlier + incrementally. Fail-open.
6
+ //
7
+ // Model choice for subagents stays the harness's job (agent frontmatter `model:` / the Task/
8
+ // workflow `model` param). wendkeep OBSERVES (this telemetry) rather than dictating a routing rule.
9
+ import { existsSync } from 'fs';
10
+ import { join } from 'path';
11
+ import { pathToFileURL } from 'url';
12
+ import { readHookInput, writeHookOutput, getVaultBase, findActiveSessionByTranscript, readControl } from './obsidian-common.mjs';
13
+ import { upsertSubagentUsage } from './subagent-usage.mjs';
14
+
15
+ export function refreshSubagents(vaultBase, input) {
16
+ const transcriptPath = input.transcript_path || input.transcriptPath || '';
17
+ const matched = transcriptPath ? findActiveSessionByTranscript(vaultBase, transcriptPath) : null;
18
+ const sessionRel = matched?.session_file || readControl(vaultBase).session_file || '';
19
+ if (!sessionRel) return false;
20
+ const sessionPath = join(vaultBase, sessionRel);
21
+ if (!existsSync(sessionPath)) return false;
22
+ upsertSubagentUsage(sessionPath, transcriptPath);
23
+ return true;
24
+ }
25
+
26
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
27
+ try {
28
+ const input = readHookInput();
29
+ refreshSubagents(getVaultBase(input), input);
30
+ writeHookOutput({});
31
+ } catch (error) {
32
+ process.stderr.write(`[wendkeep] subagent-stop falhou: ${error.message}\n`);
33
+ writeHookOutput({});
34
+ }
35
+ }
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ // TaskCompleted hook: log plan/task progress into the active session note, so the session records
3
+ // what the plan actually advanced through. It writes a durable "## Progresso do plano" section
4
+ // placed BEFORE ## Encerramento (survives the reopen/strip cycle) and does NOT try to map to a
5
+ // change's tarefas.md N.N (id-spaces differ, fuzzy) — it's a progress trail, not a task tracker.
6
+ // The TaskCompleted payload shape isn't fully pinned, so the task text is pulled defensively from
7
+ // any plausible field. Fail-open.
8
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { pathToFileURL } from 'url';
11
+ import { readHookInput, writeHookOutput, getVaultBase, findActiveSessionByTranscript, readControl, formatHourMinute } from './obsidian-common.mjs';
12
+ import { getLocale } from './locale.mjs';
13
+
14
+ // Pull the task's human text from whatever field the payload carries.
15
+ export function taskText(input) {
16
+ const t = input.task || input.tool_input || input.payload || input;
17
+ const cand = t?.content || t?.text || t?.description || t?.title || t?.subject || t?.name
18
+ || t?.prompt || input.content || input.text || input.description || '';
19
+ return String(cand || '').replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 160);
20
+ }
21
+
22
+ // Append `line` at the END of a "## <heading>" section placed before ## Encerramento, so entries
23
+ // read chronologically. Deduped. Pure.
24
+ export function appendProgress(content, line, heading) {
25
+ if (content.includes(line)) return content; // dedup
26
+ const marker = `\n## ${heading}\n`;
27
+ const at = content.indexOf(marker);
28
+ if (at !== -1) {
29
+ const sectionStart = at + marker.length;
30
+ let end = content.indexOf('\n## ', sectionStart);
31
+ if (end === -1) end = content.length;
32
+ const head = content.slice(0, end).replace(/\s+$/, '');
33
+ return `${head}\n${line}\n${content.slice(end)}`;
34
+ }
35
+ const block = `## ${heading}\n\n${line}\n\n`;
36
+ const enc = content.indexOf('\n## Encerramento');
37
+ if (enc === -1) return `${content.trimEnd()}\n\n${block}`;
38
+ return `${content.slice(0, enc).trimEnd()}\n\n${block}${content.slice(enc + 1)}`;
39
+ }
40
+
41
+ export function logTask(vaultBase, input) {
42
+ const text = taskText(input);
43
+ if (!text) return false;
44
+ const transcriptPath = input.transcript_path || input.transcriptPath || '';
45
+ const matched = transcriptPath ? findActiveSessionByTranscript(vaultBase, transcriptPath) : null;
46
+ const sessionRel = matched?.session_file || readControl(vaultBase).session_file || '';
47
+ if (!sessionRel) return false;
48
+ const sessionPath = join(vaultBase, sessionRel);
49
+ if (!existsSync(sessionPath)) return false;
50
+
51
+ const heading = getLocale(vaultBase).id === 'en' ? 'Plan progress' : 'Progresso do plano';
52
+ const line = `- [x] ${formatHourMinute(new Date()).replace('-', ':')} ${text}`;
53
+ const content = readFileSync(sessionPath, 'utf8');
54
+ const next = appendProgress(content, line, heading);
55
+ if (next !== content) { writeFileSync(sessionPath, next, 'utf8'); return true; }
56
+ return false;
57
+ }
58
+
59
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
60
+ try {
61
+ const input = readHookInput();
62
+ logTask(getVaultBase(input), input);
63
+ writeHookOutput({});
64
+ } catch (error) {
65
+ process.stderr.write(`[wendkeep] task-log falhou: ${error.message}\n`);
66
+ writeHookOutput({});
67
+ }
68
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.26.0",
3
+ "version": "0.28.1",
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/init.mjs CHANGED
@@ -126,6 +126,10 @@ export function mergeSettings(existing, { vaultPath, withMcp, force, companions
126
126
  added += 1;
127
127
  }
128
128
  s.env = { ...(s.env || {}), OBSIDIAN_VAULT_PATH: vaultPath };
129
+ // npx-launched stdio MCPs (wendkeep-vault included) can cold-start near/over Claude Code's 30s
130
+ // default under Windows startup contention (26s observed in a real log). Give them headroom —
131
+ // but never clobber a value the user already set.
132
+ if (!('MCP_TIMEOUT' in s.env)) s.env.MCP_TIMEOUT = '60000';
129
133
 
130
134
  // Claude Code plugin layer for the selected companions (additive, non-clobbering).
131
135
  const cp = companionSettingsPatch(companions);
@@ -249,6 +253,7 @@ const MESSAGES = {
249
253
  step3: ' 3. Abra o Claude Code neste projeto e envie um prompt de teste.',
250
254
  step4: ' 4. Confirme que uma nota aparece em 02-Sessões/<ano>/<mês>/DIA <dd>/ no vault.',
251
255
  updateLater: ' Atualize depois com: npm update wendkeep (sem recopiar — os hooks vivem no pacote).\n',
256
+ vaultRegistered: (v, loc) => `\n cofre já registrado neste projeto: ${v} (locale ${loc}) — pergunta pulada. Use --vault para mudar.`,
252
257
  },
253
258
  en: {
254
259
  header: '\nwendkeep init',
@@ -273,6 +278,7 @@ const MESSAGES = {
273
278
  step3: ' 3. Open Claude Code in this project and send a test prompt.',
274
279
  step4: ' 4. Confirm a note appears under 02-Sessões/<year>/<month>/DIA <dd>/ in the vault.',
275
280
  updateLater: ' Update later with: npm update wendkeep (no re-copying — hooks live in the package).\n',
281
+ vaultRegistered: (v, loc) => `\n vault already registered for this project: ${v} (locale ${loc}) — prompt skipped. Use --vault to change.`,
276
282
  },
277
283
  };
278
284
 
@@ -287,13 +293,39 @@ export function parseLocaleAnswer(ans) {
287
293
  return 'pt-BR';
288
294
  }
289
295
 
296
+ // The vault path this project was set up with — read from .claude/settings.json's
297
+ // OBSIDIAN_VAULT_PATH (written by a prior init). Empty when the project isn't configured yet.
298
+ export function detectRegisteredVault(projectPath) {
299
+ try {
300
+ const s = JSON.parse(readFileSync(join(projectPath, '.claude', 'settings.json'), 'utf8'));
301
+ const v = s && s.env && s.env.OBSIDIAN_VAULT_PATH;
302
+ return typeof v === 'string' && v ? v : '';
303
+ } catch { return ''; }
304
+ }
305
+
306
+ // The locale locked in a vault's .brain/config.json (empty if absent/invalid).
307
+ export function readVaultLocale(vaultPath) {
308
+ try {
309
+ const c = JSON.parse(readFileSync(join(vaultPath, '.brain', 'config.json'), 'utf8'));
310
+ return c && LOCALES[c.locale] ? c.locale : '';
311
+ } catch { return ''; }
312
+ }
313
+
290
314
  export async function runInit(argv) {
291
315
  const args = parseArgs(argv);
292
316
  const projectPath = resolve(args.project || process.cwd());
293
317
  const log = (s) => process.stdout.write(`${s}\n`);
294
318
 
295
- // Language first (i18n): an interactive TTY without --locale is asked the vault language;
296
- // folders, prompts and scaffold all follow the answer.
319
+ // Recognize an already-configured project: the vault is registered in the project's
320
+ // settings.json (OBSIDIAN_VAULT_PATH) and its locale is locked in the vault's config.json.
321
+ // On re-run (e.g. after `npm i -D wendkeep@latest`) we reuse both and SKIP the language + vault
322
+ // prompts — asking again risks a divergent vault from a mistyped name. `--vault` / `--locale`
323
+ // override; the vault question is a once-per-project thing.
324
+ const registeredVault = args.vault ? '' : detectRegisteredVault(projectPath);
325
+ if (registeredVault && !args.locale) args.locale = readVaultLocale(registeredVault) || args.locale;
326
+
327
+ // Language first (i18n): an interactive TTY without --locale (and not already registered) is
328
+ // asked the vault language; folders, prompts and scaffold all follow the answer.
297
329
  if (!args.locale && process.stdin.isTTY && !args.yes) {
298
330
  const rl = createInterface({ input: process.stdin, output: process.stdout });
299
331
  const ans = await rl.question('Idioma do vault / Vault language:\n [1] Português (pt-BR) [2] English (en)\n> ');
@@ -304,8 +336,10 @@ export async function runInit(argv) {
304
336
  const P = promptStrings(resolvedLocale);
305
337
  const M = initMessages(resolvedLocale);
306
338
 
307
- let vaultPath = args.vault;
308
- if (!vaultPath) {
339
+ let vaultPath = args.vault || registeredVault;
340
+ if (registeredVault) {
341
+ log(M.vaultRegistered(registeredVault, resolvedLocale));
342
+ } else if (!vaultPath) {
309
343
  const fallback = join(projectPath, deriveVaultDirName(projectPath));
310
344
  if (process.stdin.isTTY && !args.yes) {
311
345
  const rl = createInterface({ input: process.stdin, output: process.stdout });
package/src/taxonomy.mjs CHANGED
@@ -44,6 +44,9 @@ export const HOOK_FILES = [
44
44
  'brain-reindex.mjs',
45
45
  'session-backfill.mjs',
46
46
  'import-sessions.mjs',
47
+ 'decision-capture.mjs',
48
+ 'subagent-stop.mjs',
49
+ 'task-log.mjs',
47
50
  'vault-health.mjs',
48
51
  'understand-inject.mjs',
49
52
  ];
@@ -56,6 +59,9 @@ export const RUNNABLE_HOOKS = [
56
59
  'session-ensure',
57
60
  'session-stop',
58
61
  'session-backfill',
62
+ 'decision-capture',
63
+ 'subagent-stop',
64
+ 'task-log',
59
65
  'brain-inject',
60
66
  'brain-recall',
61
67
  'brain-reindex',
@@ -81,10 +87,19 @@ export const SESSION_HOOKS = [
81
87
  // Memory + active-change injection. Runs FIRST on SessionStart (order -10, folds before
82
88
  // session-start) so the agent gets CORE + DIGEST + the active change + lessons as context.
83
89
  // matcher 'startup|clear|compact' re-injects after a compaction/clear, not only cold startup.
84
- { event: 'SessionStart', matcher: 'startup|clear|compact', name: 'brain-inject', timeout: 15, order: -10, statusMessage: 'wendkeep: injecting memory + active change' },
90
+ // timeout 45 (was 15): measured ~4s warm via npx, but Windows startup contention (several npx
91
+ // cold-starts at once — a sibling MCP took 26s in a real log) blew 15s and silently dropped the
92
+ // memory injection for the whole session.
93
+ { event: 'SessionStart', matcher: 'startup|clear|compact', name: 'brain-inject', timeout: 45, order: -10, statusMessage: 'wendkeep: injecting memory + active change' },
85
94
  { event: 'SessionStart', matcher: 'startup', name: 'session-start', timeout: 30, statusMessage: 'wendkeep: opening Obsidian session' },
86
95
  { event: 'Stop', matcher: null, name: 'session-stop', timeout: 60, statusMessage: 'wendkeep: writing session checkpoint' },
87
96
  { event: 'UserPromptSubmit', matcher: null, name: 'session-ensure', timeout: 30, statusMessage: 'wendkeep: ensuring active session' },
97
+ // Capture an interactive decision (AskUserQuestion) — options + the user's choice — into 04-Decisões.
98
+ { event: 'PostToolUse', matcher: 'AskUserQuestion', name: 'decision-capture', timeout: 15, statusMessage: 'wendkeep: recording decision' },
99
+ // Refresh subagent/workflow telemetry as each subagent finishes (resilient to a missed Stop).
100
+ { event: 'SubagentStop', matcher: null, name: 'subagent-stop', timeout: 20, statusMessage: 'wendkeep: subagent telemetry' },
101
+ // Log plan/task progress into the active session note when a task is marked complete.
102
+ { event: 'TaskCompleted', matcher: null, name: 'task-log', timeout: 10, statusMessage: 'wendkeep: plan progress' },
88
103
  ];
89
104
 
90
105
  export function hookCommand(name) {
@@ -108,8 +123,10 @@ export const COMPANIONS = [
108
123
  default: false,
109
124
  marketplace: { source: 'git', url: 'https://github.com/mksglu/context-mode.git' },
110
125
  plugin: 'context-mode',
111
- // Agent-agnostic: MCP server, self-updating via unpinned npx.
112
- mcp: { key: 'context-mode', entry: { type: 'stdio', command: 'npx', args: ['-y', 'context-mode'] } },
126
+ // NO .mcp.json entry on purpose: the plugin ships its OWN MCP server, so wiring both
127
+ // double-registered it (two concurrent `npx context-mode` cold-starts at session start —
128
+ // both timed out in a real log). Plugin is the single source; on non-Claude agents add the
129
+ // MCP manually (`npx -y context-mode`).
113
130
  },
114
131
  {
115
132
  id: 'understand-anything',