Skip to content
1 change: 1 addition & 0 deletions docs/users/features/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ These commands invoke bundled skills that provide specialized workflows.
| ------------ | ------------------------------------------------------------------- | ------------------------------------------------- |
| `/review` | Review code changes with 5 parallel agents + deterministic analysis | `/review`, `/review 123`, `/review 123 --comment` |
| `/loop` | Run a prompt on a recurring schedule | `/loop 5m check the build` |
| `/simplify` | Review recent changes and apply safe cleanup edits directly | `/simplify`, `/simplify focus on duplication` |
| `/qc-helper` | Answer questions about Qwen Code usage and configuration | `/qc-helper how do I configure MCP?` |

See [Code Review](./code-review.md) for full `/review` documentation.
Expand Down
20 changes: 20 additions & 0 deletions packages/cli/src/services/BundledSkillLoader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,26 @@ describe('BundledSkillLoader', () => {
expect(commands.map((c) => c.name)).toEqual(['review', 'deploy']);
});

it('should load simplify bundled skill like other slash commands', async () => {
const skills = [
makeSkill({
name: 'simplify',
description: 'Simplify recent changes',
filePath: '/bundled/simplify/SKILL.md',
body: 'Simplify body',
}),
];
mockSkillManager.listSkills.mockResolvedValue(skills);

const loader = new BundledSkillLoader(mockConfig);
const commands = await loader.loadCommands(signal);

expect(commands).toHaveLength(1);
expect(commands[0].name).toBe('simplify');
expect(commands[0].description).toBe('Simplify recent changes');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The new simplify loading test is missing the listSkills call assertion that existing similar tests include.

Add after expect(commands[0].kind).toBe(CommandKind.SKILL);:

Suggested change
expect(commands[0].description).toBe('Simplify recent changes');
expect(mockSkillManager.listSkills).toHaveBeenCalledWith({
level: 'bundled',
});

— deepseek-v4-pro via Qwen Code /review

expect(commands[0].kind).toBe(CommandKind.SKILL);
});

it('should resolve {{model}} template variable in skill body', async () => {
const skill = makeSkill({
body: 'Review by {{model}} via Qwen Code',
Expand Down
123 changes: 123 additions & 0 deletions packages/core/src/skills/bundled/simplify/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
---
name: simplify
description: Review recent code changes for reuse, code quality, and efficiency, then directly apply straightforward cleanup improvements. Use when the user wants a post-implementation cleanup pass, pre-PR polish, or asks to simplify/refine recent changes. Invoke with `/simplify` or `/simplify <focus>`.
Comment thread
pomelo-nwu marked this conversation as resolved.
allowedTools:
- agent
- run_shell_command
- grep_search
- read_file
- write_file
- edit
- glob
---

# Simplify Recent Changes

You are running a structured cleanup workflow over recent code changes. Your goal is not just to comment on the code, but to safely improve it.

## Step 1: Identify the review scope

Determine which files and changes to review.

1. First inspect the current git state.
2. If there are staged changes, review against `HEAD` so both staged and unstaged tracked changes are included.
3. Otherwise review the current uncommitted diff.
4. If there is no git diff, fall back to `git ls-files --modified --others --exclude-standard` so the scope respects `.gitignore` (this keeps build output, `node_modules`, and other ignored paths out of the cleanup).
5. If that is still empty, fall back to files edited in this conversation.
6. If you still cannot identify a meaningful scope, stop and tell the user there are no recent changes to simplify.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Step 1 includes staged changes in the auto-cleanup scope: "If there are staged changes, review against HEAD so both staged and unstaged tracked changes are included."

A user who carefully curated their staging via git add -p will have their staged commits silently modified by auto-cleanup. Consider either (a) limiting scope to unstaged changes only, or (b) warning the user when staged changes exist and asking for confirmation.

Suggested change
6. If you still cannot identify a meaningful scope, stop and tell the user there are no recent changes to simplify.
1. First inspect the current git state.
2. If there are staged changes, warn the user: "Staged changes detected. /simplify will review and may modify both staged and unstaged tracked changes. Continue?" and only proceed if the user confirms.
3. Otherwise review the current uncommitted diff.

— deepseek-v4-pro via Qwen Code /review


Preferred commands:

- `git diff --name-only`
- `git diff --staged --name-only`
- `git diff HEAD --name-only`
- `git diff`
- `git diff HEAD`
- `git status --short`

Use `git diff HEAD` whenever staged changes exist. Otherwise use `git diff`.

## Step 2: Launch three review passes in parallel

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Step 2 launches 3 parallel sub-agents but provides no error handling for agent failures (timeout, crash, tool rejection). If an agent silently fails, the LLM may hang, skip findings, or hallucinate results with no diagnostic output. The /review skill provides a template for handling this scenario.

Suggested change
## Step 2: Launch three review passes in parallel
Use the `agent` tool and launch all review passes in a single response so they run concurrently. Each pass must receive the same review scope and diff command.
If any single pass fails (timeout, tool rejection, no output), log a warning and continue with remaining passes. If all passes fail, stop and tell the user: "All review passes failed — cannot produce simplification suggestions."

— deepseek-v4-pro via Qwen Code /review


Use the `agent` tool and launch all review passes in a single response so they run concurrently. Each pass must receive the same review scope and diff command. These passes are read-only: each one inspects and reports findings only and must not modify files — all edits happen later in Step 4.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] Sub-agents launched in Step 2 inherit full tool permissions (write_file, edit, run_shell_command) with no read-only constraint.

The agent tool calls in Step 2 do not specify subagent_type and do not instruct sub-agents to be read-only. This means 3 parallel "review" passes could accidentally modify files concurrently during the review phase — causing race-condition corruption, stale data for aggregation, and duplicate edits when the main agent applies fixes in Step 4.

Suggested change
Use the `agent` tool with `subagent_type: "general-purpose"` and launch all review passes in a single response so they run concurrently. Each pass must receive the same review scope and diff command.
In each pass prompt, add: "You are a read-only reviewer. Do NOT modify any files. Only inspect, read, and report findings."

— deepseek-v4-pro via Qwen Code /review

Keep each review prompt short and focused. Do not paste the full diff into the prompt. Tell each pass to read the diff itself and inspect only files relevant to its findings.

### Pass 1: Code Reuse Review

Look for opportunities to reduce duplication and reuse existing code:
Comment thread
pomelo-nwu marked this conversation as resolved.

- existing utilities or helpers that should be reused
- duplicated logic introduced in new code
- inline logic that should delegate to an existing abstraction
- ad-hoc helpers for string, path, env, parsing, or type checks when a project utility already exists

### Pass 2: Code Quality Review

Look for maintainability issues:

- copy-paste variants that should be unified
- parameter sprawl or awkward APIs
- redundant state or indirection
- abstraction leaks
- stringly-typed code that should be modeled more clearly
- unnecessary nesting
- unnecessary comments that explain what instead of why
- naming or structure that does not match surrounding code

### Pass 3: Efficiency Review

Look for wasteful work and unnecessary overhead:

- repeated work that can be memoized, cached, or removed
- serial work that can be parallelized safely
- unnecessary scans, allocations, reads, or traversals
- hot-path blocking work
- redundant no-op updates
- overly broad operations when a narrower one would work
- existence-check patterns that introduce TOCTOU style waste or risk

## Step 3: Aggregate findings

Wait for all three passes to finish, then merge overlapping findings.

Prioritize fixes that are:

- low risk
- local in scope
- clearly aligned with existing project patterns
- easy to validate with tests or targeted commands

Do not force a cleanup if it would require speculative architectural changes.

## Step 4: Apply straightforward improvements

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Step 4 directly modifies files via write_file/edit without instructing the agent to check for uncommitted changes, create a backup (git stash), or provide a rollback path when Step 5 verification fails. If the user has dirty working-tree changes, the agent's edits become intermixed.

Suggested change
## Step 4: Apply straightforward improvements
Directly implement safe cleanup improvements.
Before editing any file, check `git status --porcelain`. If there are uncommitted tracked changes, run `git stash push -m "simplify: pre-cleanup backup"` to preserve them.
Examples of good automatic fixes:

— deepseek-v4-pro via Qwen Code /review


Directly implement safe cleanup improvements.

Examples of good automatic fixes:

- replace duplicated logic with an existing helper
- remove redundant code, but only after a repository-wide search confirms it has no remaining callers
- simplify conditionals or control flow
- tighten loops or repeated work
- reduce unnecessary state or wrapper code
- remove low-value comments
- align code with nearby conventions

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] "remove dead or redundant code" is listed as an example of a safe automatic fix, but LLMs cannot reliably identify dead code without full call-graph visibility.

Code that has no callers within the diff scope may still be active elsewhere in the repository. This could lead to silent deletion of live code paths.

Suggested change
- remove redundant code (confirmed by grep-searching the full repository for callers)
- simplify conditionals or control flow

— deepseek-v4-pro via Qwen Code /review

Skip items that are uncertain, risky, or too invasive. Do not spend time debating rejected findings; simply move on.

## Step 5: Verify the cleanup

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Step 5 instructs the agent to run tests/typecheck/lint after cleanup but does not specify what to do when verification fails — revert the changes, fix and retry, or just report to the user. This leaves the agent's behavior unpredictable on failure.

Suggested change
## Step 5: Verify the cleanup
After making changes:
1. Run focused tests for the changed area when they exist.
2. Run the relevant project quality checks you can identify for the touched code.
3. If there are no applicable tests, at least run a targeted build, typecheck, or lint command that covers the edited files.
If any verification step fails, revert all changes with `git checkout .` (or `git stash pop` if you stashed earlier), and report the failure to the user. Do not attempt to fix verification failures.

— deepseek-v4-pro via Qwen Code /review


After making changes:

1. Run focused tests for the changed area when they exist.
2. Run the relevant project quality checks you can identify for the touched code.
3. If there are no applicable tests, at least run a targeted build, typecheck, or lint command that covers the edited files.

Prefer targeted verification over whole-repo commands unless the project only exposes repo-wide checks.

## Additional focus

If the user supplied extra instructions after `/simplify`, treat them as additional review focus and prioritize them alongside the default dimensions.

The raw user invocation appears below when present. Use it to extract any extra focus such as performance, duplication, rendering, API clarity, testability, or naming consistency.
56 changes: 48 additions & 8 deletions packages/core/src/skills/skill-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -667,43 +667,76 @@ Skill 3 content`);
isSymbolicLink: () => false,
};

const simplifyDirEntry = {
name: 'simplify',
isDirectory: () => true,
isFile: () => false,
isSymbolicLink: () => false,
};

const emptyDir = [] as unknown as Awaited<ReturnType<typeof fs.readdir>>;

function mockReaddirForLevels(levels: Set<string>) {
vi.mocked(fs.readdir).mockImplementation((dirPath) => {
const pathStr = String(dirPath);
const isBundled =
pathStr.endsWith(bundledDirSegment) && !pathStr.includes('.qwen');
const isBundled = pathStr.endsWith(bundledDirSegment);
const isProject =
pathStr.includes(projectDirSegment) &&
pathStr.startsWith(projectPrefix);
const isUser =
pathStr.includes(userDirSegment) && pathStr.startsWith(userPrefix);

if (levels.has('bundled') && isBundled) {
return Promise.resolve([
reviewDirEntry,
simplifyDirEntry,
] as unknown as Awaited<ReturnType<typeof fs.readdir>>);
}

if (
(levels.has('bundled') && isBundled) ||
(levels.has('project') && isProject) ||
(levels.has('user') && isUser)
) {
return Promise.resolve([reviewDirEntry] as unknown as Awaited<
ReturnType<typeof fs.readdir>
>);
}

return Promise.resolve(emptyDir);
});
}

function setupReviewSkillMocks() {
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readFile).mockResolvedValue(`---
vi.mocked(fs.readFile).mockImplementation(async (filePath) => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The new fs.readFile.mockImplementation has an unconditional fall-through that returns the review YAML for any path not containing ${path.sep}simplify${path.sep}. There is no path-shape validation, no error default. When a 3rd bundled skill is added later (e.g., a qc-helper bundled variant or any new entry), a test using setupReviewSkillMocks will silently receive the review SKILL.md body for that skill's path read — yielding a false positive (skill loads with wrong content) rather than a clear failure.

Also: this mock pairs with the mockParseYaml.mockImplementation below, where the two branches must stay in lock-step (path-based for readFile, YAML-content-based for parseYaml). Drift between them produces confusing test failures.

Recommend: a table-driven registry, e.g.

Suggested change
vi.mocked(fs.readFile).mockImplementation(async (filePath) => {
vi.mocked(fs.readFile).mockImplementation(async (filePath) => {
const pathStr = String(filePath);
for (const [name, fixture] of bundledSkillFixtures) {
if (pathStr.includes(`${path.sep}${name}${path.sep}`)) {
return fixture.markdown;
}
}
throw new Error(`Unexpected fs.readFile path in bundled-skill test: ${pathStr}`);
});

with a single bundledSkillFixtures map that drives both readFile (markdown) and parseYaml (parsed object) from one source of truth.

— claude-opus-4-7 via Claude Code /qreview

const pathStr = String(filePath);
if (pathStr.includes(`${path.sep}simplify${path.sep}`)) {
return `---
name: simplify
description: Simplify recent changes
---
Simplify content`;
}

return `---
name: review
description: Review code changes
---
Review content`);
Review content`;
});

mockParseYaml.mockImplementation((yamlString: string) => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] setupReviewSkillMocks calls mockParseYaml.mockImplementation(...), which replaces the global dispatcher set up in beforeEach at line 72. The global impl has a hooks: branch (lines 74–76) that uses the real yaml.parse so frontmatter with hooks: parses correctly. The new bundled-skill impl drops that branch — any test that runs after setupReviewSkillMocks will mis-parse SKILL.md frontmatter containing hooks:. SkillManager.parseSkillContent (skill-manager.ts:434) re-parses for hooks, so a future bundled skill that legitimately uses hooks frontmatter will silently fail in tests.

Recommend either delegate to yaml.parse for unrecognized fixtures, or keep parity with the global beforeEach dispatcher by re-including the hooks: branch:

Suggested change
mockParseYaml.mockImplementation((yamlString: string) => {
mockParseYaml.mockImplementation((yamlString: string) => {
if (yamlString.includes('hooks:')) {
return yaml.parse(yamlString);
}
if (yamlString.includes('name: simplify')) {
return {
name: 'simplify',
description: 'Simplify recent changes',
};
}
return {
name: 'review',
description: 'Review code changes',
};
});

— claude-opus-4-7 via Claude Code /qreview

if (yamlString.includes('name: simplify')) {
return {
name: 'simplify',
description: 'Simplify recent changes',
};
}

mockParseYaml.mockReturnValue({
name: 'review',
description: 'Review code changes',
return {
name: 'review',
description: 'Review code changes',
};
});
}

Expand All @@ -714,8 +747,11 @@ Review content`);
const skills = await manager.listSkills({ force: true });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The cross-level priority tests ("should prioritize project-level over bundled" / "user-level over bundled") verify review deduplication but do not assert that simplify — which has no name conflict — still survives in results.

If a future change causes all bundled skills to be dropped whenever a project-level skill exists (rather than per-name deduplication), these tests would not catch the regression.

Suggested change
const skills = await manager.listSkills({ force: true });
expect(skills.some((s) => s.name === 'simplify')).toBe(true);
expect(skills.some((s) => s.name === 'review')).toBe(true);

— deepseek-v4-pro via Qwen Code /review


expect(skills.some((s) => s.name === 'review')).toBe(true);
expect(skills.some((s) => s.name === 'simplify')).toBe(true);
const reviewSkill = skills.find((s) => s.name === 'review');
const simplifySkill = skills.find((s) => s.name === 'simplify');
expect(reviewSkill!.level).toBe('bundled');
expect(simplifySkill!.level).toBe('bundled');
});

it('should prioritize project-level over bundled skills with same name', async () => {
Expand All @@ -727,6 +763,8 @@ Review content`);
const reviewSkills = skills.filter((s) => s.name === 'review');
expect(reviewSkills).toHaveLength(1);
expect(reviewSkills[0].level).toBe('project');
// simplify has no name conflict, so it must still survive alongside the deduped review skill
expect(skills.some((s) => s.name === 'simplify')).toBe(true);
});

it('should prioritize user-level over bundled skills with same name', async () => {
Expand All @@ -738,6 +776,8 @@ Review content`);
const reviewSkills = skills.filter((s) => s.name === 'review');
expect(reviewSkills).toHaveLength(1);
expect(reviewSkills[0].level).toBe('user');
// simplify has no name conflict, so it must still survive alongside the deduped review skill
expect(skills.some((s) => s.name === 'simplify')).toBe(true);
});

it('should skip all skills in bare mode', async () => {
Expand Down
Loading