Skip to content

feat: add skillkit doctor command and enhance skillkit status#64

Merged
rohitg00 merged 6 commits intomainfrom
feat/doctor-status-commands
Feb 11, 2026
Merged

feat: add skillkit doctor command and enhance skillkit status#64
rohitg00 merged 6 commits intomainfrom
feat/doctor-status-commands

Conversation

@rohitg00
Copy link
Copy Markdown
Owner

@rohitg00 rohitg00 commented Feb 10, 2026

Summary

  • New skillkit doctor command — runs 10 health checks across environment, configuration, and skills categories with --json and --fix flags
  • Enhanced skillkit status — shows project overview dashboard (agent, config, version, skills count, recent history) when no active session exists

Changes

File Change
packages/cli/src/commands/doctor.ts New DoctorCommand with 10 checks
packages/cli/src/commands/status.ts Overview dashboard when no session
packages/cli/src/commands/index.ts Export DoctorCommand
apps/skillkit/src/cli.ts Register DoctorCommand

Test plan

  • pnpm build compiles cleanly
  • pnpm test — all 25 test suites pass
  • skillkit doctor — formatted output with all checks
  • skillkit doctor --json — valid JSON
  • skillkit doctor --fix — creates missing dirs, removes broken symlinks
  • skillkit status — shows overview when no session
  • skillkit status --json — includes overview data
  • CodeRabbit review — all 4 findings addressed

Open with Devin

Summary by CodeRabbit

  • New Features
    • Added CLI commands: doctor (health checks), activity (skill activity), session snapshot (save/restore/list/delete), session explain, and issue planner (issue plan & issue list).
    • Exposed ActivityLog, SnapshotManager, and SessionExplainer in the public API.
    • Status command now shows a project overview when no active session exists.
  • Documentation
    • Added Issue Planner docs and README examples.
  • Tests
    • New unit and end-to-end tests for activity logs, snapshots, session explainer, and issue planner.

@vercel
Copy link
Copy Markdown

vercel bot commented Feb 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
skillkit Ready Ready Preview, Comment Feb 11, 2026 8:01pm
skillkit-docs Ready Ready Preview, Comment Feb 11, 2026 8:01pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds multiple CLI commands (doctor, activity, issue plan/list, session snapshot save/restore/list/delete, session explain), new core session features (ActivityLog, SnapshotManager, SessionExplainer), an IssuePlanner implementation with types/tests, expanded docs/README, and registers/exports the commands in the CLI surface.

Changes

Cohort / File(s) Summary
CLI bootstrap & exports
apps/skillkit/src/cli.ts, packages/cli/src/commands/index.ts, packages/cli/src/__tests__/commands.test.ts
Imported, exported, and registered new commands (Doctor, Activity, IssuePlan/List, Session snapshot commands, SessionExplain); tests updated to assert the expanded public command surface.
New CLI commands
packages/cli/src/commands/doctor.ts, packages/cli/src/commands/activity.ts, packages/cli/src/commands/issue.ts, packages/cli/src/commands/session.ts, packages/cli/src/commands/status.ts
Added DoctorCommand (health checks with --fix/--json), ActivityCommand (activity reporting), IssuePlan/IssueList (GitHub issue planning/listing), session snapshot management and SessionExplain; status now builds an environment overview when no session active.
Session core: storage & explainer
packages/core/src/session/activity-log.ts, packages/core/src/session/snapshot-manager.ts, packages/core/src/session/session-explainer.ts, packages/core/src/session/index.ts, packages/core/src/session/types.ts
Introduced ActivityLog, SnapshotManager, SessionExplainer, new session-related types (activity, snapshot, explanation), unit tests, and re-exported these from session index.
Issue Planner core
packages/core/src/plan/issue-planner.ts, packages/core/src/plan/types.ts, packages/core/src/plan/index.ts, packages/core/src/plan/__tests__/issue-planner.test.ts
New IssuePlanner (ref parsing, gh issue fetch, task/file extraction, label→tag mapping, plan generation), new types and exports, and comprehensive unit tests.
Tests: e2e and unit additions
packages/cli/src/__tests__/e2e-session-features.test.ts, packages/core/src/session/__tests__/* (activity-log.test.ts, session-explainer.test.ts, snapshot-manager.test.ts)
Added an end-to-end CLI test suite for activity/session flows and extensive unit tests for ActivityLog, SnapshotManager, and SessionExplainer.
Docs & README
README.md, docs/fumadocs/content/docs/commands.mdx, docs/skillkit/components/Commands.tsx
Adds Issue Planner docs and README examples; UI docs updated to include an "Issues" command group (note: commands.mdx contains a duplicated block).

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI as "CLI\n(Command)"
    participant Env as "Environment\n(Node & Tools)"
    participant Config as "Config\nFiles"
    participant Agents as "Agent\nDetector"
    participant Skills as "Skills\nFilesystem"
    participant Output as "Output\nRenderer"

    User->>CLI: run `skillkit doctor` [--fix] [--json]
    CLI->>Env: probe Node, npm/pnpm, and tool versions
    Env-->>CLI: versions/status
    CLI->>Config: read/validate project config
    Config-->>CLI: config validity/agent
    CLI->>Agents: detect adapters/agents
    Agents-->>CLI: detected agents
    CLI->>Skills: inspect skill dirs, symlinks, and contents
    Skills-->>CLI: inventory & issues
    alt --fix provided
        CLI->>Skills: create missing dirs / remove broken symlinks
        Skills-->>CLI: actions taken
    end
    CLI->>Output: aggregate Environment, Config, Skills
    alt --json
        Output->>User: JSON payload
    else
        Output->>User: formatted health report
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I nibble on snapshots, hopping through code,
I plan from issues down each winding road.
I doctor the tree, log each little feat,
Save, restore, explain — every carrot neat.
Hooray for features — a crunchy, cozy treat.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: add skillkit doctor command and enhance skillkit status' accurately summarizes the primary changes: introduction of a new doctor command and improvements to the status command.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/doctor-status-commands

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 6 additional findings in Devin Review.

Open in Devin Review

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/cli/src/commands/doctor.ts`:
- Line 113: The code uses "as any" casts for detectedAgent when calling
getAdapter and getSearchDirs; instead, declare detectedAgent with the explicit
type AgentType (e.g., let detectedAgent: AgentType = 'universal') when assigning
from detectAgent(), then remove the two "as any" casts; update references to
detectedAgent in getAdapter(...) and getSearchDirs(...) to use the typed
variable so type safety is preserved (refer to detectedAgent, detectAgent,
AgentType, getAdapter, getSearchDirs).
🧹 Nitpick comments (5)
packages/cli/src/commands/doctor.ts (4)

286-294: Command injection risk in whichVersion — mitigate defensively.

Although whichVersion is private and currently only called with hardcoded literals ('npm', 'pnpm'), passing cmd directly into execSync is a shell-injection vector if a future caller ever passes untrusted input. A minimal safeguard (e.g., allowlist or execFileSync) eliminates this risk.

🛡️ Proposed safer alternative using execFileSync
+import { execFileSync } from 'node:child_process';
 ...
  private whichVersion(cmd: string): string | null {
    try {
-     return execSync(`${cmd} --version`, { encoding: 'utf-8', timeout: 5000 })
+     return execFileSync(cmd, ['--version'], { encoding: 'utf-8', timeout: 5000 })
        .trim()
        .replace(/^v/, '');
    } catch {
      return null;
    }
  }

execFileSync bypasses the shell entirely, avoiding injection even if cmd is ever sourced dynamically.


61-64: pnpm check is silently omitted when not found.

When npm is not found, a warning is pushed (Line 58). When pnpm is not found, nothing is pushed — the check just disappears from output. This is probably intentional (pnpm is optional), but the asymmetry may confuse users who expect to see all checks listed. Consider pushing a 'pass' result like "pnpm not installed (optional)" for visibility, or adding a brief comment explaining the intentional omission.


134-134: Hardcoded agent-to-directory mapping is fragile.

The ternary detectedAgent === 'claude-code' ? 'claude' : detectedAgent embeds knowledge about agent directory naming that likely belongs in the adapter layer (which already knows about skillsDir). If a new agent is added with a different directory convention, this line silently produces the wrong path.

Consider querying the adapter for the global skills directory instead, or at minimum extracting this mapping into a shared helper.


42-276: The execute() method is quite long (~235 lines) — consider extracting check functions.

Each check category (environment, configuration, agent detection, skills, symlinks) could be a private method returning CheckResult[]. This would improve readability and testability without changing behavior.

Additionally, there's notable duplication with buildOverview() in status.ts (agent detection, config loading, skills discovery). Extracting the shared data-gathering logic into a common helper would reduce drift between the two commands.

packages/cli/src/commands/status.ts (1)

140-185: Replace Record<string, unknown> with a typed interface to avoid unsafe casts.

buildOverview returns Record<string, unknown>, and showOverview immediately casts fields back (as number, as Array<…>) on lines 195 and 202. Since both methods are private and co-located, introducing a small interface eliminates the casts and catches shape mismatches at compile time.

♻️ Proposed interface
interface StatusOverview {
  agent: string;
  config: string | null;
  configAgent: string;
  version: string;
  totalSkills: number;
  projectSkills: number;
  globalSkills: number;
  skillsDir: string;
  recentHistory: Array<{ status: string; skillName: string; completedAt: string }>;
}

Then update signatures:

-private async buildOverview(manager: SessionManager): Promise<Record<string, unknown>> {
+private async buildOverview(manager: SessionManager): Promise<StatusOverview> {
...
-private showOverview(overview: Record<string, unknown>): void {
+private showOverview(overview: StatusOverview): void {

Also applies to: 187-215

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 12 additional findings in Devin Review.

Open in Devin Review

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@packages/cli/src/__tests__/commands.test.ts`:
- Around line 23-29: Add missing assertions to the test to verify the new
exports: in packages/cli/src/__tests__/commands.test.ts update the expectation
block that checks exported commands to also assert that DoctorCommand,
IssuePlanCommand, and IssueListCommand are defined (i.e., add
expect(commands.DoctorCommand).toBeDefined(),
expect(commands.IssuePlanCommand).toBeDefined(), and
expect(commands.IssueListCommand).toBeDefined()). Ensure the symbol names match
the exports in commands/index.ts so the test fails if the exports are not
present.

In `@packages/cli/src/commands/activity.ts`:
- Line 32: The code does parseInt(this.limit) without validation so a
non-numeric --limit yields NaN and silent empty output; change parsing to use
parseInt(this.limit, 10) and validate the result (e.g., const parsed =
parseInt(this.limit, 10); if (!Number.isFinite(parsed) || Number.isNaN(parsed))
{ log a concise warning/error to the user and default limit to 10 } then use
that validated value for limit (referenced as limit and this.limit in the file);
ensure you handle negative/zero values if needed and prefer a clear
error/warning before falling back.

In `@packages/cli/src/commands/issue.ts`:
- Around line 248-253: The JSON.parse(result) call that assigns to the issues
constant can throw on malformed gh output; wrap the parse in a try/catch (or use
a safe parse helper) around JSON.parse(result) to catch SyntaxError, validate
that result is non-empty before parsing, and on failure surface a user-friendly
error (e.g., log a clear message and exit gracefully or return an empty issues
array) referencing the same symbols: result and issues; update any downstream
code that assumes issues is an array to handle the fallback case.

In `@packages/core/src/plan/issue-planner.ts`:
- Around line 202-211: The ternary spreading an empty array makes includeTests a
no-op; update the steps passed to generator.addTask so the true branch yields
actual test step objects instead of an empty array. Specifically, in the addTask
call that builds steps (currently using ...(includeTests ? [] : [])), replace
the true branch with an array of test step descriptors (e.g. { type: 'test',
description: `Write tests for ${item.name}` } or similar) so when includeTests
is true additional test steps are appended to the steps array; keep the false
branch as an empty array to preserve current behavior.

In `@packages/core/src/plan/types.ts`:
- Around line 274-280: Remove the duplicate IssuePlanMetadata interface declared
in issue-planner.ts and instead import the canonical IssuePlanMetadata from
types.ts; update issue-planner.ts by deleting its local IssuePlanMetadata
declaration and adding an import statement for IssuePlanMetadata, then ensure
any references in issue-planner.ts use the imported type.

In `@packages/core/src/session/session-explainer.ts`:
- Around line 49-59: The code mistakenly sets explanation.agent to
exec.skillSource (a repo path) in the session-explainer, which misrepresents the
AI agent; update the logic in the block using state.currentExecution (where exec
is a CurrentExecution) to populate explanation.agent from the actual agent
source (e.g., a field like exec.agent, state.agent, session.agent, or
executionContext.provider) if such metadata exists, or if no agent info is
available add/derive an agent field on CurrentExecution and populate it where
executions are created; alternatively, rename explanation.agent to
explanation.skillSource to avoid confusion and update usages including
formatText to match the chosen change (reference symbols: explanation.agent,
exec.skillSource, CurrentExecution, formatText).
🧹 Nitpick comments (8)
packages/core/src/session/activity-log.ts (2)

75-80: getByCommit prefix match could return unexpected results for short/empty SHAs.

An empty string matches every commit ("".startsWith("") is true), and very short prefixes like "a" may match unrelated commits. Since .find() returns the first match, the result is non-deterministic when multiple commits share a prefix. Consider adding a minimum-length guard:

🛡️ Suggested guard
 getByCommit(sha: string): SkillActivity | undefined {
+  if (sha.length < 4) return undefined;
   const data = this.load();
   return data.activities.find(
     (a) => a.commitSha === sha || a.commitSha.startsWith(sha)
   );
 }

45-51: save() doesn't handle write errors.

If writeFileSync throws (e.g., disk full, permissions), the error propagates unhandled up through record(). The in-memory this.data will already be mutated, creating a divergence between memory and disk. Consider wrapping with a try/catch or documenting this contract.

packages/cli/src/commands/session.ts (3)

360-365: Double type assertion through unknown masks potential type mismatches.

new ObservationStore(projectPath).getAll() as unknown as Array<Record<string, unknown>> forces the type. If SnapshotManager.save() expects a specific type for observations, consider aligning the types properly rather than casting through unknown.


395-404: Restore uses Object.assign to mutate internal state directly.

This couples the CLI command to the internal shape of SessionManager's state object. If SessionManager changes its internal representation or adds validation on mutation, this will break silently. Consider adding a dedicated restore or merge method on SessionManager instead.


343-367: Remove unnecessary double type assertion on observations.

new ObservationStore(projectPath).getAll() already returns Observation[] with proper typing. The cast as unknown as Array<Record<string, unknown>> is unnecessary and loses type safety. Cast directly to the expected type or update the SnapshotManager.save() signature to accept Observation[] if that's what's available.

packages/core/src/session/snapshot-manager.ts (1)

40-58: Unsafe cast of observations parameter bypasses type safety.

The observations parameter is typed as Array<Record<string, unknown>> but is cast to SessionSnapshot['observations'] (which requires specific fields like id, timestamp, sessionId, agent, type, content, relevance). This means callers can pass any shape of objects without compile-time errors, and restored snapshots may have missing fields at runtime.

Consider aligning the parameter type with SessionSnapshot['observations'] to get compile-time guarantees:

Proposed fix
  save(
    name: string,
    sessionState: SessionState,
-   observations: Array<Record<string, unknown>>,
+   observations: SessionSnapshot['observations'],
    description?: string
  ): void {
    this.ensureDir();

    const snapshot: SessionSnapshot = {
      version: 1,
      name,
      createdAt: new Date().toISOString(),
      description,
      sessionState,
-     observations: observations as SessionSnapshot['observations'],
+     observations,
    };

    writeFileSync(this.getPath(name), stringify(snapshot));
  }
packages/cli/src/__tests__/e2e-session-features.test.ts (1)

84-118: Redundant require() calls — use top-level imports instead.

writeFileSync is already imported at line 3, and yaml could be imported at the top level. The inline require('yaml') (line 85) and require('node:fs') (line 114) are unnecessary and mix CJS require with ESM-style imports.

Suggested cleanup

Add stringify from yaml to the top-level imports:

+import { stringify } from 'yaml';

Then remove the inline require calls from the test bodies at lines 85, 114, and 161–162.

packages/core/src/plan/issue-planner.ts (1)

108-113: JSON.parse of gh output is unguarded.

If gh returns non-JSON (e.g., HTML error page from a proxy), JSON.parse throws a generic error. The CLI command catches this, but the error message won't be user-friendly. Consider wrapping with a descriptive error:

Proposed fix
-    const data = JSON.parse(result);
+    let data: Record<string, unknown>;
+    try {
+      data = JSON.parse(result);
+    } catch {
+      throw new Error(`Failed to parse GitHub CLI response for issue ${parsed.number}`);
+    }

Comment on lines +49 to +59
if (state.currentExecution) {
const exec = state.currentExecution;
explanation.agent = exec.skillSource || 'unknown';

const startTime = new Date(exec.startedAt).getTime();
explanation.duration = formatDuration(Date.now() - startTime);

explanation.skillsUsed.push({
name: exec.skillName,
status: exec.status,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

explanation.agent is populated with skillSource (repo path), not the actual agent name.

exec.skillSource is the skill's repository (e.g., "anthropics/skills"), but the field name agent and its usage in formatText (Line 137) suggest it should represent the AI agent (e.g., "cursor", "claude"). The CurrentExecution type doesn't carry an agent field, so this may need to be sourced differently or the field renamed to avoid confusion in the output.

🤖 Prompt for AI Agents
In `@packages/core/src/session/session-explainer.ts` around lines 49 - 59, The
code mistakenly sets explanation.agent to exec.skillSource (a repo path) in the
session-explainer, which misrepresents the AI agent; update the logic in the
block using state.currentExecution (where exec is a CurrentExecution) to
populate explanation.agent from the actual agent source (e.g., a field like
exec.agent, state.agent, session.agent, or executionContext.provider) if such
metadata exists, or if no agent info is available add/derive an agent field on
CurrentExecution and populate it where executions are created; alternatively,
rename explanation.agent to explanation.skillSource to avoid confusion and
update usages including formatText to match the chosen change (reference
symbols: explanation.agent, exec.skillSource, CurrentExecution, formatText).

- New `skillkit doctor` command with 10 health checks across 3 categories
  (environment, configuration, skills) with --json and --fix flags
- Enhanced `skillkit status` shows project overview dashboard when no
  active session exists instead of bare "No active session found" message
- Overview includes agent, config, version, skills count, recent history
Three new session intelligence features:

- Activity Log: tracks which skills were active per git commit
  (`skillkit activity` with --skill, --limit, --json flags)
- Session Snapshots: save/restore named snapshots of session state
  (`skillkit session snapshot save|restore|list|delete`)
- Session Explain: structured summary of current session
  (`skillkit session explain` with --json, --no-git flags)

Includes path traversal protection on snapshot names, YAML validation
on parsed activity data, and robust error handling on snapshot restore.

983 tests passing (942 core + 41 CLI including 14 E2E tests).
New `skillkit issue plan` and `skillkit issue list` commands that
fetch GitHub Issues via `gh` CLI and generate StructuredPlans.
Parses checkboxes, file mentions, and labels from issue body.
29 new tests, all passing.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@packages/cli/src/commands/doctor.ts`:
- Line 134: Replace the hardcoded globalSkillsDir construction with the
canonical mapping from AGENT_CONFIG: import AGENT_CONFIG from '@skillkit/core',
then set globalSkillsDir to AGENT_CONFIG[detectedAgent].globalSkillsDir with any
leading '~' replaced by homedir(); reference the existing detectedAgent and
globalSkillsDir symbols and ensure you handle a missing mapping by falling back
to the previous join(homedir(), `.${detectedAgent === 'claude-code' ? 'claude' :
detectedAgent}`, 'skills') behavior if AGENT_CONFIG[detectedAgent] is undefined.

In `@packages/cli/src/commands/status.ts`:
- Around line 140-185: Replace the untyped Record<string, unknown> return with a
concrete interface (e.g. StatusOverview or Overview) that lists the properties
returned by buildOverview (agent, config, configAgent, version, totalSkills,
projectSkills, globalSkills, skillsDir, recentHistory) and their proper types;
change buildOverview signature to return that interface and update any callers
(notably showOverview) to use the typed interface so you can remove the unsafe
casts (e.g. `as number`, `as Array<...>`). Ensure you import or declare the
interface in the same module (or a shared types module) and update
getAdapter/manager return types if needed to satisfy the new property types
(e.g. adapter.skillsDir, recentHistory).

In `@packages/core/src/session/session-explainer.ts`:
- Line 99: state.decisions can be undefined and calling .map on it will throw;
update the assignment in session-explainer.ts so that explanation.decisions is
populated from a safe default (e.g., use (state.decisions ?? []) or optional
chaining) before mapping; specifically change the expression that sets
explanation.decisions to handle undefined SessionState.decisions so mapping runs
over an empty array when absent.
- Around line 82-95: state.history may be undefined and iterating it with
for...of will throw; change the loop to iterate over a safe default (e.g.,
state.history ?? []) so the code defensively handles missing history entries.
Update the block that reads state.history and still populates
explanation.skillsUsed and explanation.filesModified (the same places that check
hist.skillName, hist.status, and hist.filesModified) to iterate the fallback
empty array.
🧹 Nitpick comments (16)
packages/core/src/session/activity-log.ts (3)

22-43: Loaded data is cached indefinitely — no version field validation.

Two minor observations on load():

  1. The version field from the parsed YAML is not validated (Line 33 only checks parsed.activities). If the file has version: 2 with a different schema, it will still be loaded as ActivityLogData. For forward-compatibility, consider rejecting unknown versions.
  2. Once this.data is set, subsequent calls always return the cached value. This is fine for a short-lived CLI process, but worth noting if this class is ever reused in a long-running context.

Neither is blocking for a CLI tool.


75-80: Prefix matching in getByCommit can return an ambiguous result.

a.commitSha.startsWith(sha) will match the first activity whose SHA shares the prefix. If multiple activities have SHAs with the same prefix, the caller gets whichever appears first (most recent) with no indication of ambiguity. This mirrors git's short-SHA behavior, so it's reasonable — just worth documenting or logging a warning if multiple matches exist.


45-51: save() could write null if called before load().

this.data is initialized as null. While currently save() is only reachable via record() (which calls load() first), a future refactor calling save() directly would silently write "null\n" to disk. A small guard would make the class more robust.

🛡️ Defensive check
  private save(): void {
+   if (!this.data) return;
    const dir = join(this.projectPath, '.skillkit');
    if (!existsSync(dir)) {
      mkdirSync(dir, { recursive: true });
    }
    writeFileSync(this.filePath, stringify(this.data));
  }
packages/core/src/session/__tests__/activity-log.test.ts (2)

323-333: Consider adding a test for record() after encountering a corrupted file.

The corrupted-file test only exercises the read path (getRecent). It would be valuable to also verify that record() gracefully recovers from a corrupted file — i.e., loads an empty log and successfully writes a new entry. This ensures the full recovery path works end-to-end.


32-109: existsSync mock doesn't differentiate file vs. directory checks.

In record tests, existsSync is mocked to return a single value, but the implementation calls it for both the YAML file (in load()) and the .skillkit directory (in save()). The tests work because the broad mock doesn't cause failures, but more targeted mocking (e.g., using mockImplementation keyed on path) would make the tests more precise and less brittle.

packages/cli/src/commands/doctor.ts (2)

286-294: Prefer execFileSync over template-literal execSync to avoid shell injection surface.

While cmd is currently always a hardcoded literal ('npm', 'pnpm'), using execSync with string interpolation opens a shell injection surface if this helper is ever called with dynamic input. execFileSync avoids spawning a shell entirely and is safer by default.

Suggested fix
+ import { execFileSync } from 'node:child_process';
  ...
  private whichVersion(cmd: string): string | null {
    try {
-     return execSync(`${cmd} --version`, { encoding: 'utf-8', timeout: 5000 })
+     return execFileSync(cmd, ['--version'], { encoding: 'utf-8', timeout: 5000 })
        .trim()
        .replace(/^v/, '');
    } catch {
      return null;
    }
  }

66-67: SkillKit version always reports pass even when unknown.

If this.cli.binaryVersion is nullish, the version resolves to 'unknown' but is still reported as 'pass'. Consider using 'warn' status to surface that the version couldn't be determined.

Suggested fix
     const skillkitVersion = this.cli.binaryVersion ?? 'unknown';
-    results.push({ name: 'SkillKit version', status: 'pass', message: `skillkit v${skillkitVersion}` });
+    results.push({
+      name: 'SkillKit version',
+      status: skillkitVersion === 'unknown' ? 'warn' : 'pass',
+      message: `skillkit v${skillkitVersion}`,
+    });
packages/core/src/plan/issue-planner.ts (1)

108-127: Wrap JSON.parse with a more descriptive error for gh CLI failures.

If gh returns non-JSON output (e.g., authentication errors, stderr redirected to stdout), JSON.parse(result) will throw a generic SyntaxError. Consider catching and re-throwing with context about the gh command failure.

Suggested improvement
-    const data = JSON.parse(result);
+    let data: Record<string, unknown>;
+    try {
+      data = JSON.parse(result);
+    } catch {
+      throw new Error(
+        `Failed to parse GitHub issue data. Ensure "gh" is authenticated and the issue exists.\nRaw output: ${result.slice(0, 200)}`
+      );
+    }
packages/cli/src/commands/status.ts (1)

163-163: Same as any issue as doctor.ts — use an explicit AgentType annotation for agent.

Lines 163 and 171 cast agent with as any for getSearchDirs and getAdapter. The same fix applies: declare agent with the AgentType type so both calls are type-safe without casts.

Also applies to: 171-171

packages/cli/src/commands/issue.ts (1)

53-95: Plan generation / validation errors are unhandled after the fetchIssue try/catch.

If planner.generatePlan, validator.validate, generator.toMarkdown, mkdir, or writeFile throw, the error propagates unhandled, producing an unfriendly stack trace. Consider wrapping lines 79–95 in a try/catch that writes a clear error message to stderr and returns 1.

Proposed fix
     const plan = planner.generatePlan(issue, {
       agent: this.agent,
       techStack: techStackArr,
       includeTests: !this.noTests,
     });
 
-    const validator = createPlanValidator();
-    const validation = validator.validate(plan);
-
-    const generator = createPlanGenerator();
-    const markdown = generator.toMarkdown(plan);
-
-    const outputPath =
-      this.output || resolve(`.skillkit/plans/issue-${issue.number}.md`);
-
-    await mkdir(dirname(outputPath), { recursive: true });
-    await writeFile(outputPath, markdown, 'utf-8');
+    let validation;
+    let outputPath: string;
+    try {
+      const validator = createPlanValidator();
+      validation = validator.validate(plan);
+
+      const generator = createPlanGenerator();
+      const markdown = generator.toMarkdown(plan);
+
+      outputPath =
+        this.output || resolve(`.skillkit/plans/issue-${issue.number}.md`);
+
+      await mkdir(dirname(outputPath), { recursive: true });
+      await writeFile(outputPath, markdown, 'utf-8');
+    } catch (err) {
+      const message = err instanceof Error ? err.message : String(err);
+      this.context.stderr.write(`Error generating plan: ${message}\n`);
+      return 1;
+    }
packages/core/src/session/session-explainer.ts (1)

114-124: Git commit filtering fetches up to 50 commits but only counts today's — consider passing since to reduce I/O.

getGitCommits supports a since option. Passing today's date as since would avoid fetching commits from previous days that are immediately discarded.

Proposed fix
       try {
-        const commits = getGitCommits(this.projectPath, { commits: 50 });
         const today = new Date().toISOString().split('T')[0];
-        explanation.gitCommits = commits.filter((c) =>
-          c.date.startsWith(today)
-        ).length;
+        const commits = getGitCommits(this.projectPath, { commits: 50, since: today });
+        explanation.gitCommits = commits.length;
       } catch {
packages/cli/src/commands/session.ts (3)

395-404: Snapshot restore silently drops observations.

manager.restore() returns both sessionState and observations, but only sessionState is applied here. If a user saved a snapshot expecting to capture observations, restoring it won't bring them back. Consider restoring observations via ObservationStore or at minimum documenting this limitation.


399-403: Object.assign partial overwrite may cause subtle state inconsistencies.

This copies currentExecution, history, and decisions from the snapshot but retains the current state's other fields (e.g., lastActivity, projectPath, version). If the snapshot was taken from a different project path or version, the hybrid state could be inconsistent. Consider either doing a full replacement (preserving only projectPath) or documenting which fields are restored.


360-365: Double type cast (as unknown as Array<Record<string, unknown>>) is a code smell.

This suggests a type mismatch between ObservationStore.getAll() return type and what SnapshotManager.save() expects. Consider aligning the types at the interface level rather than casting at the call site.

packages/cli/src/__tests__/e2e-session-features.test.ts (2)

85-85: Prefer top-level imports over inline require() calls.

writeFileSync is already imported at Line 3 from node:fs, and yaml could be imported at the top of the file. Using require() inline is non-idiomatic in an ESM/vitest project and adds unnecessary overhead per test.

Proposed fix (at the top of the file)
 import { describe, it, expect, beforeEach, afterEach } from 'vitest';
 import { execSync } from 'node:child_process';
-import { mkdirSync, rmSync, existsSync, readFileSync } from 'node:fs';
+import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
 import { join } from 'node:path';
 import { tmpdir } from 'node:os';
+import { stringify } from 'yaml';

Then replace the inline require calls:

-      const { stringify } = require('yaml');
       // ...
-      const { writeFileSync } = require('node:fs');

Also applies to: 114-114, 161-162


9-20: run() helper swallows all errors and returns a best-effort string.

The catch block returns err.stdout || err.stderr || err.message. If stdout is an empty string (falsy), it falls through to stderr or message, potentially hiding the real output. Consider using err.stdout ?? err.stderr ?? err.message (nullish coalescing) instead of || to preserve empty-string stdout.

Proposed fix
   } catch (err: any) {
-    return err.stdout || err.stderr || err.message;
+    return err.stdout ?? err.stderr ?? err.message;
   }

}
}

const globalSkillsDir = join(homedir(), `.${detectedAgent === 'claude-code' ? 'claude' : detectedAgent}`, 'skills');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for globalSkillsDir, global skills dir patterns, and related patterns
rg -n --type=ts 'globalSkillsDir\|global.*skills.*dir\|globalDir' -g '!node_modules' -g '!*.test.*' -C 3

Repository: rohitg00/skillkit

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Also search for adapter-related code and agent configuration
rg -n --type=ts 'adapter.*skills\|getGlobalDir\|globalDirectory\|skills.*dir' -g '!node_modules' -g '!*.test.*' -C 3

Repository: rohitg00/skillkit

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Check how the file packages/cli/src/commands/doctor.ts uses agent directories
cat -n packages/cli/src/commands/doctor.ts | head -150

Repository: rohitg00/skillkit

Length of output: 6731


🏁 Script executed:

#!/bin/bash
# Search for agent adapter implementations to understand the architecture
fd -t f 'adapter' -path '*/src/*' --type f | head -20

Repository: rohitg00/skillkit

Length of output: 229


🏁 Script executed:

#!/bin/bash
# Look for agent-related configuration or constants that map agent names to directories
rg -n --type=ts 'claude-code\|detectedAgent' -g '!node_modules' -g '!*.test.*' -C 2

Repository: rohitg00/skillkit

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Find adapter-related files
find . -type f -name '*adapter*' | grep -v node_modules | head -20

Repository: rohitg00/skillkit

Length of output: 156


🏁 Script executed:

#!/bin/bash
# Look for the agents package and its structure
find . -path '*/agents/*' -type f -name '*.ts' | grep -v node_modules | head -30

Repository: rohitg00/skillkit

Length of output: 1157


🏁 Script executed:

#!/bin/bash
# Search for the adapter interface or getAdapter implementation
rg -l 'getAdapter\|interface.*Adapter\|class.*Adapter' --type=ts -g '!node_modules' | head -10

Repository: rohitg00/skillkit

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Look for any agent name mapping or configuration
rg -n 'claude-code\|agent.*name\|agent.*mapping' --type=ts -g '!node_modules' -C 2 | head -50

Repository: rohitg00/skillkit

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Check the base adapter interface
cat -n packages/agents/src/base.ts | head -100

Repository: rohitg00/skillkit

Length of output: 1105


🏁 Script executed:

#!/bin/bash
# Check claude-code agent implementation
cat -n packages/agents/src/claude-code.ts

Repository: rohitg00/skillkit

Length of output: 2936


🏁 Script executed:

#!/bin/bash
# Check factory to understand getAdapter
cat -n packages/agents/src/factory.ts

Repository: rohitg00/skillkit

Length of output: 2838


🏁 Script executed:

#!/bin/bash
# Check all agent adapters to see how many have the agent-name-to-dir mapping
for file in packages/agents/src/*.ts; do
  if [[ "$file" == *test* ]] || [[ "$file" == *features* ]] || [[ "$file" == *index* ]] || [[ "$file" == *factory* ]] || [[ "$file" == *base* ]]; then
    continue
  fi
  echo "=== $(basename $file) ==="
  grep -n "homedir\|join.*\." "$file" | head -5
done

Repository: rohitg00/skillkit

Length of output: 3919


🏁 Script executed:

#!/bin/bash
# Get the full getAdapter/getAllAdapters implementation
rg -A 30 'export.*function.*getAdapter|export.*function.*getAllAdapters' packages/agents/src/index.ts

Repository: rohitg00/skillkit

Length of output: 868


🏁 Script executed:

#!/bin/bash
# Find where AGENT_CONFIG is defined
rg -n 'AGENT_CONFIG\|export.*config' packages/core/src --type=ts | head -30

Repository: rohitg00/skillkit

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Check the core package for AGENT_CONFIG definition
find packages/core/src -name '*.ts' | xargs grep -l "AGENT_CONFIG"

Repository: rohitg00/skillkit

Length of output: 204


🏁 Script executed:

#!/bin/bash
# Check the agent-config.ts to see if it has globalSkillsDir
cat -n packages/core/src/agent-config.ts | head -200

Repository: rohitg00/skillkit

Length of output: 6641


Use centralized AGENT_CONFIG for global skills directory mapping.

The hardcoded mapping breaks for agents with non-standard global paths. For example, opencode should resolve to ~/.config/opencode/skills and windsurf to ~/.codeium/windsurf/skills, but the current logic constructs ~/.opencode/skills and ~/.windsurf/skills respectively. Instead of manual path construction, import AGENT_CONFIG from @skillkit/core and use AGENT_CONFIG[detectedAgent].globalSkillsDir (after replacing ~ with homedir()), which already has the correct mappings for all agents.

🤖 Prompt for AI Agents
In `@packages/cli/src/commands/doctor.ts` at line 134, Replace the hardcoded
globalSkillsDir construction with the canonical mapping from AGENT_CONFIG:
import AGENT_CONFIG from '@skillkit/core', then set globalSkillsDir to
AGENT_CONFIG[detectedAgent].globalSkillsDir with any leading '~' replaced by
homedir(); reference the existing detectedAgent and globalSkillsDir symbols and
ensure you handle a missing mapping by falling back to the previous
join(homedir(), `.${detectedAgent === 'claude-code' ? 'claude' :
detectedAgent}`, 'skills') behavior if AGENT_CONFIG[detectedAgent] is undefined.

Comment on lines +82 to +95
for (const hist of state.history) {
const alreadyListed = explanation.skillsUsed.some(
(s) => s.name === hist.skillName
);
if (!alreadyListed) {
explanation.skillsUsed.push({
name: hist.skillName,
status: hist.status,
});
}
if (hist.filesModified) {
explanation.filesModified.push(...hist.filesModified);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

state.history may also be undefined if the session YAML is incomplete.

Same concern as state.decisions — if history is missing from the deserialized state, iterating with for...of will throw. Defensively default to an empty array.

Proposed fix
-    for (const hist of state.history) {
+    for (const hist of state.history ?? []) {
📝 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.

Suggested change
for (const hist of state.history) {
const alreadyListed = explanation.skillsUsed.some(
(s) => s.name === hist.skillName
);
if (!alreadyListed) {
explanation.skillsUsed.push({
name: hist.skillName,
status: hist.status,
});
}
if (hist.filesModified) {
explanation.filesModified.push(...hist.filesModified);
}
}
for (const hist of state.history ?? []) {
const alreadyListed = explanation.skillsUsed.some(
(s) => s.name === hist.skillName
);
if (!alreadyListed) {
explanation.skillsUsed.push({
name: hist.skillName,
status: hist.status,
});
}
if (hist.filesModified) {
explanation.filesModified.push(...hist.filesModified);
}
}
🤖 Prompt for AI Agents
In `@packages/core/src/session/session-explainer.ts` around lines 82 - 95,
state.history may be undefined and iterating it with for...of will throw; change
the loop to iterate over a safe default (e.g., state.history ?? []) so the code
defensively handles missing history entries. Update the block that reads
state.history and still populates explanation.skillsUsed and
explanation.filesModified (the same places that check hist.skillName,
hist.status, and hist.filesModified) to iterate the fallback empty array.


explanation.filesModified = [...new Set(explanation.filesModified)];

explanation.decisions = state.decisions.map(({ key, value }) => ({ key, value }));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

state.decisions may be undefined, causing a runtime crash.

If SessionState is deserialized from YAML and the decisions field is absent, state.decisions will be undefined. Calling .map() on undefined throws a TypeError.

Proposed fix
-    explanation.decisions = state.decisions.map(({ key, value }) => ({ key, value }));
+    explanation.decisions = (state.decisions ?? []).map(({ key, value }) => ({ key, value }));
🤖 Prompt for AI Agents
In `@packages/core/src/session/session-explainer.ts` at line 99, state.decisions
can be undefined and calling .map on it will throw; update the assignment in
session-explainer.ts so that explanation.decisions is populated from a safe
default (e.g., use (state.decisions ?? []) or optional chaining) before mapping;
specifically change the expression that sets explanation.decisions to handle
undefined SessionState.decisions so mapping runs over an empty array when
absent.

- doctor.ts: use execFileSync, typed AgentType, remove as-any casts
- activity.ts: validate parseInt with radix and NaN guard
- issue.ts: wrap JSON.parse in try/catch for gh CLI output
- issue-planner.ts: guard JSON.parse, fix includeTests no-op ternary,
  remove duplicate IssuePlanMetadata (import from types.ts)
- status.ts: typed StatusOverview interface, remove as-any casts
- snapshot-manager.ts: align observations param with SessionSnapshot type
- session.ts: proper type mapping for observations (no double cast)
- activity-log.ts: min-length guard for SHA prefix match
- e2e tests: use top-level imports instead of inline require()
- commands.test.ts: add assertions for DoctorCommand, IssuePlanCommand,
  IssueListCommand
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/core/src/plan/issue-planner.ts`:
- Around line 220-233: The no-checklist fallback creates a single implement step
and ignores the includeTests flag; update the branch around generator.addTask
(where files: PlanTaskFiles is built from allFileMentions) to conditionally
append a { type: 'test', description: issue.title } step when includeTests is
true (same pattern used in the checklist branch), so the steps array passed to
generator.addTask(plan, issue.title, {...}) contains both implement and test
steps when includeTests is enabled.
🧹 Nitpick comments (4)
packages/core/src/session/activity-log.ts (1)

30-40: Consider validating version field before casting to ActivityLogData.

Line 33 checks that parsed.activities is an array but doesn't validate parsed.version === 1. The ActivityLogData interface requires version: 1, so a file with a different or missing version would be silently accepted with a potentially incompatible structure.

Suggested tightening
       const parsed = parse(content);
-      if (parsed && Array.isArray(parsed.activities)) {
+      if (parsed && parsed.version === 1 && Array.isArray(parsed.activities)) {
         this.data = parsed as ActivityLogData;
packages/cli/src/__tests__/e2e-session-features.test.ts (1)

10-21: run() helper silently swallows all errors — consider preserving exit code.

When execSync throws (non-zero exit), the helper returns err.stdout || err.stderr || err.message without indicating failure. Tests that assert on error messages work fine, but a test that accidentally expects success output from a crashing command would silently pass with error output. This is acceptable for current test patterns but could be fragile if tests evolve.

packages/cli/src/commands/session.ts (1)

362-375: Observation mapping uses multiple as casts that bypass type safety.

Lines 368-370 cast o.agent, o.type, and o.content without runtime guards. If ObservationStore.getAll() returns records where these fields are undefined or unexpected types, the snapshot data will silently contain incorrect values.

Consider adding runtime fallbacks:

Suggested defensive mapping
       observations = raw.map((o) => ({
         id: o.id,
         timestamp: o.timestamp,
         sessionId: o.sessionId,
-        agent: o.agent as string,
-        type: o.type as string,
-        content: { ...o.content } as Record<string, unknown>,
+        agent: String(o.agent ?? 'unknown'),
+        type: String(o.type ?? 'unknown'),
+        content: (o.content && typeof o.content === 'object' ? { ...o.content } : {}) as Record<string, unknown>,
         relevance: o.relevance,
       }));
packages/cli/src/commands/doctor.ts (1)

93-111: Nested async calls inside a sync-looking loop — verify adapter detection is quick.

The adapter.isDetected() call inside the loop (line 99) is awaited individually per adapter. If any adapter's detection is slow (e.g., network call), this will block sequentially. Consider using Promise.allSettled for parallel detection if latency becomes a concern.

Parallel detection alternative
-    try {
-      const allAdapters = getAllAdapters();
-      for (const adapter of allAdapters) {
-        if (adapter.agentType === detectedAgent || adapter.agentType === 'universal') continue;
-        try {
-          if (await adapter.isDetected()) {
-            otherDetected.push(adapter.agentType);
-          }
-        } catch {
-          // skip
-        }
-      }
+    try {
+      const allAdapters = getAllAdapters()
+        .filter(a => a.agentType !== detectedAgent && a.agentType !== 'universal');
+      const results = await Promise.allSettled(allAdapters.map(a => a.isDetected()));
+      for (let i = 0; i < allAdapters.length; i++) {
+        const result = results[i];
+        if (result.status === 'fulfilled' && result.value) {
+          otherDetected.push(allAdapters[i].agentType);
+        }
+      }

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 16 additional findings in Devin Review.

Open in Devin Review

- Include test step in no-checklist fallback when includeTests is true
- Restore observations from snapshot in SessionSnapshotRestoreCommand
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/cli/src/commands/session.ts`:
- Around line 405-414: Explain in a short comment above the selective assign
where manager.restore(...) is applied that only currentExecution, history, and
decisions from the restored SessionState are intentionally copied into the
active SessionManager (via SessionManager.getOrCreate() and Object.assign(...))
while other fields such as lastActivity and projectPath are intentionally not
restored so the session is anchored to the current project context and
lastActivity is refreshed on save; mention SessionState and the specific fields
(currentExecution, history, decisions, lastActivity, projectPath) so future
maintainers understand the design choice.
🧹 Nitpick comments (3)
packages/core/src/plan/issue-planner.ts (2)

35-53: Minor: the g flag on the module-level FILE_PATH_REGEX constant is unused.

The constant's g flag is never leveraged because extractFileMentions (line 150) constructs a fresh RegExp from FILE_PATH_REGEX.source. Removing g from the constant makes it clear the stateful flag is only on the local copy.

Also, the static analysis warning about ReDoS on line 150 is a false positive — the source is a compile-time constant, not user input.

Suggested tweak
-const FILE_PATH_REGEX = /`([a-zA-Z0-9_./\-]+\.[a-zA-Z0-9]+)`/g;
+const FILE_PATH_REGEX = /`([a-zA-Z0-9_./\-]+\.[a-zA-Z0-9]+)`/;

88-130: Wrap execFileSync to surface a friendlier error when gh is missing or fails.

If the GitHub CLI isn't installed, execFileSync throws an ENOENT error with no context about what went wrong. Similarly, auth failures or network errors will surface raw child-process messages. A try/catch with a user-oriented message would improve the developer experience.

Proposed improvement
+    let result: string;
+    try {
-    const result = execFileSync('gh', args, {
+      result = execFileSync('gh', args, {
         encoding: 'utf-8',
         timeout: 15_000,
       });
+    } catch (err: unknown) {
+      const code = (err as NodeJS.ErrnoException).code;
+      if (code === 'ENOENT') {
+        throw new Error(
+          'GitHub CLI (gh) is not installed or not on PATH. Install it from https://cli.github.com/'
+        );
+      }
+      throw new Error(
+        `Failed to fetch issue #${parsed.number} via GitHub CLI: ${(err as Error).message}`
+      );
+    }
packages/cli/src/commands/session.ts (1)

416-430: Observation restore uses fragile Parameters<> type indexing.

The positional Parameters<ObservationStore['add']>[0], [1], [2] casts are brittle — if the add method signature is reordered or extended, these will silently pass incorrect values at runtime. Consider a more explicit approach, or at minimum add a comment documenting the expected add signature.

Comment on lines +405 to +414
const { sessionState, observations } = manager.restore(this.name);

const sessionMgr = new SessionManager(projectPath);
const currentState = sessionMgr.getOrCreate();
Object.assign(currentState, {
currentExecution: sessionState.currentExecution,
history: sessionState.history,
decisions: sessionState.decisions,
});
sessionMgr.save();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find SessionState interface definition
ast-grep --pattern 'interface SessionState {
  $$$
}'

Repository: rohitg00/skillkit

Length of output: 988


🏁 Script executed:

#!/bin/bash
# First, let's look at the broader context of the restore operation
sed -n '400,420p' packages/cli/src/commands/session.ts

# Also check what getOrCreate() does
fd -t f 'SessionManager' packages/ | head -5

Repository: rohitg00/skillkit

Length of output: 749


🏁 Script executed:

#!/bin/bash
# Find SessionManager class
fd -t f 'SessionManager' packages/ --exec grep -l 'class SessionManager' {}

# Get more details about SessionManager
cat -n packages/core/src/session/SessionManager.ts | head -100

Repository: rohitg00/skillkit

Length of output: 136


🏁 Script executed:

#!/bin/bash
# Search for SessionManager class definition
rg 'class SessionManager' packages/ -t ts -A 20

# Also search for the getOrCreate method
rg 'getOrCreate' packages/ -t ts -B 3 -A 10

Repository: rohitg00/skillkit

Length of output: 4874


🏁 Script executed:

#!/bin/bash
# Find the init() method that creates a fresh session
rg 'init\(\).*SessionState' packages/core/src/session/manager.ts -A 20

Repository: rohitg00/skillkit

Length of output: 476


🏁 Script executed:

#!/bin/bash
# Check the restore() method and what it does
rg 'restore\(' packages/ -t ts -B 5 -A 20 | head -60

# Also check if there are any comments about the snapshot/restore design
rg -i 'snapshot|restore' packages/cli/src/commands/session.ts -B 2 -A 2

Repository: rohitg00/skillkit

Length of output: 9518


🏁 Script executed:

#!/bin/bash
# Check if there's any documentation or comments around the snapshot/restore feature
rg -i 'snapshot' packages/core/src/session/ -t ts -B 2 -A 2 | head -80

# Also verify the complete flow by checking what the snapshot save captures
rg 'save\(.*state.*observations' packages/core/src/session/snapshot-manager.ts -B 5 -A 15

Repository: rohitg00/skillkit

Length of output: 5468


🏁 Script executed:

#!/bin/bash
# Let's check the complete context one more time to see if there are any comments
# or documentation about why selective restore is done this way
sed -n '395,425p' packages/cli/src/commands/session.ts

Repository: rohitg00/skillkit

Length of output: 1128


🏁 Script executed:

#!/bin/bash
# Check the save() method to see if it updates lastActivity
rg 'save\(\)' packages/core/src/session/manager.ts -A 15

Repository: rohitg00/skillkit

Length of output: 3205


🏁 Script executed:

#!/bin/bash
# Let me verify the flow one more time by checking what happens in init()
# to understand the complete picture
sed -n '20,45p' packages/core/src/session/manager.ts

Repository: rohitg00/skillkit

Length of output: 649


🏁 Script executed:

#!/bin/bash
# Check for any tests related to snapshot restore to understand intended behavior
rg 'SessionSnapshotRestoreCommand|restore.*snapshot' packages/ -t ts -B 3 -A 10 | head -100

# Also check if there's any documentation or design decision about this
find packages -name "*.md" -type f | xargs grep -l "snapshot\|restore" 2>/dev/null | head -3

Repository: rohitg00/skillkit

Length of output: 8253


🏁 Script executed:

#!/bin/bash
# Let me check the e2e test to understand the intended behavior better
sed -n '200,280p' packages/cli/src/__tests__/e2e-session-features.test.ts

Repository: rohitg00/skillkit

Length of output: 1380


🏁 Script executed:

#!/bin/bash
# Find the actual restore test that checks the behavior
rg 'should save and restore snapshot' packages/cli/src/__tests__/e2e-session-features.test.ts -A 80

Repository: rohitg00/skillkit

Length of output: 3046


Add a clarifying comment for the selective field restore.

The code intentionally restores only currentExecution, history, and decisions from the snapshot, while lastActivity is refreshed to the current time (via save()) and projectPath remains bound to the current project. This design preserves execution history while anchoring the restored session to the current context. However, without documentation, future maintainers—or developers adding new SessionState fields—may not realize this is intentional. Add a comment explaining the selective restore strategy:

     const currentState = sessionMgr.getOrCreate();
+    // Restore execution history into current session, preserving session identity
+    // (version, lastActivity, projectPath stay current; only execution data is restored)
     Object.assign(currentState, {
       currentExecution: sessionState.currentExecution,
       history: sessionState.history,
       decisions: sessionState.decisions,
     });
     sessionMgr.save();
🤖 Prompt for AI Agents
In `@packages/cli/src/commands/session.ts` around lines 405 - 414, Explain in a
short comment above the selective assign where manager.restore(...) is applied
that only currentExecution, history, and decisions from the restored
SessionState are intentionally copied into the active SessionManager (via
SessionManager.getOrCreate() and Object.assign(...)) while other fields such as
lastActivity and projectPath are intentionally not restored so the session is
anchored to the current project context and lastActivity is refreshed on save;
mention SessionState and the specific fields (currentExecution, history,
decisions, lastActivity, projectPath) so future maintainers understand the
design choice.

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 19 additional findings in Devin Review.

Open in Devin Review

- doctor.ts: Use getAdapter() per AgentType instead of getAllAdapters()
  spread (which destroys prototype methods like isDetected)
- doctor.ts: Use getGlobalSkillsDir() from AGENT_CONFIG instead of
  hardcoded path formula
- session-explainer.ts: Defensively default state.history and
  state.decisions to empty arrays
- session-explainer.ts: Use loadConfig().agent with skillSource fallback
  instead of always using skillSource as agent name
- observation-store.ts: Add static readAll() that reads observations
  without sessionId mismatch clearing
- session.ts: Use ObservationStore.readAll() for snapshot save to avoid
  empty observations due to sessionId mismatch
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 24 additional findings in Devin Review.

Open in Devin Review

Comment on lines +418 to +426
const store = new ObservationStore(projectPath);
for (const obs of observations) {
store.add(
obs.type as Parameters<ObservationStore['add']>[0],
obs.content as unknown as Parameters<ObservationStore['add']>[1],
obs.agent as Parameters<ObservationStore['add']>[2],
obs.relevance,
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Snapshot restore silently wipes existing observations and loses original observation metadata

When restoring a snapshot via SessionSnapshotRestoreCommand, observations from the snapshot lose their original IDs, timestamps, and sessionIds because ObservationStore.add() is used instead of directly writing the snapshot data.

Root Cause and Impact

At packages/cli/src/commands/session.ts:418, a new ObservationStore(projectPath) is created without a sessionId, so a random UUID is generated (packages/core/src/memory/observation-store.ts:40). When store.add() is called for the first restored observation, the internal load() method reads the existing observations file and compares sessionIds. Since the random UUID never matches the file's sessionId, line 60-62 of observation-store.ts clears the entire observations array:

if (this.data.sessionId !== this.sessionId) {
  this.data.sessionId = this.sessionId;
  this.data.observations = [];  // wipes all existing observations
}

Then each restored observation is re-created with add(), which generates new metadata:

const observation: Observation = {
  id: randomUUID(),                    // new ID, original lost
  timestamp: new Date().toISOString(), // current time, original lost
  sessionId: this.sessionId,           // random UUID, original lost
  agent, type, content, relevance,
};

Impact:

  1. Any observations from the current session that weren't in the snapshot are permanently destroyed.
  2. Restored observations get fabricated timestamps (all set to the restore time instead of when they originally occurred), breaking any time-based analysis.
  3. Original observation IDs are replaced with new UUIDs, invalidating any external references.
Prompt for agents
In SessionSnapshotRestoreCommand.execute() (packages/cli/src/commands/session.ts, lines 416-430), instead of using ObservationStore.add() to restore observations (which generates new IDs, timestamps, and sessionIds for each observation), directly write the snapshot's observation data to the observations YAML file. You could either:

1. Add a new method like ObservationStore.replaceAll(observations) that writes the raw observation data without generating new metadata, OR
2. Use ObservationStore.readAll / writeFileSync to directly write the snapshot observations to the file at the known path (.skillkit/memory/observations.yaml), bypassing the add() method entirely.

This preserves the original observation IDs, timestamps, and sessionIds from the snapshot.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@rohitg00 rohitg00 merged commit aafa086 into main Feb 11, 2026
10 checks passed
@rohitg00 rohitg00 deleted the feat/doctor-status-commands branch February 11, 2026 20:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant