@lh8ppl/claude-memory-kit 0.3.5 → 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-session.mjs +13 -1
- package/src/config-core.mjs +7 -9
- 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/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 +6 -2
- 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-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
|
|
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
|
|
@@ -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
|
package/src/doctor.mjs
CHANGED
|
@@ -48,6 +48,8 @@ import { checkKitBinding, checkEmbedderBinding } from './native-binding.mjs';
|
|
|
48
48
|
import { resolveDefaultSearchMode } from './semantic-backend.mjs';
|
|
49
49
|
import { checkVersionDrift } from './version-drift.mjs';
|
|
50
50
|
import { getKitVersion } from './install.mjs';
|
|
51
|
+
import { hasOurCliAgent } from './kiro-cli-agent.mjs';
|
|
52
|
+
import { stripBom } from './read-json.mjs';
|
|
51
53
|
|
|
52
54
|
const TWO_DAYS_MS = 2 * 24 * 60 * 60 * 1000;
|
|
53
55
|
const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;
|
|
@@ -59,9 +61,41 @@ const MEMORY_DIR_REL = ['context', 'memory'];
|
|
|
59
61
|
const LOCKS_REL = ['context', '.locks'];
|
|
60
62
|
const NATIVE_MEMORY_LOG_REL = ['context', '.locks', 'native-memory-status.log'];
|
|
61
63
|
|
|
64
|
+
// Which agent was this project installed for? A `--ide kiro` install wires
|
|
65
|
+
// .kiro/ surfaces (hooks + steering/cmk.md + skills + settings/mcp.json); a
|
|
66
|
+
// Claude Code install wires .claude/settings.json. HC-1 must check the RIGHT
|
|
67
|
+
// surface — before v0.4.0 it hard-checked .claude/settings.json and false-FAILed
|
|
68
|
+
// on every Kiro install (the cut-gate-kiro live-test find, Task 50).
|
|
69
|
+
//
|
|
70
|
+
// Detection precedence:
|
|
71
|
+
// 1. .claude/settings.json present → Claude Code.
|
|
72
|
+
// 2. a CMK-OWNED Kiro marker present (.kiro/steering/cmk.md — written by every
|
|
73
|
+
// installKiro run) → Kiro. We key on OUR marker, not a bare `.kiro/` dir, so
|
|
74
|
+
// a stray/unrelated .kiro/ (another tool's, or a partial non-cmk dir) does
|
|
75
|
+
// NOT flip the project to the Kiro path (I2).
|
|
76
|
+
// 3. neither → Claude Code (historical default — a not-yet-installed project
|
|
77
|
+
// still reports the Claude-shaped repair hint).
|
|
78
|
+
//
|
|
79
|
+
// NOTE (I1, deliberate punt): a project installed for BOTH agents (.claude AND a
|
|
80
|
+
// cmk .kiro marker) resolves to Claude Code by precedence — HC-1 checks only the
|
|
81
|
+
// Claude surface. Dual-install is rare; the single-surface check is intentional
|
|
82
|
+
// for v0.4.0, not an oversight. Revisit if dual-install becomes common.
|
|
83
|
+
function detectInstallKind(projectRoot) {
|
|
84
|
+
if (existsSync(join(projectRoot, '.claude', 'settings.json'))) return 'claude-code';
|
|
85
|
+
if (existsSync(join(projectRoot, '.kiro', 'steering', 'cmk.md'))) return 'kiro';
|
|
86
|
+
return 'claude-code';
|
|
87
|
+
}
|
|
88
|
+
|
|
62
89
|
// --- HC-1: Stop + SessionStart hooks registered -----------------------
|
|
63
|
-
function hc1Hooks({ projectRoot }) {
|
|
64
|
-
//
|
|
90
|
+
function hc1Hooks({ projectRoot, awsDir }) {
|
|
91
|
+
// Agent-aware (v0.4.0): a Kiro install keeps its hooks in .kiro/hooks/ (IDE)
|
|
92
|
+
// and/or ~/.kiro/agents/ (kiro-cli), so route to the Kiro check
|
|
93
|
+
// rather than false-failing on a missing .claude/settings.json with a
|
|
94
|
+
// Claude-Code repair hint.
|
|
95
|
+
if (detectInstallKind(projectRoot) === 'kiro') {
|
|
96
|
+
return hc1KiroHooks({ projectRoot, awsDir });
|
|
97
|
+
}
|
|
98
|
+
// Per design §5 — the Claude Code hooks live in .claude/settings.json
|
|
65
99
|
// alongside its plugin manifest. Required for auto-extract +
|
|
66
100
|
// session-end compression to fire.
|
|
67
101
|
const settingsPath = join(projectRoot, '.claude', 'settings.json');
|
|
@@ -76,7 +110,9 @@ function hc1Hooks({ projectRoot }) {
|
|
|
76
110
|
}
|
|
77
111
|
let settings;
|
|
78
112
|
try {
|
|
79
|
-
|
|
113
|
+
// stripBom: a Windows-editor BOM on .claude/settings.json must not make a
|
|
114
|
+
// valid file read as a parse error → false HC-1 FAIL (D-187).
|
|
115
|
+
settings = JSON.parse(stripBom(readFileSync(settingsPath, 'utf8')));
|
|
80
116
|
} catch (err) {
|
|
81
117
|
return {
|
|
82
118
|
id: 'HC-1',
|
|
@@ -141,6 +177,51 @@ function hc1Hooks({ projectRoot }) {
|
|
|
141
177
|
};
|
|
142
178
|
}
|
|
143
179
|
|
|
180
|
+
// --- HC-1 (Kiro variant): capture + inject can fire via EITHER Kiro surface ----
|
|
181
|
+
// Kiro wires capture/inject through TWO independent surfaces (D-186):
|
|
182
|
+
// • IDE hooks → .kiro/hooks/{cmk-capture,cmk-inject}.kiro.hook (the GUI user)
|
|
183
|
+
// • CLI agent → ~/.kiro/agents/cmk.json with
|
|
184
|
+
// agentSpawn(inject)+stop(capture) hooks (the kiro-cli user)
|
|
185
|
+
// HC-1 is a CAPABILITY check ("can capture/inject fire?"), not a single-file
|
|
186
|
+
// check — so it PASSES if EITHER surface is present, and FAILs only when NEITHER
|
|
187
|
+
// is. The original v0.4.0 fix checked only the IDE hooks, which false-FAILed a
|
|
188
|
+
// working kiro-cli-only install (the surface lives in ~/.aws, which is also
|
|
189
|
+
// machine-local and doesn't travel with a clone) — the same separately-correct-
|
|
190
|
+
// jointly-broken class as D-184/D-185, one level down (skill-review B1).
|
|
191
|
+
// `awsDir` is injectable so tests can sandbox the ~/.aws probe.
|
|
192
|
+
function hc1KiroHooks({ projectRoot, awsDir }) {
|
|
193
|
+
const hooksDir = join(projectRoot, '.kiro', 'hooks');
|
|
194
|
+
const ideHooks = ['cmk-capture.kiro.hook', 'cmk-inject.kiro.hook'];
|
|
195
|
+
const ideMissing = ideHooks.filter((f) => !existsSync(join(hooksDir, f)));
|
|
196
|
+
const ideComplete = ideMissing.length === 0;
|
|
197
|
+
const cliAgent = hasOurCliAgent({ awsDir });
|
|
198
|
+
|
|
199
|
+
if (ideComplete || cliAgent) {
|
|
200
|
+
const surfaces = [];
|
|
201
|
+
if (ideComplete) surfaces.push('IDE hooks (.kiro/hooks/)');
|
|
202
|
+
if (cliAgent) surfaces.push('CLI agent (~/.kiro/agents/cmk.json)');
|
|
203
|
+
return {
|
|
204
|
+
id: 'HC-1',
|
|
205
|
+
name: 'Stop + SessionStart hooks registered',
|
|
206
|
+
status: 'pass',
|
|
207
|
+
message: `Kiro capture/inject wired via ${surfaces.join(' + ')}`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Neither surface present → genuinely not wired. Name both so a kiro-cli user
|
|
212
|
+
// isn't pushed at the IDE-only repair.
|
|
213
|
+
return {
|
|
214
|
+
id: 'HC-1',
|
|
215
|
+
name: 'Stop + SessionStart hooks registered',
|
|
216
|
+
status: 'fail',
|
|
217
|
+
message:
|
|
218
|
+
`Kiro install: no capture/inject surface found — neither the IDE hooks ` +
|
|
219
|
+
`(${ideMissing.join(', ')} in .kiro/hooks/) nor a cmk CLI agent in ` +
|
|
220
|
+
`~/.kiro/agents/`,
|
|
221
|
+
recoveryCommand: 'cmk install --ide kiro',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
144
225
|
// --- HC-2: distill freshness (≤2 days) --------------------------------
|
|
145
226
|
function hc2DistillFreshness({ projectRoot, now }) {
|
|
146
227
|
const recentPath = join(projectRoot, ...RECENT_MD_REL);
|
|
@@ -566,6 +647,7 @@ export async function runDoctor({
|
|
|
566
647
|
kitVersion,
|
|
567
648
|
kitBindingProbe,
|
|
568
649
|
embedderBindingProbe,
|
|
650
|
+
awsDir, // injectable: sandboxes the HC-1 Kiro CLI-agent (~/.aws) probe in tests
|
|
569
651
|
} = {}) {
|
|
570
652
|
const t0 = Date.now();
|
|
571
653
|
if (!projectRoot) {
|
|
@@ -580,7 +662,7 @@ export async function runDoctor({
|
|
|
580
662
|
const resolvedUserDir = userDir ?? join(homedir(), '.claude-memory-kit');
|
|
581
663
|
|
|
582
664
|
// Run all checks in order.
|
|
583
|
-
const c1 = hc1Hooks({ projectRoot });
|
|
665
|
+
const c1 = hc1Hooks({ projectRoot, awsDir });
|
|
584
666
|
const c2 = hc2DistillFreshness({ projectRoot, now: ts });
|
|
585
667
|
const c3 = hc3Transcripts({ projectRoot, now: ts });
|
|
586
668
|
const c4 = hc4IndexConsistency({ projectRoot });
|