Skip to content

Commit aafa086

Browse files
authored
feat: add skillkit doctor command and enhance skillkit status (#64)
* feat: add `skillkit doctor` command and enhance `skillkit status` - 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 * feat: add skill activity log, session snapshots, and session explain 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). * feat: add issue planner - generate plans from GitHub Issues 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. * fix: address CodeRabbit review suggestions for PR #64 - 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 * fix: address review feedback from CodeRabbit and Devin - Include test step in no-checklist fallback when includeTests is true - Restore observations from snapshot in SessionSnapshotRestoreCommand * fix: address remaining CodeRabbit and Devin review feedback - 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
1 parent 344f081 commit aafa086

File tree

25 files changed

+3451
-16
lines changed

25 files changed

+3451
-16
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,14 @@ skillkit find <query> # Quick search
246246
skillkit scan <path> # Security scan for skills
247247
```
248248

249+
### Issue Planner
250+
251+
```bash
252+
skillkit issue plan "#42" # Plan from GitHub Issue
253+
skillkit issue plan owner/repo#42 # Cross-repo plan
254+
skillkit issue list # List open issues
255+
```
256+
249257
### Advanced
250258

251259
```bash

apps/skillkit/src/cli.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ import {
8686
SessionNoteCommand,
8787
SessionCompleteCommand,
8888
SessionInProgressCommand,
89+
SessionSnapshotSaveCommand,
90+
SessionSnapshotRestoreCommand,
91+
SessionSnapshotListCommand,
92+
SessionSnapshotDeleteCommand,
93+
SessionExplainCommand,
94+
ActivityCommand,
8995
ProfileCommand,
9096
ProfileListCommand,
9197
ProfileCreateCommand,
@@ -104,11 +110,14 @@ import {
104110
SkillMdCheckCommand,
105111
ServeCommand,
106112
ScanCommand,
113+
DoctorCommand,
107114
SaveCommand,
108115
AgentsMdCommand,
109116
AgentsMdInitCommand,
110117
AgentsMdSyncCommand,
111118
AgentsMdShowCommand,
119+
IssuePlanCommand,
120+
IssueListCommand,
112121
} from '@skillkit/cli';
113122

114123
const __filename = fileURLToPath(import.meta.url);
@@ -215,6 +224,12 @@ cli.register(SessionListCommand);
215224
cli.register(SessionNoteCommand);
216225
cli.register(SessionCompleteCommand);
217226
cli.register(SessionInProgressCommand);
227+
cli.register(SessionSnapshotSaveCommand);
228+
cli.register(SessionSnapshotRestoreCommand);
229+
cli.register(SessionSnapshotListCommand);
230+
cli.register(SessionSnapshotDeleteCommand);
231+
cli.register(SessionExplainCommand);
232+
cli.register(ActivityCommand);
218233

219234
cli.register(ProfileCommand);
220235
cli.register(ProfileListCommand);
@@ -238,10 +253,14 @@ cli.register(SkillMdCheckCommand);
238253

239254
cli.register(ServeCommand);
240255
cli.register(ScanCommand);
256+
cli.register(DoctorCommand);
241257
cli.register(SaveCommand);
242258
cli.register(AgentsMdCommand);
243259
cli.register(AgentsMdInitCommand);
244260
cli.register(AgentsMdSyncCommand);
245261
cli.register(AgentsMdShowCommand);
246262

263+
cli.register(IssuePlanCommand);
264+
cli.register(IssueListCommand);
265+
247266
cli.runExit(process.argv.slice(2));

docs/fumadocs/content/docs/commands.mdx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,41 @@ skillkit serve --cache-ttl 3600000
343343
| GET | `/health` | Server health |
344344
| GET | `/cache/stats` | Cache statistics |
345345

346+
## Issue Planner
347+
348+
Generate structured plans from GitHub Issues. Requires the `gh` CLI.
349+
350+
```bash
351+
skillkit issue plan "#42" # Plan from current repo
352+
skillkit issue plan rohitg00/skillkit#42 # Cross-repo
353+
skillkit issue plan "#42" --agent cursor # Target specific agent
354+
skillkit issue plan "#42" --no-tests # Skip test steps
355+
skillkit issue plan "#42" --json # JSON output
356+
skillkit issue plan "#42" --tech-stack react,ts # Override tech stack
357+
```
358+
359+
### Issue Plan Options
360+
361+
| Option | Short | Default | Purpose |
362+
|--------|-------|---------|---------|
363+
| `--agent` | `-a` | `claude-code` | Target agent |
364+
| `--output` | `-o` | `.skillkit/plans/issue-<n>.md` | Output file path |
365+
| `--no-tests` || `false` | Skip test steps |
366+
| `--json` || `false` | Output as JSON |
367+
| `--tech-stack` ||| Comma-separated tech stack |
368+
369+
### Issue List
370+
371+
```bash
372+
skillkit issue list # List open issues
373+
skillkit issue list --repo owner/repo # Specific repo
374+
skillkit issue list --label bug # Filter by label
375+
skillkit issue list --limit 20 # More results
376+
skillkit issue list --json # JSON output
377+
```
378+
379+
The planner extracts task lists (checkboxes) from the issue body, infers file mentions from backtick-quoted paths, and maps labels to tags (e.g., `bug``fix`, `enhancement``feature`). Generated plans work with `skillkit plan validate` and `skillkit plan execute`.
380+
346381
## Utility Commands
347382

348383
```bash

docs/skillkit/components/Commands.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,17 @@ const COMMAND_GROUPS: CommandGroup[] = [
9393
{ cmd: 'validate', desc: 'Validate format' },
9494
],
9595
},
96+
{
97+
name: 'Issues',
98+
commands: [
99+
{ cmd: 'issue plan "#42"', desc: 'Plan from issue' },
100+
{ cmd: 'issue plan owner/repo#42', desc: 'Cross-repo plan' },
101+
{ cmd: 'issue plan "#42" --agent cursor', desc: 'Target agent' },
102+
{ cmd: 'issue plan "#42" --json', desc: 'JSON output' },
103+
{ cmd: 'issue list', desc: 'List open issues' },
104+
{ cmd: 'issue list --label bug', desc: 'Filter by label' },
105+
],
106+
},
96107
{
97108
name: 'Advanced',
98109
commands: [

packages/cli/src/__tests__/commands.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ describe('CLI Commands', () => {
2020
expect(commands.TranslateCommand).toBeDefined();
2121
expect(commands.ContextCommand).toBeDefined();
2222
expect(commands.RecommendCommand).toBeDefined();
23+
24+
expect(commands.ActivityCommand).toBeDefined();
25+
expect(commands.DoctorCommand).toBeDefined();
26+
expect(commands.IssuePlanCommand).toBeDefined();
27+
expect(commands.IssueListCommand).toBeDefined();
28+
expect(commands.SessionSnapshotSaveCommand).toBeDefined();
29+
expect(commands.SessionSnapshotRestoreCommand).toBeDefined();
30+
expect(commands.SessionSnapshotListCommand).toBeDefined();
31+
expect(commands.SessionSnapshotDeleteCommand).toBeDefined();
32+
expect(commands.SessionExplainCommand).toBeDefined();
2333
});
2434
});
2535
});
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2+
import { execSync } from 'node:child_process';
3+
import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
4+
import { join } from 'node:path';
5+
import { tmpdir } from 'node:os';
6+
import { stringify } from 'yaml';
7+
8+
const CLI = join(__dirname, '../../../../apps/skillkit/dist/cli.js');
9+
10+
function run(args: string, cwd: string): string {
11+
try {
12+
return execSync(`node ${CLI} ${args}`, {
13+
cwd,
14+
encoding: 'utf-8',
15+
timeout: 15000,
16+
env: { ...process.env, NO_COLOR: '1' },
17+
});
18+
} catch (err: any) {
19+
return err.stdout || err.stderr || err.message;
20+
}
21+
}
22+
23+
describe('E2E: Session Features', () => {
24+
let testDir: string;
25+
26+
beforeEach(() => {
27+
testDir = join(tmpdir(), `skillkit-e2e-${Date.now()}`);
28+
mkdirSync(testDir, { recursive: true });
29+
});
30+
31+
afterEach(() => {
32+
rmSync(testDir, { recursive: true, force: true });
33+
});
34+
35+
describe('skillkit activity', () => {
36+
it('should show empty state message', () => {
37+
const output = run('activity', testDir);
38+
expect(output).toContain('No activity recorded');
39+
});
40+
41+
it('should return empty JSON array', () => {
42+
const output = run('activity --json', testDir);
43+
const parsed = JSON.parse(output.trim());
44+
expect(parsed).toEqual([]);
45+
});
46+
47+
it('should accept --skill filter without error', () => {
48+
const output = run('activity --skill code-simplifier', testDir);
49+
expect(output).toContain('No activity recorded');
50+
});
51+
52+
it('should accept --limit flag without error', () => {
53+
const output = run('activity --limit 5', testDir);
54+
expect(output).toContain('No activity recorded');
55+
});
56+
});
57+
58+
describe('skillkit session snapshot', () => {
59+
it('should list no snapshots initially', () => {
60+
const output = run('session snapshot list', testDir);
61+
expect(output).toContain('No snapshots found');
62+
});
63+
64+
it('should return empty JSON for snapshot list', () => {
65+
const output = run('session snapshot list --json', testDir);
66+
const parsed = JSON.parse(output.trim());
67+
expect(parsed).toEqual([]);
68+
});
69+
70+
it('should fail to save without active session', () => {
71+
const output = run('session snapshot save test-snap', testDir);
72+
expect(output).toContain('No active session');
73+
});
74+
75+
it('should fail to restore nonexistent snapshot', () => {
76+
const output = run('session snapshot restore nonexistent', testDir);
77+
expect(output).toContain('not found');
78+
});
79+
80+
it('should fail to delete nonexistent snapshot', () => {
81+
const output = run('session snapshot delete nonexistent', testDir);
82+
expect(output).toContain('not found');
83+
});
84+
85+
it('should save and restore snapshot with active session', () => {
86+
const skillkitDir = join(testDir, '.skillkit');
87+
mkdirSync(skillkitDir, { recursive: true });
88+
89+
const sessionState = {
90+
version: 1,
91+
lastActivity: new Date().toISOString(),
92+
projectPath: testDir,
93+
currentExecution: {
94+
skillName: 'code-simplifier',
95+
skillSource: 'local',
96+
currentStep: 1,
97+
totalSteps: 2,
98+
status: 'running',
99+
startedAt: new Date().toISOString(),
100+
tasks: [
101+
{
102+
id: 'task-1',
103+
name: 'Test task',
104+
type: 'auto',
105+
status: 'completed',
106+
},
107+
],
108+
},
109+
history: [],
110+
decisions: [{ key: 'test-key', value: 'test-value', madeAt: new Date().toISOString() }],
111+
};
112+
113+
writeFileSync(
114+
join(skillkitDir, 'session.yaml'),
115+
stringify(sessionState)
116+
);
117+
118+
const saveOutput = run('session snapshot save my-test-snap --desc "E2E test"', testDir);
119+
expect(saveOutput).toContain('Snapshot saved: my-test-snap');
120+
121+
const snapshotFile = join(skillkitDir, 'snapshots', 'my-test-snap.yaml');
122+
expect(existsSync(snapshotFile)).toBe(true);
123+
124+
const listOutput = run('session snapshot list', testDir);
125+
expect(listOutput).toContain('my-test-snap');
126+
expect(listOutput).toContain('E2E test');
127+
128+
const restoreOutput = run('session snapshot restore my-test-snap', testDir);
129+
expect(restoreOutput).toContain('Snapshot restored: my-test-snap');
130+
131+
const deleteOutput = run('session snapshot delete my-test-snap', testDir);
132+
expect(deleteOutput).toContain('Snapshot deleted: my-test-snap');
133+
134+
expect(existsSync(snapshotFile)).toBe(false);
135+
});
136+
});
137+
138+
describe('skillkit session explain', () => {
139+
it('should show empty session summary', () => {
140+
const output = run('session explain --no-git', testDir);
141+
expect(output).toContain('Session Summary');
142+
expect(output).toContain('Agent:');
143+
expect(output).toContain('Files Modified: 0 files');
144+
});
145+
146+
it('should return valid JSON', () => {
147+
const output = run('session explain --json --no-git', testDir);
148+
const parsed = JSON.parse(output.trim());
149+
expect(parsed.date).toBeDefined();
150+
expect(parsed.agent).toBe('unknown');
151+
expect(parsed.skillsUsed).toEqual([]);
152+
expect(parsed.observationCounts).toBeDefined();
153+
expect(typeof parsed.observationCounts.errors).toBe('number');
154+
expect(typeof parsed.observationCounts.solutions).toBe('number');
155+
expect(typeof parsed.observationCounts.patterns).toBe('number');
156+
});
157+
158+
it('should explain session with active execution', () => {
159+
const skillkitDir = join(testDir, '.skillkit');
160+
mkdirSync(skillkitDir, { recursive: true });
161+
162+
const sessionState = {
163+
version: 1,
164+
lastActivity: new Date().toISOString(),
165+
projectPath: testDir,
166+
currentExecution: {
167+
skillName: 'remotion-best-practices',
168+
skillSource: 'claude-code',
169+
currentStep: 2,
170+
totalSteps: 3,
171+
status: 'running',
172+
startedAt: new Date(Date.now() - 3600000).toISOString(),
173+
tasks: [
174+
{
175+
id: 't1',
176+
name: 'Setup video',
177+
type: 'auto',
178+
status: 'completed',
179+
startedAt: new Date(Date.now() - 3600000).toISOString(),
180+
completedAt: new Date(Date.now() - 1800000).toISOString(),
181+
filesModified: ['src/video.tsx'],
182+
},
183+
{
184+
id: 't2',
185+
name: 'Add effects',
186+
type: 'auto',
187+
status: 'in_progress',
188+
startedAt: new Date(Date.now() - 1800000).toISOString(),
189+
},
190+
],
191+
},
192+
history: [],
193+
decisions: [
194+
{ key: 'codec', value: 'h264', madeAt: new Date().toISOString() },
195+
],
196+
};
197+
198+
writeFileSync(
199+
join(skillkitDir, 'session.yaml'),
200+
stringify(sessionState)
201+
);
202+
203+
const output = run('session explain --no-git', testDir);
204+
expect(output).toContain('Session Summary');
205+
expect(output).toContain('Duration:');
206+
expect(output).toContain('claude-code');
207+
expect(output).toContain('remotion-best-practices');
208+
expect(output).toContain('Setup video');
209+
expect(output).toContain('Add effects');
210+
expect(output).toContain('codec');
211+
212+
const jsonOutput = run('session explain --json --no-git', testDir);
213+
const parsed = JSON.parse(jsonOutput.trim());
214+
expect(parsed.skillsUsed).toHaveLength(1);
215+
expect(parsed.skillsUsed[0].name).toBe('remotion-best-practices');
216+
expect(parsed.tasks).toHaveLength(2);
217+
expect(parsed.decisions).toHaveLength(1);
218+
expect(parsed.filesModified).toContain('src/video.tsx');
219+
});
220+
});
221+
222+
describe('skillkit session (help)', () => {
223+
it('should list all subcommands including new ones', () => {
224+
const output = run('session', testDir);
225+
expect(output).toContain('session explain');
226+
expect(output).toContain('session snapshot save');
227+
expect(output).toContain('session snapshot restore');
228+
expect(output).toContain('session snapshot list');
229+
expect(output).toContain('session snapshot delete');
230+
});
231+
});
232+
});

0 commit comments

Comments
 (0)