feat(skill): ck — persistent per-project memory for Claude Code#959
feat(skill): ck — persistent per-project memory for Claude Code#959affaan-m merged 3 commits intoaffaan-m:mainfrom
Conversation
Adds the ck (Context Keeper) skill — deterministic Node.js scripts that give Claude Code persistent, per-project memory across sessions. Architecture: - commands/ — 8 Node.js scripts handle all command logic (init, save, resume, info, list, forget, migrate, shared). Claude calls scripts and displays output — no LLM interpretation of command logic. - hooks/session-start.mjs — injects ~100 token compact summary on session start (not kilobytes). Detects unsaved sessions, git activity since last save, goal mismatch vs CLAUDE.md. - context.json as source of truth — CONTEXT.md is generated from it. Full session history, session IDs, git activity per save. Commands: /ck:init /ck:save /ck:resume /ck:info /ck:list /ck:forget /ck:migrate Source: https://github.com/sreedhargs89/context-keeper Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Analysis Failed
Troubleshooting
Retry: |
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughAdds "Context Keeper v2" (ck): a per-project persistent memory system. Introduces CLI scripts (init, save, resume, info, list, forget, migrate), shared utilities, a SessionStart hook, and an on-disk layout under ~/.claude/ck/ with Changes
Sequence Diagram(s)sequenceDiagram
participant User as Claude/User
participant CLI as ck CLI (node scripts)
participant FS as File System (~/.claude/ck/)
participant Git as Git Repo
rect rgba(100, 150, 255, 0.5)
Note over User,CLI: /ck:init
User->>CLI: run /ck:init
CLI->>FS: read project files (package.json, CLAUDE.md, README, .git)
CLI->>FS: read/write projects.json
CLI-->>User: JSON metadata output
end
rect rgba(100, 200, 100, 0.5)
Note over User,Git: /ck:save
User->>CLI: /ck:save (LLM draft -> JSON stdin)
CLI->>FS: load projects.json & context.json
CLI->>Git: gitSummary(since lastSession)
CLI->>FS: write context.json, CONTEXT.md, native memory
CLI-->>User: success confirmation (session id, git summary)
end
rect rgba(200, 100, 100, 0.5)
Note over User,CLI: SessionStart Hook
User->>CLI: session-start hook (stdin session_id)
CLI->>FS: read projects.json, current-session.json, context.json, CLAUDE.md
CLI->>Git: git log since lastSessionDate
CLI-->>User: compact project summary / SESSION START block
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR introduces ck (Context Keeper) v2 — a new community skill that gives Claude Code persistent, per-project memory across sessions using deterministic Node.js scripts with no external dependencies. It fits cleanly into the All previously-reported issues (read-after-write race in
Confidence Score: 4/5Safe to merge after fixing the tilde-expansion bug in the hook registration command — without it the core SessionStart feature is silently broken for all users All previously flagged P0/P1 issues are resolved. One new P1 remains: the hook command in SKILL.md uses ~ inside double quotes which bash does not expand, meaning the SessionStart hook never fires on any standard installation. The remaining findings are P2 and do not block merge. skills/ck/SKILL.md — hook registration command must use $HOME instead of ~ Important Files Changed
Sequence DiagramsequenceDiagram
participant U as User
participant CC as Claude Code
participant SS as session-start.mjs
participant S as save.mjs
participant R as resume.mjs
participant FS as ~/.claude/ck/
CC->>SS: SessionStart hook fires
SS->>FS: readJson(current-session.json) [prev]
SS->>FS: writeFileSync(current-session.json) [new]
SS->>FS: readJson(context.json)
SS-->>CC: additionalContext (compact ~100 token briefing)
U->>CC: /ck:init
CC->>CC: node init.mjs → JSON
CC->>U: Confirm project details?
U->>CC: Confirmed
CC->>S: echo JSON | node save.mjs --init
S->>FS: writeJson(context.json + CONTEXT.md + projects.json)
U->>CC: /ck:save
CC->>U: Draft summary — save this?
U->>CC: yes
CC->>S: echo JSON | node save.mjs
S->>FS: update context.json (append session)
S->>FS: writeFileSync(~/.claude/projects/<encoded>/memory/ck_*.md)
U->>CC: /ck:resume
CC->>R: node resume.mjs [arg]
R->>FS: readJson(context.json)
R-->>CC: Bordered briefing box
Reviews (3): Last reviewed commit: "fix(ck): preserve display names and hard..." | Re-trigger Greptile |
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (3)
skills/ck/commands/init.mjs (1)
138-141: Usepath.basename()for cross-platform directory name extraction.Splitting by
/won't work correctly on Windows paths.♻️ Proposed fix
+import { resolve, basename } from 'path'; ... // ── Name fallback: directory name ───────────────────────────────────────── if (!output.name) { - output.name = cwd.split('/').pop().toLowerCase().replace(/\s+/g, '-'); + output.name = basename(cwd).toLowerCase().replace(/\s+/g, '-'); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@skills/ck/commands/init.mjs` around lines 138 - 141, The code sets output.name using cwd.split('/').pop(), which breaks on Windows paths; replace that expression with path.basename(cwd) and ensure the module imports/uses the Node path API (e.g., require('path') or import path from 'path') so cross-platform directory name extraction is used, then continue to call .toLowerCase().replace(/\s+/g, '-') on the basename as before to preserve the existing normalization.skills/ck/commands/list.mjs (1)
11-12: Consolidate duplicate imports.The two import statements from
./shared.mjscan be combined into one.♻️ Proposed fix
-import { readProjects, loadContext, today, CONTEXTS_DIR } from './shared.mjs'; -import { renderListTable } from './shared.mjs'; +import { readProjects, loadContext, today, CONTEXTS_DIR, renderListTable } from './shared.mjs';Note:
CONTEXTS_DIRappears to be unused in this file.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@skills/ck/commands/list.mjs` around lines 11 - 12, The file has two separate imports from './shared.mjs' — combine them into a single import statement that imports readProjects, loadContext, today, renderListTable (and drop CONTEXTS_DIR if it truly isn't used in this module) to eliminate duplication; update the import to reference the unique symbols readProjects, loadContext, today, renderListTable (and remove CONTEXTS_DIR) so the module uses one consolidated import line.skills/ck/commands/shared.mjs (1)
182-185: Windows path encoding is incomplete.
encodeProjectPathonly replaces forward slashes. On Windows, paths likeC:\Users\foowould becomeC:\Users\foo(unchanged), potentially causing issues with native memory directory creation.♻️ Proposed fix for cross-platform support
export function encodeProjectPath(absolutePath) { // "/Users/sree/dev/app" -> "-Users-sree-dev-app" - return absolutePath.replace(/\//g, '-'); + return absolutePath.replace(/[/\\:]/g, '-'); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@skills/ck/commands/shared.mjs` around lines 182 - 185, The encodeProjectPath function currently only replaces forward slashes; update it to normalize Windows paths too by replacing backslashes and drive-letter colons so paths like "C:\Users\foo" are encoded safely; specifically change the replacement to target both '/' and '\' and ':' (e.g. use a regex like /[\/\\:]/g) in encodeProjectPath and keep the same replacement character '-' so all platforms produce a consistent encoded string.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@skills/ck/commands/forget.mjs`:
- Around line 39-44: The code deletes the context directory and then updates
projects.json via readProjects()/writeProjects(), which can leave projects.json
pointing to a removed context if writeProjects fails; wrap the remove-and-write
sequence in a try/catch around the operations that touch projects (use
readProjects, delete projects[projectPath], writeProjects) and on error restore
the deleted entry (re-add projects[projectPath] = originalValue) or recreate the
directory as appropriate, then log a clear error including the project name
variable (name) and the error so the caller can retry or clean up; ensure the
catch does not swallow errors and exits with a non-zero status if unrecoverable.
In `@skills/ck/commands/migrate.mjs`:
- Around line 44-47: The parser in parseDecisionsTable is treating aligned
markdown separator rows (e.g. "|:---|---:|") as data because it only checks for
lines matching /^[|\s-]+$/, so modify the logic that processes each line (the
block using line, cols and rows) to detect and skip markdown separator rows:
after splitting into cols, add a guard that skips the line when all relevant
cols match a separator pattern (e.g. /^:?-+:?$/ or generally /^[\s:-]+$/),
ensuring only real content rows are pushed into rows (keep references to line,
cols, rows and the function parseDecisionsTable).
- Around line 35-37: The parseBullets chain is using a regex that fails to match
multi-digit or dotted numbered lists (e.g. "1. item", "12. item"), causing
numbered items to be dropped; update the first .filter and the .map replace to
accept optional leading whitespace and either a dash/star or one-or-more digits
followed by a dot (e.g. use /^\s*(?:[-*]|\d+\.)\s*/ in the test/replace logic)
so parseBullets correctly retains and strips list markers for numbered and
bulleted items.
In `@skills/ck/commands/resume.mjs`:
- Around line 26-38: The current directory-existence check uses execSync with
projectPath interpolated into a shell command (in the block that references
projectPath, cwd, and execSync), which is a command-injection risk; replace that
whole execSync try/catch with a safe Node fs check: normalize/resolve
projectPath (use path.resolve) and then use fs.existsSync or fs.statSync to
verify the path exists and is a directory (e.g., check stat.isDirectory()), and
then log the same messages (→ cd ... or ⚠ Path not found ...) — remove any shell
interpolation and keep the logic that skips if projectPath is falsy or equals
cwd.
In `@skills/ck/commands/save.mjs`:
- Around line 121-128: When re-saving a session (existingIdx from
context.sessions.findIndex) the code always uses the last session's date for
gitSummary; change the logic so lastSessionDate uses the date of the session
being updated when existingIdx >= 0 (e.g., context.sessions[existingIdx]?.date)
and only fall back to the last session's date when creating a new session
(existingIdx < 0), then pass that corrected lastSessionDate into gitSummary(cwd,
lastSessionDate).
In `@skills/ck/commands/shared.mjs`:
- Around line 244-251: The table rows are corrupted if decision fields contain
pipe characters; before pushing rows for each decision in the loop (where
allDecisions.forEach and the template `| ${d.what} | ${d.why || ''} | ${d.date
|| ''} |` is built), sanitize each cell value (d.what, d.why, d.date) by
escaping or HTML-encoding pipe characters (e.g., replace '|' with '|' or
prepend a backslash) and normalizing newlines so the Markdown table stays
intact; apply this sanitization where the template is constructed.
- Around line 145-155: The runGit helper currently interpolates cwd into a shell
string causing command injection risk; update runGit to call execSync without
embedding cwd into the command (remove `-C "${cwd}"`) and instead pass cwd via
the execSync options object (use the cwd property), keep args as a separate
string or ideally as an array passed safely, preserve timeout/stdio/encoding and
return trimmed output or null on error; refer to the runGit function and its use
of execSync so you replace the `git -C "${cwd}" ${args}` invocation with
execSync('git ' + args, { cwd, timeout, stdio, encoding }) (or equivalent safe
invocation).
In `@skills/ck/hooks/session-start.mjs`:
- Line 216: The current console.log emits { additionalContext } directly, but
SessionStart hooks must output a hook envelope; replace the single top-level
additionalContext emission with a hookSpecificOutput object containing
hookEventName: "SessionStart" and the additionalContext payload (i.e., wrap the
existing parts.join(...) string under additionalContext inside
hookSpecificOutput with hookEventName "SessionStart"); update the console.log
call that currently references parts.join(...) so the platform recognizes the
hook output.
- Around line 89-97: You write current-session.json before checking the previous
session so the mismatch check never triggers; change the flow in the
session-start logic to first attempt to read/parse the existing CURRENT_SESSION
(wrap in try/catch and parse to prevSession), perform the mismatch check
comparing prevSession.sessionId (and any relevant fields) against the new
sessionId/project info, handle the warning if they differ, and only then
serialize/write the new session object to CURRENT_SESSION; reference
CURRENT_SESSION, sessionId, entry?.name, and the existing mismatch-check block
so you update the read/write order without changing the comparison logic.
- Around line 20-21: The git command built in session-start.mjs currently
interpolates projectPath and sinceDate into a shell string passed to execSync,
allowing shell injection; replace the use of execSync with execFileSync
(imported from 'child_process') and call git as an executable with an argument
array that includes projectPath and sinceDate as separate arguments (e.g., use
['-C', projectPath, 'log', `--since=${sinceDate}`, ...] or pass '--since' and
sinceDate separately) so no user data is passed through a shell string and avoid
shell:true.
In `@skills/ck/SKILL.md`:
- Around line 10-147: The SKILL.md is missing the required documentation
sections; update SKILL.md by adding three markdown sections: "When to Use"
(bullet scenarios for using /ck:init, /ck:save, /ck:resume, /ck:list,
/ck:forget, /ck:migrate and the session-start hook), "How It Works" (brief
numbered steps describing Registration (/ck:init → save.mjs), Saving (/ck:save
analysis → save.mjs), Resuming (/ck:resume and session-start hook injection),
and Migration (/ck:migrate)), and "Examples" (short sample invocations and
expected outputs for /ck:init, /ck:save, /ck:resume), ensuring wording matches
the repository skill formatting guidelines and that the examples reference the
actual command names used in the file.
---
Nitpick comments:
In `@skills/ck/commands/init.mjs`:
- Around line 138-141: The code sets output.name using cwd.split('/').pop(),
which breaks on Windows paths; replace that expression with path.basename(cwd)
and ensure the module imports/uses the Node path API (e.g., require('path') or
import path from 'path') so cross-platform directory name extraction is used,
then continue to call .toLowerCase().replace(/\s+/g, '-') on the basename as
before to preserve the existing normalization.
In `@skills/ck/commands/list.mjs`:
- Around line 11-12: The file has two separate imports from './shared.mjs' —
combine them into a single import statement that imports readProjects,
loadContext, today, renderListTable (and drop CONTEXTS_DIR if it truly isn't
used in this module) to eliminate duplication; update the import to reference
the unique symbols readProjects, loadContext, today, renderListTable (and remove
CONTEXTS_DIR) so the module uses one consolidated import line.
In `@skills/ck/commands/shared.mjs`:
- Around line 182-185: The encodeProjectPath function currently only replaces
forward slashes; update it to normalize Windows paths too by replacing
backslashes and drive-letter colons so paths like "C:\Users\foo" are encoded
safely; specifically change the replacement to target both '/' and '\' and ':'
(e.g. use a regex like /[\/\\:]/g) in encodeProjectPath and keep the same
replacement character '-' so all platforms produce a consistent encoded string.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9bb042ee-b7ff-4a1a-9f4c-3957772ee2c8
📒 Files selected for processing (10)
skills/ck/SKILL.mdskills/ck/commands/forget.mjsskills/ck/commands/info.mjsskills/ck/commands/init.mjsskills/ck/commands/list.mjsskills/ck/commands/migrate.mjsskills/ck/commands/resume.mjsskills/ck/commands/save.mjsskills/ck/commands/shared.mjsskills/ck/hooks/session-start.mjs
| // Remove from projects.json | ||
| const projects = readProjects(); | ||
| delete projects[projectPath]; | ||
| writeProjects(projects); | ||
|
|
||
| console.log(`✓ Context for '${name}' removed.`); |
There was a problem hiding this comment.
Potential state inconsistency if writeProjects fails.
If the context directory is deleted but writeProjects throws (e.g., disk full, permissions), projects.json will still reference the deleted context. Consider wrapping in try/catch to handle this edge case gracefully.
🛡️ Proposed fix
// Remove from projects.json
-const projects = readProjects();
-delete projects[projectPath];
-writeProjects(projects);
+try {
+ const projects = readProjects();
+ delete projects[projectPath];
+ writeProjects(projects);
+} catch (e) {
+ console.log(`⚠ Context directory removed but projects.json update failed: ${e.message}`);
+ process.exit(1);
+}
console.log(`✓ Context for '${name}' removed.`);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@skills/ck/commands/forget.mjs` around lines 39 - 44, The code deletes the
context directory and then updates projects.json via
readProjects()/writeProjects(), which can leave projects.json pointing to a
removed context if writeProjects fails; wrap the remove-and-write sequence in a
try/catch around the operations that touch projects (use readProjects, delete
projects[projectPath], writeProjects) and on error restore the deleted entry
(re-add projects[projectPath] = originalValue) or recreate the directory as
appropriate, then log a clear error including the project name variable (name)
and the error so the caller can retry or clean up; ensure the catch does not
swallow errors and exits with a non-zero status if unrecoverable.
| .filter(l => /^[-*\d]\s/.test(l.trim())) | ||
| .map(l => l.replace(/^[-*\d]+\.?\s+/, '').trim()) | ||
| .filter(Boolean); |
There was a problem hiding this comment.
Numbered bullet items are silently dropped during migration.
parseBullets currently misses common markdown list items like 1. item and multi-digit numbering, so nextSteps, constraints, blockers, and leftOff can lose data during v1→v2 conversion.
Proposed fix
function parseBullets(text) {
if (!text) return [];
return text.split('\n')
- .filter(l => /^[-*\d]\s/.test(l.trim()))
- .map(l => l.replace(/^[-*\d]+\.?\s+/, '').trim())
+ .filter(l => /^([-*]\s+|\d+\.?\s+)/.test(l.trim()))
+ .map(l => l.replace(/^([-*]|\d+\.?)\s+/, '').trim())
.filter(Boolean);
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@skills/ck/commands/migrate.mjs` around lines 35 - 37, The parseBullets chain
is using a regex that fails to match multi-digit or dotted numbered lists (e.g.
"1. item", "12. item"), causing numbered items to be dropped; update the first
.filter and the .map replace to accept optional leading whitespace and either a
dash/star or one-or-more digits followed by a dot (e.g. use
/^\s*(?:[-*]|\d+\.)\s*/ in the test/replace logic) so parseBullets correctly
retains and strips list markers for numbered and bulleted items.
| if (!line.startsWith('|') || line.match(/^[|\s-]+$/)) continue; | ||
| const cols = line.split('|').map(c => c.trim()).filter((c, i) => i > 0 && i < 4); | ||
| if (cols.length >= 1 && !cols[0].startsWith('Decision') && !cols[0].startsWith('_')) { | ||
| rows.push({ what: cols[0] || '', why: cols[1] || '', date: cols[2] || '' }); |
There was a problem hiding this comment.
Aligned markdown separator rows can be misparsed as decisions.
parseDecisionsTable does not skip separator rows containing :, so tables like |:---|---:| may produce fake decision records.
Proposed fix
- if (!line.startsWith('|') || line.match(/^[|\s-]+$/)) continue;
+ if (!line.startsWith('|') || line.match(/^[|\s:-]+$/)) continue;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@skills/ck/commands/migrate.mjs` around lines 44 - 47, The parser in
parseDecisionsTable is treating aligned markdown separator rows (e.g.
"|:---|---:|") as data because it only checks for lines matching /^[|\s-]+$/, so
modify the logic that processes each line (the block using line, cols and rows)
to detect and skip markdown separator rows: after splitting into cols, add a
guard that skips the line when all relevant cols match a separator pattern (e.g.
/^:?-+:?$/ or generally /^[\s:-]+$/), ensuring only real content rows are pushed
into rows (keep references to line, cols, rows and the function
parseDecisionsTable).
| // Check for duplicate (re-save of same session) | ||
| const existingIdx = context.sessions.findIndex(s => s.id === sessionId); | ||
|
|
||
| const { summary, leftOff, nextSteps, decisions, blockers, goal } = input; | ||
|
|
||
| // Capture git activity since the last session | ||
| const lastSessionDate = context.sessions?.[context.sessions.length - 1]?.date; | ||
| const gitActivity = gitSummary(cwd, lastSessionDate); |
There was a problem hiding this comment.
Git activity calculation may be incorrect on session re-save.
When re-saving an existing session (existingIdx >= 0), lastSessionDate is taken from the last session in the array (line 127), not from the session being updated. If the session being re-saved isn't the last one, gitSummary will use the wrong baseline date.
This is an edge case (re-saving a non-latest session is uncommon), but worth noting.
💡 Proposed fix
// Check for duplicate (re-save of same session)
const existingIdx = context.sessions.findIndex(s => s.id === sessionId);
const { summary, leftOff, nextSteps, decisions, blockers, goal } = input;
// Capture git activity since the last session
-const lastSessionDate = context.sessions?.[context.sessions.length - 1]?.date;
+const lastSessionDate = existingIdx >= 0
+ ? context.sessions[existingIdx]?.date // Use the session's own date on re-save
+ : context.sessions?.[context.sessions.length - 1]?.date;
const gitActivity = gitSummary(cwd, lastSessionDate);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Check for duplicate (re-save of same session) | |
| const existingIdx = context.sessions.findIndex(s => s.id === sessionId); | |
| const { summary, leftOff, nextSteps, decisions, blockers, goal } = input; | |
| // Capture git activity since the last session | |
| const lastSessionDate = context.sessions?.[context.sessions.length - 1]?.date; | |
| const gitActivity = gitSummary(cwd, lastSessionDate); | |
| // Check for duplicate (re-save of same session) | |
| const existingIdx = context.sessions.findIndex(s => s.id === sessionId); | |
| const { summary, leftOff, nextSteps, decisions, blockers, goal } = input; | |
| // Capture git activity since the last session | |
| const lastSessionDate = existingIdx >= 0 | |
| ? context.sessions[existingIdx]?.date // Use the session's own date on re-save | |
| : context.sessions?.[context.sessions.length - 1]?.date; | |
| const gitActivity = gitSummary(cwd, lastSessionDate); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@skills/ck/commands/save.mjs` around lines 121 - 128, When re-saving a session
(existingIdx from context.sessions.findIndex) the code always uses the last
session's date for gitSummary; change the logic so lastSessionDate uses the date
of the session being updated when existingIdx >= 0 (e.g.,
context.sessions[existingIdx]?.date) and only fall back to the last session's
date when creating a new session (existingIdx < 0), then pass that corrected
lastSessionDate into gitSummary(cwd, lastSessionDate).
| lines.push(`## Decisions Made`); | ||
| lines.push(`| Decision | Why | Date |`); | ||
| lines.push(`|----------|-----|------|`); | ||
| if (allDecisions.length) { | ||
| allDecisions.forEach(d => lines.push(`| ${d.what} | ${d.why || ''} | ${d.date || ''} |`)); | ||
| } else { | ||
| lines.push(`| _(none yet)_ | | |`); | ||
| } |
There was a problem hiding this comment.
Pipe characters in decision text will break the Markdown table.
If d.what or d.why contains | characters, the table structure will be corrupted.
💡 Proposed fix
+const escapeCell = (s) => String(s || '').replace(/\|/g, '\\|');
+
lines.push(`## Decisions Made`);
lines.push(`| Decision | Why | Date |`);
lines.push(`|----------|-----|------|`);
if (allDecisions.length) {
- allDecisions.forEach(d => lines.push(`| ${d.what} | ${d.why || ''} | ${d.date || ''} |`));
+ allDecisions.forEach(d => lines.push(`| ${escapeCell(d.what)} | ${escapeCell(d.why)} | ${d.date || ''} |`));
} else {
lines.push(`| _(none yet)_ | | |`);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| lines.push(`## Decisions Made`); | |
| lines.push(`| Decision | Why | Date |`); | |
| lines.push(`|----------|-----|------|`); | |
| if (allDecisions.length) { | |
| allDecisions.forEach(d => lines.push(`| ${d.what} | ${d.why || ''} | ${d.date || ''} |`)); | |
| } else { | |
| lines.push(`| _(none yet)_ | | |`); | |
| } | |
| const escapeCell = (s) => String(s || '').replace(/\|/g, '\\|'); | |
| lines.push(`## Decisions Made`); | |
| lines.push(`| Decision | Why | Date |`); | |
| lines.push(`|----------|-----|------|`); | |
| if (allDecisions.length) { | |
| allDecisions.forEach(d => lines.push(`| ${escapeCell(d.what)} | ${escapeCell(d.why)} | ${d.date || ''} |`)); | |
| } else { | |
| lines.push(`| _(none yet)_ | | |`); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@skills/ck/commands/shared.mjs` around lines 244 - 251, The table rows are
corrupted if decision fields contain pipe characters; before pushing rows for
each decision in the loop (where allDecisions.forEach and the template `|
${d.what} | ${d.why || ''} | ${d.date || ''} |` is built), sanitize each cell
value (d.what, d.why, d.date) by escaping or HTML-encoding pipe characters
(e.g., replace '|' with '|' or prepend a backslash) and normalizing
newlines so the Markdown table stays intact; apply this sanitization where the
template is constructed.
|
|
||
| const parts = main(); | ||
| if (parts.length > 0) { | ||
| console.log(JSON.stringify({ additionalContext: parts.join('\n\n---\n\n') })); |
There was a problem hiding this comment.
SessionStart payload shape does not follow the expected hook envelope.
Line 216 emits { additionalContext }, but SessionStart hooks should emit hookSpecificOutput with hookEventName: "SessionStart" so the platform treats it as hook context.
Proposed fix
if (parts.length > 0) {
- console.log(JSON.stringify({ additionalContext: parts.join('\n\n---\n\n') }));
+ console.log(JSON.stringify({
+ hookSpecificOutput: {
+ hookEventName: 'SessionStart',
+ additionalContext: parts.join('\n\n---\n\n'),
+ },
+ }));
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| console.log(JSON.stringify({ additionalContext: parts.join('\n\n---\n\n') })); | |
| if (parts.length > 0) { | |
| console.log(JSON.stringify({ | |
| hookSpecificOutput: { | |
| hookEventName: 'SessionStart', | |
| additionalContext: parts.join('\n\n---\n\n'), | |
| }, | |
| })); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@skills/ck/hooks/session-start.mjs` at line 216, The current console.log emits
{ additionalContext } directly, but SessionStart hooks must output a hook
envelope; replace the single top-level additionalContext emission with a
hookSpecificOutput object containing hookEventName: "SessionStart" and the
additionalContext payload (i.e., wrap the existing parts.join(...) string under
additionalContext inside hookSpecificOutput with hookEventName "SessionStart");
update the console.log call that currently references parts.join(...) so the
platform recognizes the hook output.
| # ck — Context Keeper | ||
|
|
||
| You are the **Context Keeper** assistant. When the user invokes any `/ck:*` command, | ||
| run the corresponding Node.js script and present its stdout to the user verbatim. | ||
| Scripts live at: `~/.claude/skills/ck/commands/` (expand `~` with `$HOME`). | ||
|
|
||
| --- | ||
|
|
||
| ## Data Layout | ||
|
|
||
| ``` | ||
| ~/.claude/ck/ | ||
| ├── projects.json ← path → {name, contextDir, lastUpdated} | ||
| └── contexts/<name>/ | ||
| ├── context.json ← SOURCE OF TRUTH (structured JSON, v2) | ||
| └── CONTEXT.md ← generated view — do not hand-edit | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Commands | ||
|
|
||
| ### `/ck:init` — Register a Project | ||
| ```bash | ||
| node "$HOME/.claude/skills/ck/commands/init.mjs" | ||
| ``` | ||
| The script outputs JSON with auto-detected info. Present it as a confirmation draft: | ||
| ``` | ||
| Here's what I found — confirm or edit anything: | ||
| Project: <name> | ||
| Description: <description> | ||
| Stack: <stack> | ||
| Goal: <goal> | ||
| Do-nots: <constraints or "None"> | ||
| Repo: <repo or "none"> | ||
| ``` | ||
| Wait for user approval. Apply any edits. Then pipe confirmed JSON to save.mjs --init: | ||
| ```bash | ||
| echo '<confirmed-json>' | node "$HOME/.claude/skills/ck/commands/save.mjs" --init | ||
| ``` | ||
| Confirmed JSON schema: `{"name":"...","path":"...","description":"...","stack":["..."],"goal":"...","constraints":["..."],"repo":"..." }` | ||
|
|
||
| --- | ||
|
|
||
| ### `/ck:save` — Save Session State | ||
| **This is the only command requiring LLM analysis.** Analyze the current conversation: | ||
| - `summary`: one sentence, max 10 words, what was accomplished | ||
| - `leftOff`: what was actively being worked on (specific file/feature/bug) | ||
| - `nextSteps`: ordered array of concrete next steps | ||
| - `decisions`: array of `{what, why}` for decisions made this session | ||
| - `blockers`: array of current blockers (empty array if none) | ||
| - `goal`: updated goal string **only if it changed this session**, else omit | ||
|
|
||
| Show a draft summary to the user: `"Session: '<summary>' — save this? (yes / edit)"` | ||
| Wait for confirmation. Then pipe to save.mjs: | ||
| ```bash | ||
| echo '<json>' | node "$HOME/.claude/skills/ck/commands/save.mjs" | ||
| ``` | ||
| JSON schema (exact): `{"summary":"...","leftOff":"...","nextSteps":["..."],"decisions":[{"what":"...","why":"..."}],"blockers":["..."]}` | ||
| Display the script's stdout confirmation verbatim. | ||
|
|
||
| --- | ||
|
|
||
| ### `/ck:resume [name|number]` — Full Briefing | ||
| ```bash | ||
| node "$HOME/.claude/skills/ck/commands/resume.mjs" [arg] | ||
| ``` | ||
| Display output verbatim. Then ask: "Continue from here? Or has anything changed?" | ||
| If user reports changes → run `/ck:save` immediately. | ||
|
|
||
| --- | ||
|
|
||
| ### `/ck:info [name|number]` — Quick Snapshot | ||
| ```bash | ||
| node "$HOME/.claude/skills/ck/commands/info.mjs" [arg] | ||
| ``` | ||
| Display output verbatim. No follow-up question. | ||
|
|
||
| --- | ||
|
|
||
| ### `/ck:list` — Portfolio View | ||
| ```bash | ||
| node "$HOME/.claude/skills/ck/commands/list.mjs" | ||
| ``` | ||
| Display output verbatim. If user replies with a number or name → run `/ck:resume`. | ||
|
|
||
| --- | ||
|
|
||
| ### `/ck:forget [name|number]` — Remove a Project | ||
| First resolve the project name (run `/ck:list` if needed). | ||
| Ask: `"This will permanently delete context for '<name>'. Are you sure? (yes/no)"` | ||
| If yes: | ||
| ```bash | ||
| node "$HOME/.claude/skills/ck/commands/forget.mjs" [name] | ||
| ``` | ||
| Display confirmation verbatim. | ||
|
|
||
| --- | ||
|
|
||
| ### `/ck:migrate` — Convert v1 Data to v2 | ||
| ```bash | ||
| node "$HOME/.claude/skills/ck/commands/migrate.mjs" | ||
| ``` | ||
| For a dry run first: | ||
| ```bash | ||
| node "$HOME/.claude/skills/ck/commands/migrate.mjs" --dry-run | ||
| ``` | ||
| Display output verbatim. Migrates all v1 CONTEXT.md + meta.json files to v2 context.json. | ||
| Originals are backed up as `meta.json.v1-backup` — nothing is deleted. | ||
|
|
||
| --- | ||
|
|
||
| ## SessionStart Hook | ||
|
|
||
| The hook at `~/.claude/skills/ck/hooks/session-start.mjs` must be registered in | ||
| `~/.claude/settings.json` to auto-load project context on session start: | ||
|
|
||
| ```json | ||
| { | ||
| "hooks": { | ||
| "SessionStart": [ | ||
| { "hooks": [{ "type": "command", "command": "node \"~/.claude/skills/ck/hooks/session-start.mjs\"" }] } | ||
| ] | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| The hook injects ~100 tokens per session (compact 5-line summary). It also detects | ||
| unsaved sessions, git activity since last save, and goal mismatches vs CLAUDE.md. | ||
|
|
||
| --- | ||
|
|
||
| ## Rules | ||
| - Always expand `~` as `$HOME` in Bash calls. | ||
| - Commands are case-insensitive: `/CK:SAVE`, `/ck:save`, `/Ck:Save` all work. | ||
| - If a script exits with code 1, display its stdout as an error message. | ||
| - Never edit `context.json` or `CONTEXT.md` directly — always use the scripts. | ||
| - If `projects.json` is malformed, tell the user and offer to reset it to `{}`. |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Missing required skill documentation sections.
Per coding guidelines, skills under skills/**/*.md must include 'When to Use', 'How It Works', and 'Examples' sections. This SKILL.md is missing all three.
Consider adding these sections to comply with the repository's skill formatting standards:
📝 Suggested structure to add after line 9
## When to Use
- When starting a new Claude Code session and you want to quickly resume context from your last session
- When working across multiple projects and need to track session state for each
- When you want decisions, blockers, and next steps to persist between sessions
- When onboarding to a project and need a quick briefing on current state
## How It Works
1. **Registration**: `/ck:init` detects project metadata (stack, goal, constraints) from config files and CLAUDE.md
2. **Saving**: `/ck:save` captures session state (decisions, blockers, next steps) to `context.json` and writes to native memory
3. **Resuming**: `/ck:resume` loads the full briefing box; the session-start hook auto-injects a compact summary (~100 tokens)
4. **Portfolio**: `/ck:list` shows all registered projects with staleness indicators
## Examples
### Initialize a new project/ck:init
Claude auto-detects project info and asks for confirmation before saving.
### Save session state
/ck:save
Claude analyzes the conversation and proposes a summary to save.
### Resume a project by name
/ck:resume my-app
/ck:resume 2
As per coding guidelines: "Skills must be formatted as Markdown files with clear sections including 'When to Use', 'How It Works', and 'Examples'".
</details>
<details>
<summary>🤖 Prompt for AI Agents</summary>
Verify each finding against the current code and only fix it if needed.
In @skills/ck/SKILL.md around lines 10 - 147, The SKILL.md is missing the
required documentation sections; update SKILL.md by adding three markdown
sections: "When to Use" (bullet scenarios for using /ck:init, /ck:save,
/ck:resume, /ck:list, /ck:forget, /ck:migrate and the session-start hook), "How
It Works" (brief numbered steps describing Registration (/ck:init → save.mjs),
Saving (/ck:save analysis → save.mjs), Resuming (/ck:resume and session-start
hook injection), and Migration (/ck:migrate)), and "Examples" (short sample
invocations and expected outputs for /ck:init, /ck:save, /ck:resume), ensuring
wording matches the repository skill formatting guidelines and that the examples
reference the actual command names used in the file.
</details>
<!-- fingerprinting:phantom:medusa:ocelot:57dcdb90-4846-489b-bd12-c20feb894a8a -->
<!-- This is an auto-generated comment by CodeRabbit -->
There was a problem hiding this comment.
22 issues found across 10 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="skills/ck/commands/init.mjs">
<violation number="1" location="skills/ck/commands/init.mjs:36">
P2: Section extraction regex is newline-fragile; it won’t match CRLF (`\r\n`) headings, causing CLAUDE.md sections to be skipped on Windows.</violation>
<violation number="2" location="skills/ck/commands/init.mjs:97">
P2: Repo URL from .git/config is emitted verbatim; if the remote contains embedded credentials (https://token@host/...), they will be exposed in output and persisted. Sanitize userinfo before saving.</violation>
<violation number="3" location="skills/ck/commands/init.mjs:140">
P2: Name fallback splits `cwd` only on '/', which breaks on Windows paths and yields a full path instead of the directory name.</violation>
</file>
<file name="skills/ck/SKILL.md">
<violation number="1" location="skills/ck/SKILL.md:7">
P2: User-facing skill metadata links to an external GitHub repo, which violates the supply-chain hardening guidance to avoid unvetted external repositories.</violation>
<violation number="2" location="skills/ck/SKILL.md:48">
P2: Using single-quoted `echo '<confirmed-json>'` is unsafe for JSON that may contain apostrophes; it can break the shell string or allow unintended shell interpretation. Use a quoted heredoc or printf-safe piping instead.</violation>
<violation number="3" location="skills/ck/SKILL.md:66">
P2: Using single-quoted `echo '<json>'` is unsafe for JSON that may contain apostrophes; it can break the shell string or allow unintended shell interpretation. Use a quoted heredoc or printf-safe piping instead.</violation>
<violation number="4" location="skills/ck/SKILL.md:131">
P2: SessionStart hook example quotes `~`, preventing tilde expansion and likely breaking the hook command.</violation>
</file>
<file name="skills/ck/commands/resume.mjs">
<violation number="1" location="skills/ck/commands/resume.mjs:29">
P2: Shell-based `test -d` directory check is non-portable (fails on Windows) and errors are swallowed, so `/ck:resume` can silently skip the path message. Prefer a cross-platform fs check (fs.stat/access) with explicit error handling.</violation>
<violation number="2" location="skills/ck/commands/resume.mjs:29">
P2: `execSync` shells out with an unsanitized `projectPath`. Because the value is interpolated into a shell command, a crafted project path (from persisted projects.json) can trigger command injection. Use fs APIs instead of a shell command to check for directory existence.</violation>
</file>
<file name="skills/ck/commands/migrate.mjs">
<violation number="1" location="skills/ck/commands/migrate.mjs:27">
P2: extractSection only matches LF (`\n`) line endings, so CRLF Markdown headings/sections won't parse and migrated fields can be dropped.</violation>
<violation number="2" location="skills/ck/commands/migrate.mjs:35">
P2: parseBullets filters out standard numbered list items like `1. item`, causing those entries to be dropped during migration.</violation>
<violation number="3" location="skills/ck/commands/migrate.mjs:44">
P2: Skip alignment separator rows that contain `:` in the decisions table parser; otherwise `|:---|---:|` lines can be migrated as bogus decision records.</violation>
<violation number="4" location="skills/ck/commands/migrate.mjs:175">
P2: Migration renames meta.json before saveContext; a failure leaves the project in a partially migrated state with meta.json missing, so retries lose v1 metadata unless the user restores the backup manually.</violation>
</file>
<file name="skills/ck/hooks/session-start.mjs">
<violation number="1" location="skills/ck/hooks/session-start.mjs:50">
P1: Shell command injection risk: execSync is called with a string that interpolates projectPath and sinceDate, allowing crafted values to break out of quotes and execute arbitrary commands.</violation>
<violation number="2" location="skills/ck/hooks/session-start.mjs:91">
P2: Unsaved-session detection reads CURRENT_SESSION after it has already been overwritten with the new session, so prevSession usually equals sessionId and the warning never triggers.</violation>
</file>
<file name="skills/ck/commands/save.mjs">
<violation number="1" location="skills/ck/commands/save.mjs:57">
P2: `contextDir` can be empty after sanitization, but it is still used to save/register a project. An empty `contextDir` writes context.json/CONTEXT.md directly into the base contexts directory, causing collisions/overwrites and ambiguous lookups for projects whose names sanitize to empty.</violation>
<violation number="2" location="skills/ck/commands/save.mjs:178">
P2: Unescaped `session.summary` is inserted directly into YAML frontmatter, so summaries containing newlines or YAML-significant characters can corrupt the metadata and break native memory parsing.</violation>
</file>
<file name="skills/ck/commands/shared.mjs">
<violation number="1" location="skills/ck/commands/shared.mjs:147">
P1: Dynamic shell command construction in `runGit` allows command injection via unescaped `cwd`/`args` interpolation.</violation>
<violation number="2" location="skills/ck/commands/shared.mjs:169">
P2: `gitSummary` incorrectly uses reflog index syntax (`HEAD@{count}`) where commit ancestry was intended, causing inaccurate or brittle diff-based file-change summaries.</violation>
<violation number="3" location="skills/ck/commands/shared.mjs:184">
P1: Project-path encoding is lossy and Unix-only, which can collide distinct projects and misroute native memory storage.</violation>
<violation number="4" location="skills/ck/commands/shared.mjs:248">
P3: Escape `|` characters in decision fields before writing markdown table cells; otherwise `CONTEXT.md` tables break when decision text includes pipes.</violation>
<violation number="5" location="skills/ck/commands/shared.mjs:285">
P3: Use `displayName` (with `name` as fallback) in user-facing render output. Right now saved projects always display the slug instead of the original project name.</violation>
</file>
Since this is your first cubic review, here's how it works:
- cubic automatically reviews your code and comments on bugs and improvements
- Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
- Add one-off context when rerunning by tagging
@cubic-dev-aiwith guidance or docs links (includingllms.txt) - Ask questions if you need clarification on any suggestion
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
|
|
||
| export function encodeProjectPath(absolutePath) { | ||
| // "/Users/sree/dev/app" -> "-Users-sree-dev-app" | ||
| return absolutePath.replace(/\//g, '-'); |
There was a problem hiding this comment.
P1: Project-path encoding is lossy and Unix-only, which can collide distinct projects and misroute native memory storage.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At skills/ck/commands/shared.mjs, line 184:
<comment>Project-path encoding is lossy and Unix-only, which can collide distinct projects and misroute native memory storage.</comment>
<file context>
@@ -0,0 +1,384 @@
+
+export function encodeProjectPath(absolutePath) {
+ // "/Users/sree/dev/app" -> "-Users-sree-dev-app"
+ return absolutePath.replace(/\//g, '-');
+}
+
</file context>
| const gitConfig = readFile('.git/config'); | ||
| if (gitConfig) { | ||
| const repoMatch = gitConfig.match(/url\s*=\s*(.+)/); | ||
| if (repoMatch) output.repo = repoMatch[1].trim(); |
There was a problem hiding this comment.
P2: Repo URL from .git/config is emitted verbatim; if the remote contains embedded credentials (https://token@host/...), they will be exposed in output and persisted. Sanitize userinfo before saving.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At skills/ck/commands/init.mjs, line 97:
<comment>Repo URL from .git/config is emitted verbatim; if the remote contains embedded credentials (https://token@host/...), they will be exposed in output and persisted. Sanitize userinfo before saving.</comment>
<file context>
@@ -0,0 +1,143 @@
+const gitConfig = readFile('.git/config');
+if (gitConfig) {
+ const repoMatch = gitConfig.match(/url\s*=\s*(.+)/);
+ if (repoMatch) output.repo = repoMatch[1].trim();
+}
+
</file context>
| const projects = readProjects(); | ||
|
|
||
| // Derive contextDir (lowercase, spaces→dashes, deduplicate) | ||
| let contextDir = name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''); |
There was a problem hiding this comment.
P2: contextDir can be empty after sanitization, but it is still used to save/register a project. An empty contextDir writes context.json/CONTEXT.md directly into the base contexts directory, causing collisions/overwrites and ambiguous lookups for projects whose names sanitize to empty.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At skills/ck/commands/save.mjs, line 57:
<comment>`contextDir` can be empty after sanitization, but it is still used to save/register a project. An empty `contextDir` writes context.json/CONTEXT.md directly into the base contexts directory, causing collisions/overwrites and ambiguous lookups for projects whose names sanitize to empty.</comment>
<file context>
@@ -0,0 +1,210 @@
+ const projects = readProjects();
+
+ // Derive contextDir (lowercase, spaces→dashes, deduplicate)
+ let contextDir = name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
+ let suffix = 2;
+ const existingDirs = Object.values(projects).map(p => p.contextDir);
</file context>
| if (!text) return []; | ||
| const rows = []; | ||
| for (const line of text.split('\n')) { | ||
| if (!line.startsWith('|') || line.match(/^[|\s-]+$/)) continue; |
There was a problem hiding this comment.
P2: Skip alignment separator rows that contain : in the decisions table parser; otherwise |:---|---:| lines can be migrated as bogus decision records.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At skills/ck/commands/migrate.mjs, line 44:
<comment>Skip alignment separator rows that contain `:` in the decisions table parser; otherwise `|:---|---:|` lines can be migrated as bogus decision records.</comment>
<file context>
@@ -0,0 +1,198 @@
+ if (!text) return [];
+ const rows = [];
+ for (const line of text.split('\n')) {
+ if (!line.startsWith('|') || line.match(/^[|\s-]+$/)) continue;
+ const cols = line.split('|').map(c => c.trim()).filter((c, i) => i > 0 && i < 4);
+ if (cols.length >= 1 && !cols[0].startsWith('Decision') && !cols[0].startsWith('_')) {
</file context>
| lines.push(`| Decision | Why | Date |`); | ||
| lines.push(`|----------|-----|------|`); | ||
| if (allDecisions.length) { | ||
| allDecisions.forEach(d => lines.push(`| ${d.what} | ${d.why || ''} | ${d.date || ''} |`)); |
There was a problem hiding this comment.
P3: Escape | characters in decision fields before writing markdown table cells; otherwise CONTEXT.md tables break when decision text includes pipes.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At skills/ck/commands/shared.mjs, line 248:
<comment>Escape `|` characters in decision fields before writing markdown table cells; otherwise `CONTEXT.md` tables break when decision text includes pipes.</comment>
<file context>
@@ -0,0 +1,384 @@
+ lines.push(`| Decision | Why | Date |`);
+ lines.push(`|----------|-----|------|`);
+ if (allDecisions.length) {
+ allDecisions.forEach(d => lines.push(`| ${d.what} | ${d.why || ''} | ${d.date || ''} |`));
+ } else {
+ lines.push(`| _(none yet)_ | | |`);
</file context>
- Fix read-after-write in session-start.mjs: read prevSession BEFORE
overwriting current-session.json so unsaved-session detection fires
- Fix shell injection in resume.mjs: replace execSync shell string with
fs.existsSync for directory existence check
- Fix shell injection in shared.mjs gitSummary: replace nested \$(git ...)
subshell with a separate runGit() call to get rev count
- Fix displayName never shown: render functions now use ctx.displayName
?? ctx.name so user-supplied names show instead of the slug
- Fix renderListTable: uses context.displayName ?? entry.name
- Fix init.mjs: use path.basename() instead of cwd.split('/').pop()
- Fix save.mjs confirmation: show original name, not contextDir slug
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Analysis Failed
Troubleshooting
Retry: |
There was a problem hiding this comment.
4 issues found across 5 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="skills/ck/commands/resume.mjs">
<violation number="1" location="skills/ck/commands/resume.mjs:28">
P2: Directory check was weakened to existence-only, so `/ck:resume` may suggest `cd` into a non-directory path.</violation>
</file>
<file name="skills/ck/commands/shared.mjs">
<violation number="1" location="skills/ck/commands/shared.mjs:171">
P2: `gitSummary` can silently lose file-change statistics because it now relies on a single brittle `HEAD~revCount..HEAD` range with no fallback when the range is invalid.</violation>
<violation number="2" location="skills/ck/commands/shared.mjs:356">
P2: `/ck:list` now shows `displayName`, but name lookup for `/ck:resume` and `/ck:info` still matches only stored `name` (slug), so visible project names may not resolve.</violation>
</file>
<file name="skills/ck/hooks/session-start.mjs">
<violation number="1" location="skills/ck/hooks/session-start.mjs:90">
P2: Unsaved-session warning is not scoped to the previous session’s project, so it can fire incorrectly after switching projects.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
|
|
||
| // Attempt to cd to the project path | ||
| if (projectPath && projectPath !== cwd) { | ||
| if (existsSync(projectPath)) { |
There was a problem hiding this comment.
P2: Directory check was weakened to existence-only, so /ck:resume may suggest cd into a non-directory path.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At skills/ck/commands/resume.mjs, line 28:
<comment>Directory check was weakened to existence-only, so `/ck:resume` may suggest `cd` into a non-directory path.</comment>
<file context>
@@ -25,16 +25,11 @@ const { context, projectPath } = resolved;
- console.log(`⚠ Path not found: ${projectPath}`);
- }
- } catch { /* non-fatal */ }
+ if (existsSync(projectPath)) {
+ console.log(`→ cd ${projectPath}`);
+ } else {
</file context>
| const statusLabel = icon === '●' ? '● Active' : icon === '◐' ? '◐ Warm' : '○ Stale'; | ||
| const sessId = latest.id ? latest.id.slice(0, 8) : '—'; | ||
| const summary = (latest.summary || '—').slice(0, 34); | ||
| const displayName = ((e.context?.displayName ?? e.name) + (isHere ? ' <-' : '')).slice(0, 18); |
There was a problem hiding this comment.
P2: /ck:list now shows displayName, but name lookup for /ck:resume and /ck:info still matches only stored name (slug), so visible project names may not resolve.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At skills/ck/commands/shared.mjs, line 356:
<comment>`/ck:list` now shows `displayName`, but name lookup for `/ck:resume` and `/ck:info` still matches only stored `name` (slug), so visible project names may not resolve.</comment>
<file context>
@@ -352,7 +353,7 @@ export function renderListTable(entries, cwd, todayStr) {
const sessId = latest.id ? latest.id.slice(0, 8) : '—';
const summary = (latest.summary || '—').slice(0, 34);
- const displayName = (e.name + (isHere ? ' <-' : '')).slice(0, 18);
+ const displayName = ((e.context?.displayName ?? e.name) + (isHere ? ' <-' : '')).slice(0, 18);
return {
num: String(i + 1),
</file context>
skills/ck/commands/shared.mjs
Outdated
| // Count unique files changed: use a separate runGit call to avoid nested shell substitution | ||
| const countStr = runGit(`rev-list --count HEAD --since="${sinceDate}"`, projectPath); | ||
| const revCount = countStr ? parseInt(countStr, 10) : commits; | ||
| const diff = runGit(`diff --shortstat HEAD~${Math.min(revCount, 50)}..HEAD`, projectPath); |
There was a problem hiding this comment.
P2: gitSummary can silently lose file-change statistics because it now relies on a single brittle HEAD~revCount..HEAD range with no fallback when the range is invalid.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At skills/ck/commands/shared.mjs, line 171:
<comment>`gitSummary` can silently lose file-change statistics because it now relies on a single brittle `HEAD~revCount..HEAD` range with no fallback when the range is invalid.</comment>
<file context>
@@ -165,9 +165,10 @@ export function gitSummary(projectPath, sinceDate) {
+ // Count unique files changed: use a separate runGit call to avoid nested shell substitution
+ const countStr = runGit(`rev-list --count HEAD --since="${sinceDate}"`, projectPath);
+ const revCount = countStr ? parseInt(countStr, 10) : commits;
+ const diff = runGit(`diff --shortstat HEAD~${Math.min(revCount, 50)}..HEAD`, projectPath);
if (diff) {
</file context>
| const entry = projects[cwd]; | ||
|
|
||
| // Read previous session BEFORE overwriting current-session.json | ||
| const prevSession = readJson(CURRENT_SESSION); |
There was a problem hiding this comment.
P2: Unsaved-session warning is not scoped to the previous session’s project, so it can fire incorrectly after switching projects.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At skills/ck/hooks/session-start.mjs, line 90:
<comment>Unsaved-session warning is not scoped to the previous session’s project, so it can fire incorrectly after switching projects.</comment>
<file context>
@@ -86,6 +86,9 @@ function main() {
const entry = projects[cwd];
+ // Read previous session BEFORE overwriting current-session.json
+ const prevSession = readJson(CURRENT_SESSION);
+
// Write current-session.json
</file context>
There was a problem hiding this comment.
🧹 Nitpick comments (4)
skills/ck/hooks/session-start.mjs (1)
29-45: Duplicated helpers could be imported fromshared.mjs.
readJson,daysAgo/daysAgoLabel, andstalenessIconare reimplemented here with minor format differences. Consider importing fromshared.mjsfor consistency and maintainability.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@skills/ck/hooks/session-start.mjs` around lines 29 - 45, This file reimplements utility helpers that live in shared.mjs—replace the local implementations of readJson, daysAgo (or daysAgoLabel), and stalenessIcon with imports from shared.mjs: remove the local functions readJson, daysAgo, and stalenessIcon, add imports for the canonical symbols (e.g., readJson, daysAgoLabel/daysAgo, stalenessIcon) from shared.mjs, and update any call sites in this module to use those imported names so formatting/semantics match the shared utilities.skills/ck/commands/shared.mjs (1)
183-186: Windows path separators are not encoded.The function only replaces forward slashes. On Windows, backslashes (
\) would remain, potentially causing filesystem issues or collisions.export function encodeProjectPath(absolutePath) { // "/Users/sree/dev/app" -> "-Users-sree-dev-app" - return absolutePath.replace(/\//g, '-'); + return absolutePath.replace(/[\\/]/g, '-'); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@skills/ck/commands/shared.mjs` around lines 183 - 186, The encodeProjectPath function only replaces forward slashes and misses Windows backslashes; update encodeProjectPath to replace both '/' and '\' (e.g. using a regex character class like [\/\\]) so Windows paths are normalized the same way as POSIX paths, and ensure the replacement preserves the existing behavior of turning separators into hyphens.skills/ck/commands/resume.mjs (1)
26-33: Directory check is incomplete.The previous shell injection was fixed, but
existsSyncreturnstruefor files too. The original suggestion included a directory check.-import { existsSync } from 'fs'; +import { existsSync, statSync } from 'fs';if (projectPath && projectPath !== cwd) { - if (existsSync(projectPath)) { + if (existsSync(projectPath) && statSync(projectPath).isDirectory()) { console.log(`→ cd ${projectPath}`); } else {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@skills/ck/commands/resume.mjs` around lines 26 - 33, The directory existence check using existsSync(projectPath) is insufficient because it returns true for files too; update the conditional that checks projectPath (the block that compares projectPath !== cwd and currently uses existsSync(projectPath)) to verify the path is a directory e.g. use fs.statSync(projectPath).isDirectory() (or fs.lstatSync with isDirectory()) combined with existsSync inside a try/catch to avoid throwing on broken symlinks; keep the same console messages but only print "→ cd ..." when the path exists and is a directory, otherwise print the "⚠ Path not found" or an appropriate "not a directory" warning.skills/ck/commands/save.mjs (1)
130-165:today()called multiple times may produce inconsistent dates.If the save spans midnight,
session.date(line 132), the memory filename (line 165), and content (line 178) could have different dates. Consider capturing the date once.+const todayStr = today(); + const session = { id: sessionId, - date: today(), + date: todayStr, summary: summary || 'Session saved', // ... };Then use
todayStrat lines 165 and 178.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@skills/ck/commands/save.mjs` around lines 130 - 165, The code calls today() multiple times causing potential inconsistencies across session.date, the memory filename (memFile) and memory content; capture the date once into a variable (e.g., todayStr) at the start of save logic, use that variable for session.date instead of today(), use it when constructing memFile (resolve(memDir, `ck_${todayStr}_${sessionId.slice(0,8)}.md`)) and anywhere else the date is needed (e.g., memory content or projects[cwd].lastUpdated) so all saved artifacts share the same date; update references in the session creation, memFile construction, and any subsequent writes (functions/variables: session, sessionId, memFile, nativeMemoryDir, projects[cwd].lastUpdated, writeProjects) to use todayStr.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@skills/ck/commands/resume.mjs`:
- Around line 26-33: The directory existence check using existsSync(projectPath)
is insufficient because it returns true for files too; update the conditional
that checks projectPath (the block that compares projectPath !== cwd and
currently uses existsSync(projectPath)) to verify the path is a directory e.g.
use fs.statSync(projectPath).isDirectory() (or fs.lstatSync with isDirectory())
combined with existsSync inside a try/catch to avoid throwing on broken
symlinks; keep the same console messages but only print "→ cd ..." when the path
exists and is a directory, otherwise print the "⚠ Path not found" or an
appropriate "not a directory" warning.
In `@skills/ck/commands/save.mjs`:
- Around line 130-165: The code calls today() multiple times causing potential
inconsistencies across session.date, the memory filename (memFile) and memory
content; capture the date once into a variable (e.g., todayStr) at the start of
save logic, use that variable for session.date instead of today(), use it when
constructing memFile (resolve(memDir,
`ck_${todayStr}_${sessionId.slice(0,8)}.md`)) and anywhere else the date is
needed (e.g., memory content or projects[cwd].lastUpdated) so all saved
artifacts share the same date; update references in the session creation,
memFile construction, and any subsequent writes (functions/variables: session,
sessionId, memFile, nativeMemoryDir, projects[cwd].lastUpdated, writeProjects)
to use todayStr.
In `@skills/ck/commands/shared.mjs`:
- Around line 183-186: The encodeProjectPath function only replaces forward
slashes and misses Windows backslashes; update encodeProjectPath to replace both
'/' and '\' (e.g. using a regex character class like [\/\\]) so Windows paths
are normalized the same way as POSIX paths, and ensure the replacement preserves
the existing behavior of turning separators into hyphens.
In `@skills/ck/hooks/session-start.mjs`:
- Around line 29-45: This file reimplements utility helpers that live in
shared.mjs—replace the local implementations of readJson, daysAgo (or
daysAgoLabel), and stalenessIcon with imports from shared.mjs: remove the local
functions readJson, daysAgo, and stalenessIcon, add imports for the canonical
symbols (e.g., readJson, daysAgoLabel/daysAgo, stalenessIcon) from shared.mjs,
and update any call sites in this module to use those imported names so
formatting/semantics match the shared utilities.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5c3418f0-cbb1-4881-b745-c56a9d4f2c80
📒 Files selected for processing (5)
skills/ck/commands/init.mjsskills/ck/commands/resume.mjsskills/ck/commands/save.mjsskills/ck/commands/shared.mjsskills/ck/hooks/session-start.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
- skills/ck/commands/init.mjs
|
Analysis Failed
Troubleshooting
Retry: |
Summary
Adds ck (Context Keeper) v2 — a skill that gives Claude Code persistent, per-project memory across sessions using deterministic Node.js scripts.
commands/*.mjshandle all logic; Claude calls them and displays output. Behavior is consistent across model versions (no LLM-interpreted prose instructions).CLAUDE.md.context.jsonas source of truth — structured JSON with full session history, session IDs, git activity per save.CONTEXT.mdis generated from it./ck:savewrites a memory entry to~/.claude/projects/*/memory/so decisions surface across sessions even without/ck:resume./ck:migrate— converts v1CONTEXT.md+meta.jsondata to v2context.json; supports--dry-run; backs up originals.Commands
/ck:init/ck:save/ck:resume [name|#]/ck:info [name|#]/ck:list/ck:forget/ck:migrateStructure
Source
Full repo with install script, changelog, and releases:
https://github.com/sreedhargs89/context-keeper
Test plan
/ck:initauto-detects info/ck:saveafter a session →context.jsonwritten,CONTEXT.mdregenerated/ck:resume,/ck:info,/ck:listrender correctly/ck:migrate --dry-runon v1 data → preview without writing🤖 Generated with Claude Code
Summary by cubic
Adds
ck(Context Keeper) v2 — deterministic Node.js scripts that give Claude Code persistent, per‑project memory and a compact session‑start briefing. Usescontext.jsonas the source of truth, generatesCONTEXT.md, writes native memory, and supports v1 → v2 via/ck:migrate.New Features
/ck:init,/ck:save,/ck:resume,/ck:info,/ck:list,/ck:forget,/ck:migrate.CLAUDE.md.~/.claude/projects/*/memory/.Bug Fixes
current-session.json.displayNamein briefings and lists (fallback to slug only if missing)..gitchecks and 3s timeouts;runGit()replaces subshells;resume.mjsusesfs.existsSync.init.mjsusespath.basename()for fallbacks; save confirmation shows the original name, not the slug.Written for commit 00787d6. Summary will update on new commits.
Summary by CodeRabbit