@lh8ppl/claude-memory-kit 0.3.4 → 0.4.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 (47) hide show
  1. package/README.md +6 -0
  2. package/bin/cmk-guard-memory.mjs +57 -0
  3. package/package.json +3 -2
  4. package/src/agent-profile.mjs +115 -0
  5. package/src/agent-profiles.mjs +118 -0
  6. package/src/auto-persona.mjs +4 -1
  7. package/src/compress-retry.mjs +25 -0
  8. package/src/compress-session.mjs +27 -3
  9. package/src/config-core.mjs +7 -9
  10. package/src/daily-distill.mjs +7 -3
  11. package/src/decisions-journal.mjs +71 -3
  12. package/src/doctor.mjs +86 -4
  13. package/src/guard-memory.mjs +151 -0
  14. package/src/import-anthropic-memory.mjs +15 -1
  15. package/src/inject-context.mjs +34 -3
  16. package/src/install-agent.mjs +220 -0
  17. package/src/install-kiro.mjs +287 -0
  18. package/src/install.mjs +16 -3
  19. package/src/kiro-cli-agent.mjs +270 -0
  20. package/src/kiro-constants.mjs +19 -0
  21. package/src/kiro-hook-bin.mjs +105 -0
  22. package/src/kiro-hook-command.mjs +67 -0
  23. package/src/kiro-hook-dispatch.mjs +115 -0
  24. package/src/kiro-ide-hooks.mjs +219 -0
  25. package/src/kiro-permissions.mjs +175 -0
  26. package/src/kiro-skills.mjs +96 -0
  27. package/src/kiro-transcript.mjs +366 -0
  28. package/src/kiro-trusted-commands.mjs +130 -0
  29. package/src/lazy-compress.mjs +6 -0
  30. package/src/managed-block.mjs +138 -0
  31. package/src/memory-write.mjs +23 -8
  32. package/src/mutate-agent-config.mjs +243 -0
  33. package/src/read-json.mjs +43 -0
  34. package/src/reindex.mjs +15 -2
  35. package/src/repair.mjs +39 -3
  36. package/src/result-shapes.mjs +8 -0
  37. package/src/review-queue.mjs +3 -0
  38. package/src/scratchpad.mjs +12 -2
  39. package/src/search.mjs +12 -5
  40. package/src/semantic-backend.mjs +7 -9
  41. package/src/settings-hooks.mjs +12 -2
  42. package/src/subcommands.mjs +360 -27
  43. package/src/tier-paths.mjs +48 -1
  44. package/src/weekly-curate.mjs +13 -6
  45. package/template/.claude/skills/memory-search/SKILL.md +14 -1
  46. package/template/.claude/skills/memory-write/SKILL.md +37 -1
  47. package/template/project/memory/INDEX.md.template +1 -1
@@ -14,6 +14,16 @@
14
14
  // asserts exactly what's exported here, so coverage stays automatic.
15
15
 
16
16
  import { install as installAction, initUserTier as initUserTierAction } from './install.mjs';
17
+ import { installAgent } from './install-agent.mjs';
18
+ import { installKiro, uninstallKiro } from './install-kiro.mjs';
19
+ import { getAgentProfile, listAgentProfiles } from './agent-profiles.mjs';
20
+ import { runKiroHook } from './kiro-hook-bin.mjs';
21
+ import { readKiroTurn } from './kiro-transcript.mjs';
22
+ import { injectContext } from './inject-context.mjs';
23
+ import { captureTurn } from './capture-turn.mjs';
24
+ import { capturePrompt } from './capture-prompt.mjs';
25
+ import { observeEdit } from './observe-edit.mjs';
26
+ import { decideGuard } from './guard-memory.mjs';
17
27
  import { removeClaudeMdBlock } from './claude-md.mjs';
18
28
  import { reindex as reindexAction } from './reindex.mjs';
19
29
  import { openIndexDb } from './index-db.mjs';
@@ -52,7 +62,7 @@ import {
52
62
  } from './register-crons.mjs';
53
63
  import { fileURLToPath } from 'node:url';
54
64
  import { dirname } from 'node:path';
55
- import { readFileSync } from 'node:fs';
65
+ import { readFileSync, existsSync } from 'node:fs';
56
66
 
57
67
  const __filename_subcommands = fileURLToPath(import.meta.url);
58
68
  const __dirname_subcommands = dirname(__filename_subcommands);
@@ -65,6 +75,27 @@ import { createInterface } from 'node:readline';
65
75
  import { spawnSync } from 'node:child_process';
66
76
  import { checkKitBinding } from './native-binding.mjs';
67
77
  import { resolve as resolvePath, join, basename } from 'node:path';
78
+ import { resolveMcpProjectRoot, normalizeProjectPath } from './tier-paths.mjs';
79
+
80
+ /**
81
+ * Resolve the cmk-auto-extract.mjs bin path so the KIRO stop hook's captureTurn
82
+ * can spawn the detached auto-extract child (D-200). Mirrors the Claude Code bin's
83
+ * resolution (env override → sibling bin/): without this, runHook called
84
+ * captureTurn with no autoExtractPath and the in-module default is null, so
85
+ * spawnAutoExtract short-circuits 'no-auto-extract-path' — capture writes the
86
+ * transcript but NEVER extracts facts → no wedge promotion. The CLI's runHook
87
+ * lives in src/, so the bin is a `../bin/` sibling. A missing bin → null → no
88
+ * spawn (fail-safe for a never-crash hook); spawnAutoExtract logs the reason.
89
+ * @param {Record<string,string|undefined>} [env=process.env]
90
+ * @returns {string|null} absolute path to the bin, or null if it can't be found
91
+ */
92
+ export function resolveKiroAutoExtractPath(env = process.env) {
93
+ if (env.CMK_AUTO_EXTRACT_PATH && env.CMK_AUTO_EXTRACT_PATH.trim() !== '') {
94
+ return env.CMK_AUTO_EXTRACT_PATH;
95
+ }
96
+ const sibling = join(__dirname_subcommands, '..', 'bin', 'cmk-auto-extract.mjs');
97
+ return existsSync(sibling) ? sibling : null;
98
+ }
68
99
 
69
100
  const NOTICE_PREFIX = 'not yet implemented';
70
101
 
@@ -169,6 +200,16 @@ export async function runInstall(options /* , command */) {
169
200
  // commander maps `--no-hooks` to options.hooks === false.
170
201
  const noHooks = !!(options && options.hooks === false);
171
202
  const verbose = !!(options && options.verbose);
203
+
204
+ // Task 50.F — cross-agent routing. Default is claude-code (the existing path,
205
+ // untouched). For another agent, scaffold the agent-neutral project tier via
206
+ // installAction with hooks OFF (the Claude-Code hook wiring doesn't apply), then
207
+ // wire THAT agent's legs (hooks + MCP + instruction file) via installAgent.
208
+ const ide = (options && options.ide) || 'claude-code';
209
+ if (ide !== 'claude-code') {
210
+ return runInstallForAgent({ ide, options, log, logError });
211
+ }
212
+
172
213
  const result = await installAction({
173
214
  force: !!(options && options.force),
174
215
  noHooks,
@@ -254,20 +295,281 @@ export async function runInstall(options /* , command */) {
254
295
  }
255
296
 
256
297
  /**
257
- * `cmk uninstall`wired in Task 4. Strips the kit-managed block from
258
- * the project's CLAUDE.md (if present). Everything outside the markers
259
- * is byte-preserved. Does NOT touch context/, context.local/, the user
260
- * tier, or .gitignore `cmk uninstall` is conservative; users delete
261
- * those by hand if they really want to.
298
+ * Task 50.F`cmk install --ide <agent>` for a non-Claude-Code agent.
299
+ * Scaffolds the agent-neutral project tier (context/, CLAUDE.md block, gitignore
300
+ * via installAction with hooks off, since the Claude-Code hook wiring doesn't
301
+ * apply), then wires THAT agent's legs (hooks + MCP + instruction file) via
302
+ * installAgent. One step no second command (the user-friendly criterion).
262
303
  */
263
- function runUninstall(/* options, command */) {
264
- const projectRoot = resolvePath(process.cwd());
304
+ async function runInstallForAgent({ ide, options, log, logError }) {
305
+ const profile = getAgentProfile(ide);
306
+ if (!profile) {
307
+ const known = listAgentProfiles().map((p) => p.name).join(', ');
308
+ logError(`cmk install: unknown --ide '${ide}'. Supported: ${known}.`);
309
+ process.exitCode = 2;
310
+ return;
311
+ }
312
+
313
+ // 1) agent-neutral scaffold (context/ + the kit's own CLAUDE.md block live
314
+ // regardless of agent). Hooks OFF — the agent's hooks are wired in step 2.
315
+ // skipClaudeFiles: `.claude/skills/` + `CLAUDE.md` are Claude-Code-specific;
316
+ // a non-CC agent gets its instructions from its own surface (Kiro →
317
+ // .kiro/skills/ + .kiro/steering/ + AGENTS.md), so we must not leave dead
318
+ // Claude files on the project (D-188). An EXISTING Claude install's files
319
+ // are untouched — we just don't create fresh ones.
320
+ const scaffold = await installAction({
321
+ force: !!(options && options.force),
322
+ noHooks: true,
323
+ withSemantic: !!(options && options.withSemantic),
324
+ noSemantic: !!(options && options.semantic === false),
325
+ projectRoot: options?.cwd,
326
+ userTier: options?.userTier,
327
+ bindingProbe: options?.bindingProbe,
328
+ spawnNpm: options?.spawnNpm,
329
+ warmEmbedder: options?.warmEmbedder,
330
+ skipClaudeFiles: true,
331
+ });
332
+
333
+ const projectName = basename(scaffold.projectRoot);
334
+
335
+ // 2) wire the agent's surfaces. Kiro has its OWN orchestrator (D-182): five
336
+ // surfaces (MCP + steering + skills + IDE hooks + the CLI agent-config),
337
+ // not the generic installAgent's Claude-Code-shaped model.
338
+ if (ide === 'kiro') {
339
+ // awsDir: tests/sandboxes pass $MEMORY_KIT_AWS_DIR via options.awsDir to keep
340
+ // the CLI-agent leg out of the real ~/.aws; production leaves it undefined →
341
+ // the real ~/.kiro (where kiro-cli actually reads its agents).
342
+ const r = installKiro({ projectRoot: scaffold.projectRoot, awsDir: options?.awsDir });
343
+ if (r.action === 'error') {
344
+ for (const e of r.errors || []) {
345
+ logError(` error: Kiro ${e.surface}: ${(e.errors || []).join('; ')}`);
346
+ }
347
+ logError(
348
+ `cmk install: ${projectName} scaffolded but Kiro wiring failed (a config file could not be safely written — see above).`,
349
+ );
350
+ process.exitCode = 1;
351
+ return;
352
+ }
353
+ log(
354
+ `cmk install: ${projectName} ready for Kiro — context/ scaffolded; ${r.surfaces.join(' + ')} wired.`,
355
+ );
356
+ log(' Restart Kiro to activate the hooks (steering + skills + MCP are immediate).');
357
+ // The CLI agent-config (kiro-cli) is automatic only when cmk is the default
358
+ // agent. If the user already has a default, surface the one manual step.
359
+ if (r.cliDefaultAgent === 'skipped-existing') {
360
+ log(' Note: you already have a Kiro CLI default agent — the kit installed a `cmk` agent instead.');
361
+ log(' Run `kiro-cli --agent cmk`, or set `chat.defaultAgent` to `cmk`, for automatic CLI memory.');
362
+ }
363
+ if (scaffold.errors.length > 0) {
364
+ for (const e of scaffold.errors) logError(` error: ${e.path}: ${e.error}`);
365
+ process.exitCode = 1;
366
+ }
367
+ return;
368
+ }
369
+
370
+ // Other agents: the generic per-profile installer.
371
+ const wired = installAgent({ projectRoot: scaffold.projectRoot, profile });
372
+
373
+ if (wired.action === 'error') {
374
+ for (const e of wired.errors || []) {
375
+ logError(` error: ${profile.displayName} ${e.leg}: ${(e.errors || []).join('; ')}`);
376
+ }
377
+ // Report which legs DID land (every leg is independently idempotent +
378
+ // touch-only, so a re-run after fixing the flagged file is safe).
379
+ const landed = Object.entries(wired.legs || {})
380
+ .filter(([, action]) => action && action !== 'error')
381
+ .map(([leg]) => leg);
382
+ if (landed.length) {
383
+ logError(` (${landed.join(' + ')} already wired; re-run after fixing the file above — safe, idempotent.)`);
384
+ }
385
+ logError(
386
+ `cmk install: ${projectName} scaffolded but ${profile.displayName} wiring failed (a config file could not be safely written — see above).`,
387
+ );
388
+ process.exitCode = 1;
389
+ return;
390
+ }
391
+
392
+ // Describe what THIS integration type actually wired (instruction-only writes
393
+ // just the instruction file; full agents wire hooks + MCP too).
394
+ const wiredLegs = [
395
+ wired.legs.instruction ? 'instruction file' : null,
396
+ wired.legs.mcp ? 'MCP' : null,
397
+ wired.legs.hooks ? 'hooks' : null,
398
+ ].filter(Boolean);
399
+ log(
400
+ `cmk install: ${projectName} ready for ${profile.displayName} — context/ scaffolded, ${wiredLegs.join(' + ')} wired.`,
401
+ );
402
+ if (profile.integrationType === 'instruction-only') {
403
+ log(' Instruction-file only (no hooks/MCP) — a portable memory-awareness rung for tools that read AGENTS.md.');
404
+ } else {
405
+ log(' Restart the agent to activate. Complete install — one step, no separate command.');
406
+ }
407
+
408
+ if (scaffold.errors.length > 0) {
409
+ for (const e of scaffold.errors) logError(` error: ${e.path}: ${e.error}`);
410
+ process.exitCode = 1;
411
+ }
412
+ }
413
+
414
+ /**
415
+ * Task 50.J/50.L — `cmk hook <event>` — the Kiro hook entrypoint.
416
+ *
417
+ * Kiro's IDE + CLI hooks call `cmk hook <event>` (agentSpawn / promptSubmit /
418
+ * stop / preToolUse). Unlike Claude Code's hook bins (which read a stdin JSON
419
+ * payload), Kiro passes context via argv + env + cwd + its transcript file
420
+ * (probe-verified, P-CJYGTQYR). runKiroHook adapts that to the kit's cores.
421
+ *
422
+ * ALWAYS exits 0 EXCEPT a deliberate preToolUse BLOCK (exit 2 → the memory
423
+ * delete-guardrail, D-192). A crashed hook still exits 0 (fail-open).
424
+ */
425
+ /**
426
+ * Extract the about-to-run TOOL COMMAND from a preToolUse hook payload, for the
427
+ * memory delete-guardrail (D-192).
428
+ *
429
+ * Kiro's `preToolUse` delivers `{ tool_name, tool_input: { command } }` on STDIN
430
+ * — the SAME shape as Claude Code (VERIFIED from the real oh-my-kiro + vibekit
431
+ * preToolUse hooks: `cat | jq '.tool_input.command'`). So the production guard
432
+ * uses the `cmk-guard-memory` bin directly (it reads that stdin); this helper is
433
+ * the forward-compat path if a `cmk hook preToolUse` is ever wired instead. A
434
+ * `.command` fallback covers a flattened payload. No match → '' (fail-open).
435
+ */
436
+ export function kiroToolCommand(payload, _cwd) {
437
+ const p = payload || {};
438
+ const cmd = p.tool_input?.command ?? p.command;
439
+ return typeof cmd === 'string' ? cmd : '';
440
+ }
441
+
442
+ export function runHook(event, _options = {}, _command, deps = {}) {
443
+ const log = deps.log ?? ((s) => process.stdout.write(s));
444
+ const logError = deps.logError ?? ((s) => process.stderr.write(`${s}\n`));
445
+ // The Kiro hook payload. Most events (agentSpawn/promptSubmit/stop/preToolUse)
446
+ // get their input from argv + env (USER_PROMPT) + cwd + transcript, NOT stdin —
447
+ // and runHook must NOT blocking-read fd 0 for them (a dangling-open `runCommand`
448
+ // stdin would hang the hook to its timeout — B1, the skill-review catch).
449
+ //
450
+ // postToolUse (50.N.2) is the ONE event whose data (tool_name/tool_input/
451
+ // tool_response) has no env fallback — it arrives as a STDIN JSON payload. We
452
+ // read stdin ONLY for postToolUse, TTY-guarded (readHookStdin returns '' on a
453
+ // TTY). Tests inject `deps.payload` to bypass the read entirely.
454
+ //
455
+ // WHY THIS IS SAFE (the precedent, not a guess): the kit ALREADY ships a
456
+ // stdin-reading hook in the SAME kiro-cli agent config — `cmk-guard-memory`
457
+ // (preToolUse) reads stdin the identical way (readHookStdin → readFileSync(0)).
458
+ // preToolUse and postToolUse are siblings in the same Amazon-Q hook contract,
459
+ // both delivering {tool_name, tool_input, …} on a piped-and-closed stdin. If
460
+ // Kiro left that stdin dangling-open, the already-merged guard would hang on
461
+ // every shell command — it doesn't. So this read rests on the same (verified-by-
462
+ // -contract-shape) assumption the guard already relies on, NOT a new risk.
463
+ // The 50.N.1 B1 fix removed a stdin read for events that have an ENV fallback
464
+ // (the prompt) — postToolUse has none, so the read is necessary here.
465
+ // VERIFICATION (surfaced, not buried): the live stdin-close + payload-shape
466
+ // assumption both this leg AND the preToolUse guard depend on is a BLOCKING
467
+ // cut-gate item (KG-observe, paired with KG-guard) — one real kiro-cli fs_write
468
+ // verifies both. Until that passes live, this leg is "structurally wired,
469
+ // live-unverified" (stated in the PR body + the 50.N.2 task, per the no-disclaimer
470
+ // -ships-latent rule).
471
+ let payload = deps.payload;
472
+ if (payload === undefined) {
473
+ if (event === 'postToolUse') {
474
+ try {
475
+ const readStdin = deps.readStdin ?? (() => readHookStdin({ isTTY: process.stdin.isTTY }));
476
+ const raw = readStdin();
477
+ payload = raw && raw.trim() !== '' ? JSON.parse(raw) : {};
478
+ } catch {
479
+ payload = {};
480
+ }
481
+ } else {
482
+ payload = {};
483
+ }
484
+ }
485
+ const r = runKiroHook({
486
+ argv: [event],
487
+ cwd: deps.cwd ?? process.cwd(),
488
+ env: deps.env ?? process.env,
489
+ payload,
490
+ deps: {
491
+ readKiroTurn: deps.readKiroTurn ?? readKiroTurn,
492
+ // injectContext returns the assembled context string; normalize to {text}.
493
+ // userDir is passed through so cross-project user-tier memory surfaces on
494
+ // Kiro inject too (injectContext resolves $MEMORY_KIT_USER_DIR when userDir
495
+ // is absent, but pass it explicitly when the caller provides one).
496
+ inject: deps.inject ?? ((args) => {
497
+ const text = injectContext({ cwd: args.cwd, ...(args.userDir ? { userDir: args.userDir } : {}) });
498
+ return { ok: true, text: typeof text === 'string' ? text : text?.text ?? '' };
499
+ }),
500
+ // Pass a RESOLVED autoExtractPath so captureTurn can spawn the detached
501
+ // auto-extract child (D-200 — else no extraction, no wedge promotion). The
502
+ // `captureTurn` impl is injectable (deps.captureTurn) so a test can assert
503
+ // the autoExtractPath is forwarded WITHOUT replacing the whole capture dep
504
+ // (replacing `capture` would bypass the very wiring under test).
505
+ capture: deps.capture ?? ((args) => (deps.captureTurn ?? captureTurn)({
506
+ payload: args.payload,
507
+ projectRoot: args.projectRoot,
508
+ autoExtractPath: resolveKiroAutoExtractPath(deps.env ?? process.env),
509
+ })),
510
+ // 50.N.1 — prompt-capture on the prompt-submit events (the <private>-strip
511
+ // + transcript-append half of Claude Code's UserPromptSubmit). Best-effort;
512
+ // the dispatcher swallows a throw so inject still runs.
513
+ capturePrompt: deps.capturePrompt ?? ((args) => capturePrompt({
514
+ payload: args.payload,
515
+ projectRoot: args.projectRoot,
516
+ })),
517
+ // 50.N.2 — postToolUse → observe-edit (the file-edit observation leg). The
518
+ // bin maps Kiro's fs_write → Write before this runs. Best-effort.
519
+ observe: deps.observe ?? ((args) => observeEdit({
520
+ payload: args.payload,
521
+ projectRoot: args.projectRoot,
522
+ })),
523
+ // preToolUse → the memory delete-guardrail (D-192). Reads the about-to-run
524
+ // tool command out of the Kiro payload and returns {block, reason?}.
525
+ guard: deps.guard ?? ((args) => decideGuard(kiroToolCommand(args.payload, args.cwd))),
526
+ },
527
+ });
528
+ if (r.stdout) log(r.stdout);
529
+ if (r.stderr) logError(r.stderr);
530
+ // The always-exit-0 invariant holds for EVERY event EXCEPT a deliberate
531
+ // preToolUse BLOCK (exit 2 → Kiro blocks the tool, D-192). Honor the
532
+ // dispatcher's exitCode (0 for inject/capture/noop/error; 2 only on a guard
533
+ // block) instead of hardcoding 0 — a crashed guard still fails open (the
534
+ // dispatcher's catch returns exitCode 0).
535
+ process.exitCode = typeof r.exitCode === 'number' ? r.exitCode : 0;
536
+ }
537
+
538
+ /**
539
+ * `cmk uninstall [--ide <agent>]` — remove ONE agent's kit-managed surface,
540
+ * scoped by `--ide` exactly like `cmk install` (D-189). Default (no flag)
541
+ * removes the Claude Code surface (the CLAUDE.md managed block); `--ide kiro`
542
+ * removes the Kiro surface (the .kiro/ managed blocks + skills + IDE hooks + the
543
+ * guarded ~/.aws CLI agent + the AGENTS.md block). BOTH are conservative: they
544
+ * NEVER touch context/, context.local/, the user tier, or .gitignore — the
545
+ * shared brain is sacred. Everything outside the kit's markers is byte-preserved.
546
+ */
547
+ export function runUninstall(options /*, command */) {
548
+ const projectRoot = resolvePath((options && options.cwd) || process.cwd());
549
+ const ide = (options && options.ide) || 'claude-code';
550
+ // Injectable log sinks (mirror runInstall) so tests can capture output.
551
+ const log = options?.log ?? console.log;
552
+ const logError = options?.logError ?? console.error;
553
+
554
+ if (ide === 'kiro') {
555
+ const r = uninstallKiro({ projectRoot, awsDir: options?.awsDir });
556
+ log(
557
+ `cmk uninstall (kiro): ${r.changed ? 'removed the Kiro managed surface' : 'nothing to remove'} — context/ preserved.`,
558
+ );
559
+ return;
560
+ }
561
+ if (ide !== 'claude-code') {
562
+ logError(`cmk uninstall: unknown --ide '${ide}'. Supported: claude-code, kiro.`);
563
+ process.exitCode = 2;
564
+ return;
565
+ }
566
+
265
567
  const result = removeClaudeMdBlock({ projectRoot });
266
- console.log(`cmk uninstall: CLAUDE.md=${result.action} (${result.path})`);
568
+ log(`cmk uninstall: CLAUDE.md=${result.action} (${result.path})`);
267
569
  if (result.action === 'not-found') {
268
- console.log(' (no kit-managed block found; CLAUDE.md left unchanged)');
570
+ log(' (no kit-managed block found; CLAUDE.md left unchanged)');
269
571
  } else if (result.action === 'no-file') {
270
- console.log(' (no CLAUDE.md to uninstall from)');
572
+ log(' (no CLAUDE.md to uninstall from)');
271
573
  }
272
574
  }
273
575
 
@@ -381,7 +683,10 @@ function runLessonsPromote(id, options = {}) {
381
683
  * --include-tombstoned (default false)
382
684
  */
383
685
  async function runSearch(queryParts, options) {
384
- const projectRoot = resolvePath(process.cwd());
686
+ // --project <dir> overrides cwd (the kiro-cli fix, Kiro #4579 — no `cd` prefix).
687
+ const projectRoot = options?.project
688
+ ? resolvePath(normalizeProjectPath(options.project))
689
+ : resolvePath(process.cwd());
385
690
  const userDir =
386
691
  process.env.MEMORY_KIT_USER_DIR ?? join(homedir(), '.claude-memory-kit');
387
692
  const query = Array.isArray(queryParts) ? queryParts.join(' ') : queryParts;
@@ -763,7 +1068,13 @@ export function parseFactInput(options, { readFile, readStdin } = {}) {
763
1068
  // (one model call, explicit path only). Commander awaits actions; the
764
1069
  // terse-path tests were updated to await (contract change, intent preserved).
765
1070
  export async function runRemember(textParts, options, deps = {}) {
766
- const projectRoot = deps.projectRoot ?? resolvePath(process.cwd());
1071
+ // --project <dir> overrides cwd (the kiro-cli fix, Kiro #4579): kiro-cli's
1072
+ // command allowlist rejects a `cd … && cmk remember …` prefix, so the kiro-cli
1073
+ // agent runs `cmk remember "<fact>" --project "<abs>"` with NO cd. normalize a
1074
+ // git-bash `/c/Temp` path the model may emit → `C:/Temp`.
1075
+ const projectRoot = options?.project
1076
+ ? resolvePath(normalizeProjectPath(options.project))
1077
+ : (deps.projectRoot ?? resolvePath(process.cwd()));
767
1078
  const userDir =
768
1079
  deps.userDir ?? process.env.MEMORY_KIT_USER_DIR ?? join(homedir(), '.claude-memory-kit');
769
1080
  const log = deps.log ?? console.log;
@@ -1410,12 +1721,16 @@ export function runConfigCli(options /* , command */) {
1410
1721
  async function runRepairCli(options /* , command */) {
1411
1722
  const projectRoot = resolvePath(process.cwd());
1412
1723
  const userDir = join(homedir(), '.claude-memory-kit');
1413
- // Scope flags: --hooks / --locks / --index → run that one only.
1414
- // --all OR no flag → run all three.
1724
+ // Scope flags: --hooks / --locks / --index / --format → run that one only.
1725
+ // --all OR no flag → run all of them.
1726
+ const only = (flag) =>
1727
+ options?.[flag] &&
1728
+ !['hooks', 'locks', 'index', 'format'].filter((f) => f !== flag).some((f) => options?.[f]);
1415
1729
  let scope;
1416
- if (options?.hooks && !options?.locks && !options?.index) scope = 'hooks';
1417
- else if (options?.locks && !options?.hooks && !options?.index) scope = 'locks';
1418
- else if (options?.index && !options?.hooks && !options?.locks) scope = 'index';
1730
+ if (only('hooks')) scope = 'hooks';
1731
+ else if (only('locks')) scope = 'locks';
1732
+ else if (only('index')) scope = 'index';
1733
+ else if (only('format')) scope = 'format';
1419
1734
  else scope = 'all';
1420
1735
 
1421
1736
  try {
@@ -1680,11 +1995,13 @@ async function runCompress(options /* , command */) {
1680
1995
 
1681
1996
  async function runMcpDispatch(childName) {
1682
1997
  if (childName === 'serve') {
1683
- // Claude Code sets CLAUDE_PROJECT_DIR in the spawned MCP server's environment
1684
- // to the project root (code.claude.com/docs/en/mcp). Prefer it over cwd so the
1685
- // server indexes the right project even when Claude Code launches it with a
1686
- // different working directory. Falls back to cwd for a manual `cmk mcp serve`.
1687
- const projectRoot = resolvePath(process.env.CLAUDE_PROJECT_DIR ?? process.cwd());
1998
+ // Which project does this server serve? Claude Code sets CLAUDE_PROJECT_DIR in
1999
+ // the spawned server's env; otherwise fall back to the launch cwd (or walk up
2000
+ // to the nearest `context/`). The kiro-cli surface does NOT use MCP at all (its
2001
+ // agent sets includeMcpJson:false explicit memory goes through the `cmk
2002
+ // remember`/`cmk search` shell commands), so this server is the Claude Code +
2003
+ // Kiro IDE path. See resolveMcpProjectRoot in tier-paths.mjs.
2004
+ const projectRoot = resolveMcpProjectRoot();
1688
2005
  const userDir = process.env.MEMORY_KIT_USER_DIR ?? join(homedir(), '.claude-memory-kit');
1689
2006
  // ALL logs to stderr per design §10.1; stdout is reserved for
1690
2007
  // JSON-RPC messages handled by the SDK's StdioServerTransport.
@@ -1966,6 +2283,7 @@ export const subcommands = [
1966
2283
  description: 'cross-OS one-shot install — scaffold 3-tier dirs + inject .gitignore + drop kit CLAUDE.md block + wire Claude Code hooks',
1967
2284
  milestone: 3,
1968
2285
  optionSpec: [
2286
+ { flags: '--ide <agent>', description: 'target agent: claude-code (default) | kiro — wires that agent\'s hooks + MCP + instruction file' },
1969
2287
  { flags: '--force', description: 'allow downgrade of an existing newer-version CLAUDE.md block' },
1970
2288
  { flags: '--no-hooks', description: 'scaffold only; do NOT wire hooks into .claude/settings.json' },
1971
2289
  { flags: '--with-semantic', description: 'enable semantic recall: install the local embedder (~260 MB once), default search to hybrid, pre-warm the model' },
@@ -1974,10 +2292,20 @@ export const subcommands = [
1974
2292
  ],
1975
2293
  action: runInstall,
1976
2294
  },
2295
+ {
2296
+ name: 'hook',
2297
+ description: 'Kiro hook entrypoint — `cmk hook <agentSpawn|promptSubmit|stop>` (inject/capture; called by Kiro IDE + CLI hooks, not by users)',
2298
+ milestone: 50,
2299
+ argSpec: [{ flags: '<event>', description: 'the Kiro lifecycle event: agentSpawn | promptSubmit | stop' }],
2300
+ action: runHook,
2301
+ },
1977
2302
  {
1978
2303
  name: 'uninstall',
1979
- description: 'remove the CLAUDE.md kit block (preserves everything else byte-for-byte)',
2304
+ description: 'remove the kit-managed surface (preserves everything else byte-for-byte; never touches context/)',
1980
2305
  milestone: 4,
2306
+ optionSpec: [
2307
+ { flags: '--ide <agent>', description: 'which agent to uninstall: claude-code (default) | kiro — removes only THAT agent\'s managed surface' },
2308
+ ],
1981
2309
  action: runUninstall,
1982
2310
  },
1983
2311
  {
@@ -2003,6 +2331,7 @@ export const subcommands = [
2003
2331
  { flags: '--links <a,b>', description: 'rich: related fact names for [[cross-links]]' },
2004
2332
  { flags: '--from-file <path>', description: 'rich: read the fact as a JSON object from a file — shell-safe (content never touches argv; the safe way to capture backtick/quote-heavy Why/How). JSON keys: text (required), why, how, type, title, links. Self-contained — other flags are ignored.' },
2005
2333
  { flags: '--json', description: 'rich: read the fact as a JSON object from stdin (pipe-safe, shell-safe) — same JSON keys as --from-file' },
2334
+ { flags: '--project <dir>', description: 'project root to write to (default: cwd). Used by the kiro-cli agent — kiro-cli rejects a `cd … &&` prefix (Kiro #4579), so it passes the project root explicitly instead.' },
2006
2335
  ],
2007
2336
  action: runRemember,
2008
2337
  },
@@ -2019,6 +2348,7 @@ export const subcommands = [
2019
2348
  { flags: '--since <date>', description: 'ISO date — exclude observations older than this' },
2020
2349
  { flags: '--limit <n>', description: 'max results (default: 20)' },
2021
2350
  { flags: '--include-tombstoned', description: 'include deleted observations in results' },
2351
+ { flags: '--project <dir>', description: 'project root to search (default: cwd). Used by the kiro-cli agent (no `cd` prefix — Kiro #4579).' },
2022
2352
  ],
2023
2353
  action: runSearch,
2024
2354
  },
@@ -2222,13 +2552,14 @@ export const subcommands = [
2222
2552
  },
2223
2553
  {
2224
2554
  name: 'repair',
2225
- description: 'idempotent self-repair — re-register hooks, reset stale locks, rebuild index',
2555
+ description: 'idempotent self-repair — re-register hooks, reset stale locks, rebuild index, lint-clean memory',
2226
2556
  milestone: 39,
2227
2557
  optionSpec: [
2228
2558
  { flags: '--hooks', description: 're-register hooks from template (merges kit hooks into .claude/settings.json)' },
2229
2559
  { flags: '--locks', description: 'clear stale locks (>1h old by default)' },
2230
2560
  { flags: '--index', description: 'invoke `cmk reindex --full`' },
2231
- { flags: '--all', description: 'run all three repairs in order (default if no scope flag given)' },
2561
+ { flags: '--format', description: 'migrate committed memory markdown to the lint-clean format (DECISIONS.md headings)' },
2562
+ { flags: '--all', description: 'run all repairs in order (default if no scope flag given)' },
2232
2563
  ],
2233
2564
  action: runRepairCli,
2234
2565
  },
@@ -2305,7 +2636,9 @@ export const subcommands = [
2305
2636
  name: 'mcp',
2306
2637
  description: 'run the MCP server over stdio (invoked by Claude Code, not by humans)',
2307
2638
  milestone: 31,
2308
- children: [{ name: 'serve', description: 'start the stdio MCP server; JSON-RPC on stdin/stdout' }],
2639
+ children: [
2640
+ { name: 'serve', description: 'start the stdio MCP server; JSON-RPC on stdin/stdout' },
2641
+ ],
2309
2642
  action: runMcpDispatch,
2310
2643
  },
2311
2644
  {
@@ -9,11 +9,58 @@
9
9
  // resolveTierRoot({tier, projectRoot, userDir}) → absolute path
10
10
  // resolveFactDir(tier, tierRoot) → absolute path to <memory|fragments>
11
11
 
12
+ import { existsSync } from 'node:fs';
12
13
  import { homedir } from 'node:os';
13
- import { join } from 'node:path';
14
+ import { dirname, join, resolve } from 'node:path';
14
15
 
15
16
  export const VALID_TIERS = new Set(['U', 'P', 'L']);
16
17
 
18
+ /**
19
+ * Normalize a project path that may arrive in a unix/git-bash form. kiro-cli's
20
+ * model sometimes emits a `/c/Temp/proj` style path (git-bash) for `--project`,
21
+ * which Windows `resolve()` mangles. Convert a leading `/<drive>/` → `<DRIVE>:/`.
22
+ * A normal Windows or POSIX absolute path passes through unchanged. Used by
23
+ * `cmk remember`/`cmk search` --project (the kiro-cli explicit-memory path).
24
+ */
25
+ export function normalizeProjectPath(p) {
26
+ if (typeof p !== 'string') return p;
27
+ const m = /^\/([a-zA-Z])\/(.*)$/.exec(p); // /c/Temp/proj → c , Temp/proj
28
+ if (m) return `${m[1].toUpperCase()}:/${m[2]}`;
29
+ return p;
30
+ }
31
+
32
+ /**
33
+ * Resolve which project `cmk mcp serve` should serve (the Claude Code + Kiro IDE
34
+ * MCP path — kiro-cli doesn't use MCP). The MCP server is a long-lived child the
35
+ * agent launches, so it can't just trust cwd.
36
+ *
37
+ * Precedence: CLAUDE_PROJECT_DIR (Claude Code sets it in the spawned env) → walk
38
+ * UP from cwd to the nearest `context/` ancestor → cwd (last resort). Pure (env +
39
+ * cwd injected) so it's unit-testable without spawning.
40
+ *
41
+ * @param {object} [opts]
42
+ * @param {Record<string,string|undefined>} [opts.env=process.env]
43
+ * @param {string} [opts.cwd=process.cwd()]
44
+ * @returns {string} the resolved project root (absolute)
45
+ */
46
+ export function resolveMcpProjectRoot({ env = process.env, cwd = process.cwd() } = {}) {
47
+ const fromClaude = env.CLAUDE_PROJECT_DIR;
48
+ if (fromClaude && fromClaude.trim() !== '') return resolve(fromClaude);
49
+
50
+ // walk up from cwd to the nearest dir that has a context/ subdir (an installed
51
+ // kit project), stopping at the filesystem root.
52
+ let dir = resolve(cwd);
53
+ // eslint-disable-next-line no-constant-condition
54
+ while (true) {
55
+ if (existsSync(join(dir, 'context'))) return dir;
56
+ const parent = dirname(dir);
57
+ if (parent === dir) break; // reached the root
58
+ dir = parent;
59
+ }
60
+
61
+ return resolve(cwd); // last resort
62
+ }
63
+
17
64
  // Matches IDs produced by @lh8ppl/cmk-canonicalize.generateId(). Tier prefix +
18
65
  // 8 chars from the custom 32-char base32 alphabet that excludes the six
19
66
  // ambiguous chars (0, O, 1, l, I, 8). See design §3.1.
@@ -39,7 +39,7 @@ import { canonicalize } from '@lh8ppl/cmk-canonicalize';
39
39
  import { nowIso } from './audit-log.mjs';
40
40
  import { ERROR_CATEGORIES, errorResult } from './result-shapes.mjs';
41
41
  import { HaikuTimeoutError } from './compressor.mjs';
42
- import { compressWithRetry } from './compress-retry.mjs';
42
+ import { compressWithRetry, CEILING_FREE_TIMEOUT_MS, CEILING_FREE_BACKOFF_MS } from './compress-retry.mjs';
43
43
  import {
44
44
  DEFAULT_COOLDOWN_MS,
45
45
  isCooldownActive,
@@ -47,6 +47,7 @@ import {
47
47
  } from './cooldown.mjs';
48
48
  import { dailyDistill } from './daily-distill.mjs';
49
49
  import { autoPersona } from './auto-persona.mjs';
50
+ import { trimTrailingNewlines } from './managed-block.mjs';
50
51
  import { initUserTier } from './install.mjs';
51
52
  import { autoDrainQueues } from './auto-drain.mjs';
52
53
 
@@ -344,7 +345,8 @@ export async function weeklyCurate({
344
345
  // corpus (heavier than a session summary) and runs as a cron/lazy child
345
346
  // with no 60s hook ceiling — give the classifier headroom so a large
346
347
  // corpus doesn't time out. (Corpus is byte-capped at PERSONA_CORPUS_BYTES.)
347
- timeoutMs: 120_000,
348
+ // The shared ceiling-free timeout (D-92/F-2; was the original 120s literal).
349
+ timeoutMs: CEILING_FREE_TIMEOUT_MS,
348
350
  });
349
351
  }
350
352
 
@@ -400,9 +402,11 @@ export async function weeklyCurate({
400
402
  instructions,
401
403
  preserveCitationIds: true,
402
404
  maxOutputBytes: archiveMaxBytes,
403
- timeoutMs: 50_000,
405
+ // Ceiling-free → the generous timeout, NOT the hook-sized 50s (D-92/F-2 + D-179).
406
+ timeoutMs: CEILING_FREE_TIMEOUT_MS,
404
407
  },
405
- { maxAttempts: 2, onRetry: () => { retries += 1; } },
408
+ // 5s backoff so a retry lands after the slow-Haiku window, not inside it (D-179).
409
+ { maxAttempts: 2, baseBackoffMs: CEILING_FREE_BACKOFF_MS, onRetry: () => { retries += 1; } },
406
410
  );
407
411
  touchCooldownMarker({ projectRoot, now: ts });
408
412
  } catch (err) {
@@ -446,8 +450,11 @@ export async function weeklyCurate({
446
450
  // Append to archive.md (NOT overwrite — archive is append-only history).
447
451
  const archivePath = archiveMdPath(projectRoot);
448
452
  mkdirSync(join(projectRoot, ...SESSIONS_REL), { recursive: true });
449
- const suffix = dedupedOutput.endsWith('\n') ? '' : '\n';
450
- appendFileSync(archivePath, dedupedOutput + suffix + '\n', 'utf8');
453
+ // Lint-clean append (MD012 no-multiple-blanks): the old `dedupedOutput + suffix
454
+ // + '\n'` could yield THREE consecutive newlines when Haiku output already ended
455
+ // in a blank line. Trim trailing newlines (ReDoS-safe helper), then append
456
+ // exactly one blank-line separator (`\n\n`).
457
+ appendFileSync(archivePath, `${trimTrailingNewlines(dedupedOutput)}\n\n`, 'utf8');
451
458
 
452
459
  // Delete OLD today-*.md files (audit retention via git history;
453
460
  // committed tier per .gitignore.fragment).
@@ -1,6 +1,19 @@
1
1
  ---
2
2
  name: memory-search
3
- description: Searches the project's recorded memory (claude-memory-kit) — decisions, conventions, architecture, the reasoning behind choices, and where things live — and returns a curated, cited summary. Fire whenever the answer might be something the project already established in past work, HOWEVER the question is phrased — any prior decision, convention, rationale, or "how/where/why is it this way" question, including oblique or roundabout asks ("why is everything so spread out?", "remind me what we settled on for X", "how come these files are tiny?"). Also fire when a "[claude-memory-kit] Memory available" hint appears on the prompt. The examples are illustrative, not a checklist — prefer recalling over re-deriving an answer from the code. The session-start snapshot is a bounded index; this skill reaches the rest. Skip only when the question is purely about uncommitted or just-edited live code that memory cannot know, concerns this conversation only, or the user asked to ignore memory.
3
+ description: >-
4
+ Searches the project's recorded memory (claude-memory-kit) — decisions,
5
+ conventions, architecture, the reasoning behind choices, and where things live
6
+ — and returns a curated, cited summary. Fire whenever the answer might be
7
+ something the project already established in past work, HOWEVER the question is
8
+ phrased — any prior decision, convention, rationale, or "how/where/why is it
9
+ this way" question, including oblique or roundabout asks ("why is everything so
10
+ spread out?", "remind me what we settled on for X", "how come these files are
11
+ tiny?"). Also fire when a "[claude-memory-kit] Memory available" hint appears on
12
+ the prompt. The examples are illustrative, not a checklist — prefer recalling
13
+ over re-deriving an answer from the code. The session-start snapshot is a
14
+ bounded index; this skill reaches the rest. Skip only when the question is
15
+ purely about uncommitted or just-edited live code that memory cannot know,
16
+ concerns this conversation only, or the user asked to ignore memory.
4
17
  context: fork
5
18
  allowed-tools: mcp__cmk__mk_search mcp__cmk__mk_get mcp__cmk__mk_timeline mcp__cmk__mk_recent_activity Bash(cmk search *) Bash(cmk get *) Bash(cmk timeline *) Bash(cmk recent-activity *)
6
19
  ---