@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.
- package/README.md +6 -0
- package/bin/cmk-guard-memory.mjs +57 -0
- package/package.json +3 -2
- package/src/agent-profile.mjs +115 -0
- package/src/agent-profiles.mjs +118 -0
- package/src/auto-persona.mjs +4 -1
- package/src/compress-retry.mjs +25 -0
- package/src/compress-session.mjs +27 -3
- package/src/config-core.mjs +7 -9
- package/src/daily-distill.mjs +7 -3
- package/src/decisions-journal.mjs +71 -3
- package/src/doctor.mjs +86 -4
- package/src/guard-memory.mjs +151 -0
- package/src/import-anthropic-memory.mjs +15 -1
- package/src/inject-context.mjs +34 -3
- package/src/install-agent.mjs +220 -0
- package/src/install-kiro.mjs +287 -0
- package/src/install.mjs +16 -3
- package/src/kiro-cli-agent.mjs +270 -0
- package/src/kiro-constants.mjs +19 -0
- package/src/kiro-hook-bin.mjs +105 -0
- package/src/kiro-hook-command.mjs +67 -0
- package/src/kiro-hook-dispatch.mjs +115 -0
- package/src/kiro-ide-hooks.mjs +219 -0
- package/src/kiro-permissions.mjs +175 -0
- package/src/kiro-skills.mjs +96 -0
- package/src/kiro-transcript.mjs +366 -0
- package/src/kiro-trusted-commands.mjs +130 -0
- package/src/lazy-compress.mjs +6 -0
- package/src/managed-block.mjs +138 -0
- package/src/memory-write.mjs +23 -8
- package/src/mutate-agent-config.mjs +243 -0
- package/src/read-json.mjs +43 -0
- package/src/reindex.mjs +15 -2
- package/src/repair.mjs +39 -3
- package/src/result-shapes.mjs +8 -0
- package/src/review-queue.mjs +3 -0
- package/src/scratchpad.mjs +12 -2
- package/src/search.mjs +12 -5
- package/src/semantic-backend.mjs +7 -9
- package/src/settings-hooks.mjs +12 -2
- package/src/subcommands.mjs +360 -27
- package/src/tier-paths.mjs +48 -1
- package/src/weekly-curate.mjs +13 -6
- package/template/.claude/skills/memory-search/SKILL.md +14 -1
- package/template/.claude/skills/memory-write/SKILL.md +37 -1
- package/template/project/memory/INDEX.md.template +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
- **Explicit capture when you want it** — say "remember this" / "from now on" / "we decided" / "forget X" (the `memory-write` skill), or run `cmk remember "<fact>"`. Both dedup, screen for secrets, abstract machine paths to `~`, and write silently. For backtick/quote-heavy rich facts, capture them shell-safe as JSON: `cmk remember --from-file fact.json` (or `--json` from stdin) — content never touches the shell.
|
|
19
19
|
- **Search + MCP — Claude runs every memory op for you, in conversation** — `cmk search "<term>"` (keyword over facts + scratchpads; with the optional local embedder, **semantic + hybrid recall**: ask in your own words and get the fact even with zero keyword overlap — measured R@5 0.941 / paraphrase 1.000 on the kit's benchmark, no API calls). `cmk install` registers the kit's **MCP server**, so Claude can do the whole memory surface as tools without you ever typing `cmk`: capture (`mk_remember`, rich Why/How too), recall (`mk_search` / `mk_get` / `mk_timeline` / `mk_cite`), adjust trust (`mk_trust`), promote a fact across projects (`mk_lessons_promote`), forget (`mk_forget` — previews first, then deletes on confirm), and clear the review/conflict queues (`mk_queue_list` / `mk_queue_resolve`). The tools are allow-listed on install, so they run prompt-free.
|
|
20
20
|
- **Bounded by compression** — session → daily → weekly Haiku rollups (cron or lazy-on-read) keep the snapshot small as history grows. The session-buffer rollup self-heals at session start too, so memory stays bounded even if you never cleanly close the window.
|
|
21
|
+
- **Guards your memory from an accidental delete** — `cmk install` wires a `PreToolUse` hook (`cmk-guard-memory`) that **blocks** a destructive command (`rm`, `Remove-Item`, `del`, `git clean`, `git reset --hard`, `find … -delete`, `truncate`, `>`-truncate) the moment it's aimed at a memory path — *before* it runs, on both Claude Code (`Bash` / `PowerShell`) and Kiro (`execute_bash`). Fail-open (a broken guard never wedges your session) and intentionally broad (a false block is recoverable; a false allow is the data loss it prevents). A safe command, or a delete of anything else, runs untouched.
|
|
21
22
|
- **Don't start empty — import the rules you already own** — `cmk import-claude-md` parses an existing `CLAUDE.md` / `.cursorrules` / `AGENTS.md` into typed, searchable facts through the same safe write path (secret screening, sanitization, dedup), with provenance back to source file + line. `--dry-run` previews first.
|
|
22
23
|
- **Per-project, in-repo** — `context/` lives inside your project and travels with `git clone`. Each project keeps its own memory.
|
|
23
24
|
- **9 health checks** — `cmk doctor` validates hook wiring, distill freshness, transcript firing, INDEX consistency, cron registration, native-memory coexistence, stale locks, native-binding health (npm 12 readiness), and version drift (a project scaffold behind your installed `cmk` after an update) — each failure with a repair command.
|
|
@@ -33,6 +34,7 @@ npm install -g @lh8ppl/claude-memory-kit
|
|
|
33
34
|
cd ~/my-project
|
|
34
35
|
cmk install # scaffolds context/ + the memory-write + memory-search skills AND wires the lifecycle hooks into .claude/settings.json
|
|
35
36
|
cmk install --with-semantic # (optional) local semantic recall — one-time ~260 MB, search defaults to hybrid
|
|
37
|
+
cmk install --ide kiro # (optional) target Kiro instead — wires the IDE + kiro-cli surfaces (MCP + steering + AGENTS.md + skills + hooks)
|
|
36
38
|
cmk register-crons # (optional) scheduled background compression — otherwise self-heals lazily
|
|
37
39
|
cmk import-claude-md --yes # (optional) seed memory from an existing CLAUDE.md / .cursorrules (--dry-run previews)
|
|
38
40
|
cmk doctor # verify, then restart Claude Code
|
|
@@ -42,6 +44,10 @@ cmk doctor # verify, then restart Claude Code
|
|
|
42
44
|
|
|
43
45
|
> Installing the package globally adds the `cmk` CLI **and** the installer. It's the `cmk install` *subcommand* that wires the hooks — not the bare `npm install`.
|
|
44
46
|
|
|
47
|
+
**Other agents (Kiro).** `cmk install --ide kiro` targets [Kiro](https://kiro.dev) (AWS's agentic IDE + `kiro-cli`) instead of Claude Code — one command wires both its GUI hooks (`.kiro/hooks/`) and its terminal CLI agent (`~/.aws/amazonq/cli-agents/`), plus MCP, steering, skills, and an `AGENTS.md` instruction file. A project can carry **both** agents (run both installs — they share one `context/` brain and never clobber each other). Restart Kiro to load its hooks.
|
|
48
|
+
|
|
49
|
+
**Uninstall** is per-agent and conservative — it never deletes your `context/` memory: `cmk uninstall` removes the Claude Code surface; `cmk uninstall --ide kiro` removes the Kiro surface.
|
|
50
|
+
|
|
45
51
|
### Route B — Claude Code plugin marketplace
|
|
46
52
|
|
|
47
53
|
Inside Claude Code:
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PreToolUse hook handler — the memory delete-guardrail (D-192).
|
|
3
|
+
//
|
|
4
|
+
// Wired by `cmk install` as a Claude Code PreToolUse hook (matcher
|
|
5
|
+
// "Bash|PowerShell"). Reads the tool call on stdin and, if it's a destructive
|
|
6
|
+
// command aimed at a claude-memory-kit memory path (context/ , the persona
|
|
7
|
+
// tier, a memory file), BLOCKS it by exiting 2 — Claude Code shows the stderr
|
|
8
|
+
// reason to the model and the command never runs.
|
|
9
|
+
//
|
|
10
|
+
// Exit contract (Claude Code PreToolUse):
|
|
11
|
+
// exit 0 → allow (no opinion)
|
|
12
|
+
// exit 2 → BLOCK; stderr is surfaced as the reason
|
|
13
|
+
// Fail-OPEN: any load/parse error exits 0 — a broken guardrail must never wedge
|
|
14
|
+
// the session; it just stops guarding.
|
|
15
|
+
|
|
16
|
+
import { dirname, join } from 'node:path';
|
|
17
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = dirname(__filename);
|
|
21
|
+
const readHookStdinPath = join(__dirname, '..', 'src', 'read-hook-stdin.mjs');
|
|
22
|
+
const modulePath = join(__dirname, '..', 'src', 'guard-memory.mjs');
|
|
23
|
+
|
|
24
|
+
let readHookStdin;
|
|
25
|
+
let evaluatePayload;
|
|
26
|
+
try {
|
|
27
|
+
({ readHookStdin } = await import(pathToFileURL(readHookStdinPath).href));
|
|
28
|
+
({ evaluatePayload } = await import(pathToFileURL(modulePath).href));
|
|
29
|
+
} catch (err) {
|
|
30
|
+
process.stderr.write(`cmk-guard-memory: failed to load modules: ${err?.message ?? err}\n`);
|
|
31
|
+
process.exit(0); // fail-open
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Drain the hook payload — but not on an interactive TTY (a manual run), where
|
|
35
|
+
// a blocking stdin read would hang forever (the Task-101 lesson).
|
|
36
|
+
const raw = readHookStdin({ isTTY: process.stdin.isTTY });
|
|
37
|
+
|
|
38
|
+
let payload;
|
|
39
|
+
try {
|
|
40
|
+
payload = raw.trim() === '' ? {} : JSON.parse(raw);
|
|
41
|
+
} catch {
|
|
42
|
+
process.exit(0); // fail-open on unparseable input
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let verdict;
|
|
46
|
+
try {
|
|
47
|
+
verdict = evaluatePayload(payload);
|
|
48
|
+
} catch {
|
|
49
|
+
process.exit(0); // fail-open on any logic error
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (verdict && verdict.block) {
|
|
53
|
+
process.stderr.write(`${verdict.reason}\n`);
|
|
54
|
+
process.exit(2); // BLOCK
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
process.exit(0); // allow
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lh8ppl/claude-memory-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "cmk — the CLI for claude-memory-kit. Per-project, in-repo memory system for Claude Code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"cmk-capture-prompt": "./bin/cmk-capture-prompt.mjs",
|
|
13
13
|
"cmk-observe-edit": "./bin/cmk-observe-edit.mjs",
|
|
14
14
|
"cmk-capture-turn": "./bin/cmk-capture-turn.mjs",
|
|
15
|
-
"cmk-compress-session": "./bin/cmk-compress-session.mjs"
|
|
15
|
+
"cmk-compress-session": "./bin/cmk-compress-session.mjs",
|
|
16
|
+
"cmk-guard-memory": "./bin/cmk-guard-memory.mjs"
|
|
16
17
|
},
|
|
17
18
|
"files": [
|
|
18
19
|
"bin/",
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// agent-profile.mjs — the per-agent profile factory (Task 50.C).
|
|
2
|
+
//
|
|
3
|
+
// D-180: per-agent adapters are DATA, not classes. This factory validates +
|
|
4
|
+
// normalizes a profile DECLARATION so the install routing (Task 50.F) can drive
|
|
5
|
+
// ANY agent through ONE code path — the config legs (MCP registration, hook
|
|
6
|
+
// entry) go through the shared `mutateAgentConfig` primitive (Task 50.B); the
|
|
7
|
+
// instruction leg goes through the kit's marker-block machinery. The factory
|
|
8
|
+
// itself does NO I/O — it's a pure validator/normalizer that returns a frozen
|
|
9
|
+
// descriptor. A bad profile fails LOUD at definition time (throws), not at
|
|
10
|
+
// install time against a user's machine.
|
|
11
|
+
//
|
|
12
|
+
// Integration-type taxonomy (the claude-mem insight — the type dictates which
|
|
13
|
+
// legs an agent wires):
|
|
14
|
+
// native-hooks-mcp — instruction + MCP + lifecycle hooks (Claude Code, Kiro)
|
|
15
|
+
// hooks-mcp — instruction + MCP + hooks, hooks via a dedicated file
|
|
16
|
+
// (Cursor) — same required legs as native-hooks-mcp;
|
|
17
|
+
// the `hooks.mechanism` field distinguishes them
|
|
18
|
+
// mcp-only — instruction + MCP, NO hooks (Copilot/Warp/Roo/Goose)
|
|
19
|
+
// instruction-only — instruction file only, NO MCP, NO hooks (AGENTS.md rung)
|
|
20
|
+
//
|
|
21
|
+
// Public surface:
|
|
22
|
+
// defineAgentProfile(declaration) → frozen normalized descriptor (throws on invalid)
|
|
23
|
+
// INTEGRATION_TYPES — frozen list of the valid integrationType values
|
|
24
|
+
|
|
25
|
+
export const INTEGRATION_TYPES = Object.freeze([
|
|
26
|
+
'native-hooks-mcp',
|
|
27
|
+
'hooks-mcp',
|
|
28
|
+
'mcp-only',
|
|
29
|
+
'instruction-only',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const TYPES = new Set(INTEGRATION_TYPES);
|
|
33
|
+
|
|
34
|
+
// Which legs each type REQUIRES / FORBIDS. instructionFile is required by every
|
|
35
|
+
// type (the universal leg). mcp + hooks vary by type.
|
|
36
|
+
const TYPE_LEGS = Object.freeze({
|
|
37
|
+
'native-hooks-mcp': { mcp: 'required', hooks: 'required' },
|
|
38
|
+
'hooks-mcp': { mcp: 'required', hooks: 'required' },
|
|
39
|
+
'mcp-only': { mcp: 'required', hooks: 'forbidden' },
|
|
40
|
+
'instruction-only': { mcp: 'forbidden', hooks: 'forbidden' },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function fail(msg) {
|
|
44
|
+
throw new Error(`defineAgentProfile: ${msg}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Validate + normalize an agent profile declaration.
|
|
49
|
+
* @param {object} decl
|
|
50
|
+
* @returns {Readonly<object>} the frozen descriptor
|
|
51
|
+
*/
|
|
52
|
+
export function defineAgentProfile(decl) {
|
|
53
|
+
if (decl === null || typeof decl !== 'object') {
|
|
54
|
+
fail('declaration must be an object');
|
|
55
|
+
}
|
|
56
|
+
const { name, displayName, integrationType, detect, instructionFile, mcp, hooks, transcript } = decl;
|
|
57
|
+
|
|
58
|
+
// ── universal required fields ────────────────────────────────────────────
|
|
59
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
60
|
+
fail('name is required (non-empty string)');
|
|
61
|
+
}
|
|
62
|
+
if (!TYPES.has(integrationType)) {
|
|
63
|
+
fail(`integrationType must be one of: ${INTEGRATION_TYPES.join(', ')} (got ${JSON.stringify(integrationType)})`);
|
|
64
|
+
}
|
|
65
|
+
if (detect === null || typeof detect !== 'object') {
|
|
66
|
+
fail(`profile ${name}: detect descriptor is required (e.g. {homeDir:'.kiro'} | {command:'x'} | {always:true})`);
|
|
67
|
+
}
|
|
68
|
+
if (typeof instructionFile !== 'string' || instructionFile.length === 0) {
|
|
69
|
+
fail(`profile ${name}: instructionFile is required (every integration type wires the instruction leg)`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── per-type leg contract (the parity invariant 50.D will also enforce) ──
|
|
73
|
+
const legs = TYPE_LEGS[integrationType];
|
|
74
|
+
enforceLeg(name, integrationType, 'mcp', mcp, legs.mcp);
|
|
75
|
+
enforceLeg(name, integrationType, 'hooks', hooks, legs.hooks);
|
|
76
|
+
|
|
77
|
+
// ── shape checks on present legs ─────────────────────────────────────────
|
|
78
|
+
if (mcp !== undefined) {
|
|
79
|
+
if (typeof mcp.path !== 'string' || typeof mcp.serversKey !== 'string') {
|
|
80
|
+
fail(`profile ${name}: mcp requires {path, serversKey} strings`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (hooks !== undefined) {
|
|
84
|
+
if (typeof hooks.mechanism !== 'string') {
|
|
85
|
+
fail(`profile ${name}: hooks requires a {mechanism} string`);
|
|
86
|
+
}
|
|
87
|
+
if (hooks.eventMap === null || typeof hooks.eventMap !== 'object') {
|
|
88
|
+
fail(`profile ${name}: hooks requires an {eventMap} object`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── normalize + freeze ───────────────────────────────────────────────────
|
|
93
|
+
const descriptor = {
|
|
94
|
+
name,
|
|
95
|
+
displayName: displayName || name,
|
|
96
|
+
integrationType,
|
|
97
|
+
detect: Object.freeze({ ...detect }),
|
|
98
|
+
instructionFile,
|
|
99
|
+
...(mcp !== undefined ? { mcp: Object.freeze({ ...mcp }) } : {}),
|
|
100
|
+
...(hooks !== undefined
|
|
101
|
+
? { hooks: Object.freeze({ ...hooks, eventMap: Object.freeze({ ...hooks.eventMap }) }) }
|
|
102
|
+
: {}),
|
|
103
|
+
...(transcript !== undefined ? { transcript: Object.freeze({ ...transcript }) } : {}),
|
|
104
|
+
};
|
|
105
|
+
return Object.freeze(descriptor);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function enforceLeg(name, type, leg, value, requirement) {
|
|
109
|
+
if (requirement === 'required' && value === undefined) {
|
|
110
|
+
fail(`profile ${name}: integrationType '${type}' requires the ${leg} leg`);
|
|
111
|
+
}
|
|
112
|
+
if (requirement === 'forbidden' && value !== undefined) {
|
|
113
|
+
fail(`profile ${name}: integrationType '${type}' must NOT declare ${leg} (it over-wires its type)`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// agent-profiles.mjs — the per-agent profile REGISTRY (Task 50.C/50.E).
|
|
2
|
+
//
|
|
3
|
+
// Each agent is a pure-DATA profile built via defineAgentProfile (the D-180
|
|
4
|
+
// "data, not classes" seam). The kit's core (store / compression / search / CLI
|
|
5
|
+
// / MCP server) is identical across agents; only these declarations differ.
|
|
6
|
+
//
|
|
7
|
+
// Scope discipline (D-180): a plain object keyed by name, NOT a registry
|
|
8
|
+
// framework — single-digit agent count; opencode's data-row registry pays off
|
|
9
|
+
// at N≈75, premature here. Adding an agent in a later version = one more entry.
|
|
10
|
+
//
|
|
11
|
+
// Public surface:
|
|
12
|
+
// AGENT_PROFILES — frozen map { name → frozen descriptor }
|
|
13
|
+
// getAgentProfile(name) → descriptor | undefined
|
|
14
|
+
// listAgentProfiles() → descriptor[]
|
|
15
|
+
|
|
16
|
+
import { defineAgentProfile } from './agent-profile.mjs';
|
|
17
|
+
|
|
18
|
+
// ── Claude Code ──────────────────────────────────────────────────────────────
|
|
19
|
+
// The reference profile. Declares the SAME legs install.mjs wires today (the
|
|
20
|
+
// KIT_HOOKS_BLOCK in settings-hooks.mjs + the `cmk` MCP server in .mcp.json),
|
|
21
|
+
// expressed as data. install.mjs keeps using its existing Claude-Code path for
|
|
22
|
+
// v0.4.0 (regression-proof); this profile is the canonical declaration the
|
|
23
|
+
// routing (50.F) dispatches on and the parity validator (50.D) checks.
|
|
24
|
+
const claudeCode = defineAgentProfile({
|
|
25
|
+
name: 'claude-code',
|
|
26
|
+
displayName: 'Claude Code',
|
|
27
|
+
integrationType: 'native-hooks-mcp',
|
|
28
|
+
detect: { command: 'claude' },
|
|
29
|
+
// Claude Code's instruction surface is the managed block in CLAUDE.md.
|
|
30
|
+
instructionFile: 'CLAUDE.md',
|
|
31
|
+
mcp: { path: '.mcp.json', serversKey: 'mcpServers' },
|
|
32
|
+
hooks: {
|
|
33
|
+
mechanism: 'settings-json', // <projectRoot>/.claude/settings.json hooks[] entries
|
|
34
|
+
path: '.claude/settings.json',
|
|
35
|
+
// abstract lifecycle event → Claude Code's event name (here they coincide).
|
|
36
|
+
eventMap: {
|
|
37
|
+
sessionStart: 'SessionStart',
|
|
38
|
+
promptSubmit: 'UserPromptSubmit',
|
|
39
|
+
postEdit: 'PostToolUse',
|
|
40
|
+
turnEnd: 'Stop',
|
|
41
|
+
sessionEnd: 'SessionEnd',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
// Claude Code transcripts: ~/.claude/projects/<slug>/<session>.jsonl (JSONL).
|
|
45
|
+
transcript: { dir: '~/.claude/projects', workspaceKey: 'slug', parse: 'jsonl' },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// ── Kiro (Task 50.E) ─────────────────────────────────────────────────────────
|
|
49
|
+
// Primary-verified against kiro.dev + a real install (D-180). VS Code fork.
|
|
50
|
+
// Kiro wires BOTH hook surfaces (Task 50.N — full Claude-Code parity):
|
|
51
|
+
// • CLI agent-config (.kiro/agents/cmk.json "hooks") — the eventMap below;
|
|
52
|
+
// • IDE Agent-Hooks (.kiro/hooks/*.json v1 + legacy .kiro.hook) — kiro-ide-hooks.mjs.
|
|
53
|
+
// Both drive the SAME `cmk hook <event>` dispatcher → the same inject/capture/
|
|
54
|
+
// observe/guard cores; only the per-surface trigger names differ (CLI camelCase
|
|
55
|
+
// vs IDE v1 PascalCase). All four legs (inject/capture/observe-edit/delete-guard)
|
|
56
|
+
// are wired on both surfaces as of 50.N.1–50.N.3.
|
|
57
|
+
const kiro = defineAgentProfile({
|
|
58
|
+
name: 'kiro',
|
|
59
|
+
displayName: 'Kiro',
|
|
60
|
+
integrationType: 'native-hooks-mcp',
|
|
61
|
+
detect: { homeDir: '.kiro' },
|
|
62
|
+
// Steering file with `inclusion: always` frontmatter (applied at write time).
|
|
63
|
+
instructionFile: '.kiro/steering/claude-memory-kit.md',
|
|
64
|
+
mcp: { path: '.kiro/settings/mcp.json', serversKey: 'mcpServers' },
|
|
65
|
+
hooks: {
|
|
66
|
+
mechanism: 'agent-config-json', // .kiro/agents/<name>.json "hooks" object (CLI)
|
|
67
|
+
path: '.kiro/agents/cmk.json',
|
|
68
|
+
// The CLI agent-config trigger names. The IDE v1 surface uses PascalCase
|
|
69
|
+
// equivalents (UserPromptSubmit/Stop/PostToolUse/PreToolUse) — see
|
|
70
|
+
// kiro-ide-hooks.mjs. postEdit→postToolUse + the delete-guard (preToolUse)
|
|
71
|
+
// are both wired (50.N.2/50.N.3).
|
|
72
|
+
eventMap: {
|
|
73
|
+
sessionStart: 'agentSpawn',
|
|
74
|
+
promptSubmit: 'userPromptSubmit',
|
|
75
|
+
postEdit: 'postToolUse',
|
|
76
|
+
turnEnd: 'stop',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
// Transcript — TWO schemas (D-200), resolved at capture time by readKiroTurn:
|
|
80
|
+
// • IDE: per-session JSON under globalStorage, base64url(workspacePath), a
|
|
81
|
+
// `history[]` array (the dir/key/parse below);
|
|
82
|
+
// • kiro-CLI: ~/.kiro/sessions/cli/<uuid>.json, matched by `cwd`, a
|
|
83
|
+
// `session_state.conversation_metadata.user_turn_metadatas[]` shape (the
|
|
84
|
+
// fallback path — readKiroCliTurn). The fields here describe the IDE shape;
|
|
85
|
+
// the CLI shape is handled by the readKiroTurn fallback, not a profile field.
|
|
86
|
+
transcript: {
|
|
87
|
+
dir: 'globalStorage/kiro.kiroagent/workspace-sessions',
|
|
88
|
+
workspaceKey: 'base64url',
|
|
89
|
+
parse: 'json-history',
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ── AGENTS.md (Task 50.G — the instruction-only breadth rung) ────────────────
|
|
94
|
+
// The cheap multi-tool reach: emit a managed block in AGENTS.md (the cross-tool
|
|
95
|
+
// instruction-file convention several non-Claude agents read — Cursor, Zed,
|
|
96
|
+
// Codex, gemini-cli, …). NO hooks, NO MCP — instruction surface only. For tools
|
|
97
|
+
// we haven't built a full adapter for; a thin rung, not the depth play. (D-180 §5.)
|
|
98
|
+
const agentsmd = defineAgentProfile({
|
|
99
|
+
name: 'agents-md',
|
|
100
|
+
displayName: 'AGENTS.md',
|
|
101
|
+
integrationType: 'instruction-only',
|
|
102
|
+
detect: { always: true }, // any repo can carry an AGENTS.md
|
|
103
|
+
instructionFile: 'AGENTS.md',
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
export const AGENT_PROFILES = Object.freeze({
|
|
107
|
+
'claude-code': claudeCode,
|
|
108
|
+
kiro,
|
|
109
|
+
'agents-md': agentsmd,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export function getAgentProfile(name) {
|
|
113
|
+
return AGENT_PROFILES[name];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function listAgentProfiles() {
|
|
117
|
+
return Object.values(AGENT_PROFILES);
|
|
118
|
+
}
|
package/src/auto-persona.mjs
CHANGED
|
@@ -421,7 +421,10 @@ export function appendPersonaReviewQueue({ userDir, entries, now }) {
|
|
|
421
421
|
if (blocks.length === 0) return queuePath;
|
|
422
422
|
|
|
423
423
|
const header = `## ${ts} — persona-synthesis (pending review)`;
|
|
424
|
-
|
|
424
|
+
// Blank line below the heading (MD022). SAFE: parsePersonaReviewQueue keys on
|
|
425
|
+
// the bullet line + its i+1 meta-comment, not on heading adjacency; the blank
|
|
426
|
+
// is between heading and the first bullet, never inside the bullet↔comment pair.
|
|
427
|
+
appendFileSync(queuePath, `${header}\n\n${blocks.join('\n')}\n\n`, 'utf8');
|
|
425
428
|
return queuePath;
|
|
426
429
|
}
|
|
427
430
|
|
package/src/compress-retry.mjs
CHANGED
|
@@ -37,6 +37,31 @@
|
|
|
37
37
|
// a time, gated by the cooldown), so there is no herd to avoid — a plain exponential
|
|
38
38
|
// backoff is sufficient and keeps the timing deterministic for tests.
|
|
39
39
|
|
|
40
|
+
// The timeout for compress callers that have NO outer hook ceiling — the cron /
|
|
41
|
+
// detached-lazy children (daily-distill, weekly-curate, the lazy compressSession).
|
|
42
|
+
// The D-92/F-2 composition rule: a ceiling-free caller must NOT inherit the
|
|
43
|
+
// hook-sized 50s bound (which is sized under the 60s SessionEnd ceiling, §8.5).
|
|
44
|
+
// 120s is chosen against MEASURED `claude --print` latency: it runs ~18-27s when
|
|
45
|
+
// fast but was observed at 78s (Task 163 live, on a 4.7KB input) and 89s (the v0.3.4
|
|
46
|
+
// cut-gate, 10KB) in slow-Haiku windows — environmental, not size-driven (D-174).
|
|
47
|
+
// 120s clears those with headroom; the 50s budget killed them needlessly, leaving
|
|
48
|
+
// `recent.md` stale (D-179). One constant so the family can't drift back to 50s.
|
|
49
|
+
// (auto-extract uses its own 90s under the Stop hook — a separate detached path.)
|
|
50
|
+
export const CEILING_FREE_TIMEOUT_MS = 120_000;
|
|
51
|
+
|
|
52
|
+
// The backoff BETWEEN retries on the ceiling-free paths. The default baseBackoffMs
|
|
53
|
+
// (600ms) is far too short for the kit's failure mode: `claude --print` slowness is
|
|
54
|
+
// a transient WINDOW (slow for a stretch, then fine — D-174), and the whole point of
|
|
55
|
+
// backoff is to let that window PASS before retrying. A 600ms wait retries while
|
|
56
|
+
// still INSIDE the same slow window, so attempt 2 hits the same slowness and also
|
|
57
|
+
// times out. The field waits SECONDS for exactly this reason (graphiti 5-120s,
|
|
58
|
+
// Letta cap 10s, mempalace 2-8s — all checked across 19 systems; NONE use sub-second
|
|
59
|
+
// backoff, and NONE escalate the timeout itself). 5s is the field's low end — one
|
|
60
|
+
// 5s wait between the 2 ceiling-free attempts gives the slow window room to clear.
|
|
61
|
+
// Safe on every path: the HOOK path is maxAttempts:1 (no retry → backoff never
|
|
62
|
+
// fires); the ceiling-free paths run detached/cron, so a multi-second wait is free.
|
|
63
|
+
export const CEILING_FREE_BACKOFF_MS = 5_000;
|
|
64
|
+
|
|
40
65
|
/**
|
|
41
66
|
* Classify a compress() rejection as transient (worth a retry) or deterministic
|
|
42
67
|
* (a re-call re-fails identically — don't waste the attempt or the budget).
|
package/src/compress-session.mjs
CHANGED
|
@@ -206,10 +206,22 @@ function restoreRolling(projectRoot, rollingPath) {
|
|
|
206
206
|
function appendToTodayMd({ projectRoot, date, body }) {
|
|
207
207
|
const path = todayMdPath(projectRoot, date);
|
|
208
208
|
mkdirSync(dirname(path), { recursive: true });
|
|
209
|
+
// Lint-clean append (MD022 blanks-around-headings): a same-day re-append puts
|
|
210
|
+
// the new block's leading `## ` heading right after the prior block, with only
|
|
211
|
+
// the prior block's single trailing `\n` above it → heading not blank-
|
|
212
|
+
// surrounded. Guarantee exactly one blank line ABOVE the new block when the
|
|
213
|
+
// file already has content.
|
|
214
|
+
let prefix = '';
|
|
215
|
+
if (existsSync(path)) {
|
|
216
|
+
const existing = readFileSync(path, 'utf8');
|
|
217
|
+
if (existing.trim() !== '') {
|
|
218
|
+
prefix = existing.endsWith('\n\n') ? '' : existing.endsWith('\n') ? '\n' : '\n\n';
|
|
219
|
+
}
|
|
220
|
+
}
|
|
209
221
|
// Append with a trailing newline so successive same-day appends
|
|
210
222
|
// don't collide on a missing terminator.
|
|
211
223
|
const suffix = body.endsWith('\n') ? '' : '\n';
|
|
212
|
-
appendFileSync(path, body + suffix, 'utf8');
|
|
224
|
+
appendFileSync(path, prefix + body + suffix, 'utf8');
|
|
213
225
|
return path;
|
|
214
226
|
}
|
|
215
227
|
|
|
@@ -232,6 +244,16 @@ export async function compressSession({
|
|
|
232
244
|
// caller (runLazyCompress) passes maxAttempts:2 to opt into one retry; the hook
|
|
233
245
|
// keeps its restore-on-failure (D-79) and delegates the retry to that lazy path.
|
|
234
246
|
maxAttempts = 1,
|
|
247
|
+
// DEFAULT 50s = the SessionEnd-hook budget (sized under the 60s ceiling, §8.5).
|
|
248
|
+
// The ceiling-free LAZY caller (runLazyCompress, a detached SessionStart child
|
|
249
|
+
// with NO outer ceiling) passes 120s so a slow-but-not-broken `claude --print`
|
|
250
|
+
// window doesn't time out needlessly — the D-92/F-2 composition rule: a
|
|
251
|
+
// ceiling-free caller must not inherit a ceiling-sized timeout.
|
|
252
|
+
timeoutMs = 50_000,
|
|
253
|
+
// Backoff between retries (only the lazy maxAttempts:2 path retries). DEFAULT
|
|
254
|
+
// undefined → compressWithRetry's 600ms; the ceiling-free LAZY caller passes the
|
|
255
|
+
// 5s ceiling-free backoff so a retry lands AFTER the slow-Haiku window (D-179).
|
|
256
|
+
baseBackoffMs,
|
|
235
257
|
} = {}) {
|
|
236
258
|
const ts = now ?? nowIso();
|
|
237
259
|
const date = dateFromIso(ts);
|
|
@@ -343,9 +365,11 @@ export async function compressSession({
|
|
|
343
365
|
instructions,
|
|
344
366
|
preserveCitationIds: true,
|
|
345
367
|
maxOutputBytes,
|
|
346
|
-
timeoutMs
|
|
368
|
+
timeoutMs,
|
|
347
369
|
},
|
|
348
|
-
|
|
370
|
+
// baseBackoffMs only forwarded when the caller set it (the lazy ceiling-free
|
|
371
|
+
// path passes the 5s backoff); undefined → compressWithRetry's 600ms default.
|
|
372
|
+
{ maxAttempts, ...(baseBackoffMs != null ? { baseBackoffMs } : {}), onRetry: () => { retries += 1; } },
|
|
349
373
|
);
|
|
350
374
|
} catch (err) {
|
|
351
375
|
// Distinguish HAIKU_TIMEOUT (slow Anthropic) from COMPRESS_FAILED
|
package/src/config-core.mjs
CHANGED
|
@@ -21,9 +21,10 @@
|
|
|
21
21
|
// (observations have their own provenance/shadowed_by surface, §6); this is
|
|
22
22
|
// the concrete settings half the semantic default forced into existence.
|
|
23
23
|
|
|
24
|
-
import {
|
|
24
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
25
25
|
import { dirname, join } from 'node:path';
|
|
26
26
|
import { resolveTierRoot } from './tier-paths.mjs';
|
|
27
|
+
import { parseJsonFile } from './read-json.mjs';
|
|
27
28
|
|
|
28
29
|
// Highest-precedence first.
|
|
29
30
|
const TIERS = Object.freeze([
|
|
@@ -47,14 +48,11 @@ function settingsPathFor(tierName, { projectRoot, userDir }) {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
function readSettings(path) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// throw on a read (a hand-broken JSON shouldn't crash `cmk config get`).
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
51
|
+
// BOM-tolerant (parseJsonFile): a settings.json written by a Windows editor
|
|
52
|
+
// carries a UTF-8 BOM that a bare JSON.parse would reject (D-187). A missing
|
|
53
|
+
// OR malformed file resolves to null — never throw on a read (a hand-broken
|
|
54
|
+
// JSON shouldn't crash `cmk config get`).
|
|
55
|
+
return parseJsonFile(path, { fallback: null });
|
|
58
56
|
}
|
|
59
57
|
|
|
60
58
|
// Walk a dotted path; returns {found, value}. `found` distinguishes a key
|
package/src/daily-distill.mjs
CHANGED
|
@@ -28,7 +28,7 @@ import { join } from 'node:path';
|
|
|
28
28
|
import { nowIso } from './audit-log.mjs';
|
|
29
29
|
import { ERROR_CATEGORIES } from './result-shapes.mjs';
|
|
30
30
|
import { HaikuTimeoutError } from './compressor.mjs';
|
|
31
|
-
import { compressWithRetry } from './compress-retry.mjs';
|
|
31
|
+
import { compressWithRetry, CEILING_FREE_TIMEOUT_MS, CEILING_FREE_BACKOFF_MS } from './compress-retry.mjs';
|
|
32
32
|
import {
|
|
33
33
|
DEFAULT_COOLDOWN_MS,
|
|
34
34
|
isCooldownActive,
|
|
@@ -209,9 +209,13 @@ export async function dailyDistill({
|
|
|
209
209
|
instructions,
|
|
210
210
|
preserveCitationIds: true,
|
|
211
211
|
maxOutputBytes,
|
|
212
|
-
|
|
212
|
+
// Ceiling-free (cron / detached lazy child, NO 60s hook ceiling) → the
|
|
213
|
+
// generous ceiling-free timeout, NOT the hook-sized 50s (D-92/F-2 + D-179).
|
|
214
|
+
timeoutMs: CEILING_FREE_TIMEOUT_MS,
|
|
213
215
|
},
|
|
214
|
-
|
|
216
|
+
// 5s backoff between the 2 attempts (NOT the 600ms default) so a retry lands
|
|
217
|
+
// AFTER the transient slow-Haiku window, not inside it (D-179).
|
|
218
|
+
{ maxAttempts: 2, baseBackoffMs: CEILING_FREE_BACKOFF_MS, onRetry: () => { retries += 1; } },
|
|
215
219
|
);
|
|
216
220
|
touchCooldownMarker({ projectRoot, now: ts });
|
|
217
221
|
} catch (err) {
|
|
@@ -33,6 +33,7 @@ import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
|
33
33
|
import { join } from 'node:path';
|
|
34
34
|
import { parse as parseFrontmatter } from './frontmatter.mjs';
|
|
35
35
|
import { ID_PATTERN } from './tier-paths.mjs';
|
|
36
|
+
import { trimTrailingNewlines } from './managed-block.mjs';
|
|
36
37
|
|
|
37
38
|
export const DECISIONS_HEADER =
|
|
38
39
|
'# Decisions\n\n' +
|
|
@@ -62,9 +63,15 @@ function dateOnly(iso) {
|
|
|
62
63
|
*/
|
|
63
64
|
export function buildDecisionEntry(f) {
|
|
64
65
|
const date = dateOnly(f.createdAt);
|
|
66
|
+
// Blank lines around the `###` heading so the committed journal is lint-clean
|
|
67
|
+
// markdown (markdownlint MD022 "blanks-around-headings"). The HTML marker, the
|
|
68
|
+
// heading, and the When/Why block are each separated by a blank line — the
|
|
69
|
+
// generated memory passes a strict linter by construction, not by exemption.
|
|
65
70
|
const lines = [
|
|
66
71
|
markerFor(f.id),
|
|
67
|
-
|
|
72
|
+
'',
|
|
73
|
+
`## ${f.title}`,
|
|
74
|
+
'',
|
|
68
75
|
`**When:** ${date} · **Fact:** \`${f.id}\``,
|
|
69
76
|
];
|
|
70
77
|
if (f.why && String(f.why).trim()) {
|
|
@@ -129,8 +136,12 @@ export function updateDecisionsJournal({ existingContent = '', facts = [], tombs
|
|
|
129
136
|
// from attaching the retraction note to the NEXT entry's heading.
|
|
130
137
|
const nextMarker = content.indexOf('<!-- decision:', idx + marker.length);
|
|
131
138
|
const spanEnd = nextMarker === -1 ? content.length : nextMarker;
|
|
132
|
-
// Find this entry's heading line (the
|
|
133
|
-
|
|
139
|
+
// Find this entry's heading line (the `## …` after the marker, within span).
|
|
140
|
+
// Anchor on a line-start `## ` (newline-prefixed) so body text containing
|
|
141
|
+
// `##` can't be mistaken for the heading. buildDecisionEntry emits the
|
|
142
|
+
// heading on its own line right after the marker + a blank line.
|
|
143
|
+
const headingNl = content.indexOf('\n## ', idx);
|
|
144
|
+
const headingStart = headingNl === -1 ? -1 : headingNl + 1;
|
|
134
145
|
if (headingStart === -1 || headingStart >= spanEnd) continue;
|
|
135
146
|
const headingEnd = content.indexOf('\n', headingStart);
|
|
136
147
|
if (headingEnd === -1) continue;
|
|
@@ -145,6 +156,63 @@ export function updateDecisionsJournal({ existingContent = '', facts = [], tombs
|
|
|
145
156
|
return content;
|
|
146
157
|
}
|
|
147
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Migrate an OLD-format journal (pre-Task-164.1: `### title` headings with no
|
|
161
|
+
* blank lines around them) to the lint-clean shape buildDecisionEntry now emits
|
|
162
|
+
* (`## title` with a blank line above + below). Append-only-safe: every entry +
|
|
163
|
+
* its content is preserved; only the heading level + surrounding blank lines
|
|
164
|
+
* change. Idempotent (already-clean content returns unchanged) and CRLF-tolerant
|
|
165
|
+
* (re-emits `\n`). Line-based (no backtracking regex — the ReDoS-safe discipline).
|
|
166
|
+
*
|
|
167
|
+
* The retraction tag (`_(retracted …)_`) sits on the line directly under the
|
|
168
|
+
* heading; it stays there (the blank-below goes AFTER the tag, matching the
|
|
169
|
+
* updateDecisionsJournal inserter).
|
|
170
|
+
*
|
|
171
|
+
* @param {string} content the DECISIONS.md content
|
|
172
|
+
* @returns {string} the normalized content (idempotent)
|
|
173
|
+
*/
|
|
174
|
+
export function normalizeDecisionsJournal(content) {
|
|
175
|
+
if (typeof content !== 'string' || content === '') return content;
|
|
176
|
+
const lines = content.split(/\r?\n/);
|
|
177
|
+
const out = [];
|
|
178
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
179
|
+
const line = lines[i];
|
|
180
|
+
// A decision entry begins with the machine marker. Normalize the heading
|
|
181
|
+
// that follows it (skipping any blank lines already present).
|
|
182
|
+
if (/^<!-- decision:/.test(line)) {
|
|
183
|
+
out.push(line);
|
|
184
|
+
// ensure exactly one blank line after the marker
|
|
185
|
+
let j = i + 1;
|
|
186
|
+
while (j < lines.length && lines[j].trim() === '') j += 1; // skip existing blanks
|
|
187
|
+
out.push('');
|
|
188
|
+
// the heading line (### or ## title) — demote ### -> ##
|
|
189
|
+
if (j < lines.length && /^#{2,3}\s/.test(lines[j])) {
|
|
190
|
+
const headingText = lines[j].replace(/^#{2,3}\s+/, '');
|
|
191
|
+
out.push(`## ${headingText}`);
|
|
192
|
+
let k = j + 1;
|
|
193
|
+
// an optional retraction tag stays directly under the heading
|
|
194
|
+
if (k < lines.length && lines[k].trim().startsWith('_(retracted')) {
|
|
195
|
+
out.push(lines[k]);
|
|
196
|
+
k += 1;
|
|
197
|
+
}
|
|
198
|
+
// ensure exactly one blank line below the heading (or heading+tag)
|
|
199
|
+
while (k < lines.length && lines[k].trim() === '') k += 1; // collapse existing blanks
|
|
200
|
+
out.push('');
|
|
201
|
+
i = k - 1; // continue after the blank we just normalized
|
|
202
|
+
} else {
|
|
203
|
+
// malformed entry (marker with no heading) — leave the rest as-is
|
|
204
|
+
i = j - 1;
|
|
205
|
+
}
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
out.push(line);
|
|
209
|
+
}
|
|
210
|
+
// collapse any accidental >1 trailing blank, keep a single trailing newline
|
|
211
|
+
let result = out.join('\n');
|
|
212
|
+
result = trimTrailingNewlines(result) + '\n';
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
|
|
148
216
|
// --- File-IO orchestration (the impure shell over the pure core) ----------
|
|
149
217
|
|
|
150
218
|
// Leading indent is [ \t]* (NOT \s*) so it can't match the newline the
|