qwen-code/docs/design/skill-nudge
顾盼 f5af7fbf95
feat(memory): add autoSkill background project skill extraction (#3673)
* feat(memory): add autoSkill background project skill extraction

* fix(test): add missing mock methods for autoSkill (getAutoSkillEnabled, recordCompletedToolCall, consumePendingMemoryTaskPromises)

* fix(test): fix cross-platform path comparison in skillReviewNudge integration test

* fix(autoSkill): address critical review comments

- Fix merged_with_extract silent drop: remove broken merge optimization
  in scheduleSkillReview(). When an extract task is pending/running,
  skill review is now scheduled independently instead of recording
  metadata that no production code ever reads.

- Fix SKILL_MANAGE blocked from skill review agent: prepareTools() now
  only enforces the recursion guard (AGENT tool) when the agent has an
  explicit tools list. Wildcard/inherit subagents still get the full
  EXCLUDED_TOOLS_FOR_SUBAGENTS filter, preventing task subagents from
  calling skill_manage. The dedicated skill review agent can now receive
  the skill_manage tool it requires.

- Update manager.test.ts: replace merged_with_extract tests with
  concurrent-extract independent-scheduling tests.
- Update skill-manage.test.ts: clarify test description to reflect
  wildcard-only exclusion semantics.

* fix(autoSkill): reject symlink traversal in skill_manage path guard

assertProjectSkillPath() uses path.resolve() which is purely lexical and
does not dereference symlinks. If any path component inside .qwen/skills/
is a symlink pointing outside the project, fs.writeFile/readFile/rm would
follow the link and mutate files outside the advertised write boundary.

Add assertRealProjectSkillPath() (async) in skill-paths.ts that:
- Resolves the real path of the skills root via fs.realpath()
- Walks up from targetPath to find the nearest existing ancestor
- Resolves that ancestor to its real filesystem path
- Rejects if the real path falls outside the real skills root

skill-manage.ts execute() now calls both the cheap lexical check (fast
fail for obviously wrong paths) and the async real-path check before any
fs.writeFile / fs.rm mutation.

Add three symlink-specific tests in skill-paths.test.ts covering:
- Legitimate path accepted
- Symlinked directory pointing outside skills root rejected
- Skills root itself being a symlink (safe target) accepted

* refactor(autoSkill): remove skill_manage tool, use path-based skill write detection

Address reviewer feedback: instead of keeping skill_manage as the sole
write gate (which still had symlink bypass risk via generic tools), remove
the dedicated tool entirely and replace with a two-layer protection:

1. skillsModifiedInSession (client.ts): detects writes to .qwen/skills/
   by inspecting the file_path arg of every completed tool call, replacing
   the fragile historyCallsSkillManage() history scan.

2. hasAutoSkillSource + evaluateScopedDecision (skillReviewAgentPlanner.ts):
   the review agent's permission sandbox now verifies BOTH that the target
   path is inside the skills directory AND that the existing file already
   contains 'source: auto-skill' in its frontmatter before allowing edits,
   preventing the agent from overwriting user-managed skills.

Changes:
- Delete skill-manage.ts and skill-manage.test.ts
- Remove SKILL_MANAGE from ToolNames, ToolDisplayNames, config registerLazy,
  agent-core EXCLUDED_TOOLS comment, and agent.ts comment
- Replace historyCallsSkillManage() with skillsModified: boolean param in
  scheduleSkillReview; skip reason renamed skills_modified_in_session
- recordCompletedToolCall(name, filePath?) detects .qwen/skills/ writes;
  CLI layers pass file_path arg from tool call request
- Fix buildTaskPrompt frontmatter template to use top-level source: auto-skill
- Update skill-paths.ts error messages to remove skill_manage references
- Update all unit/integration tests accordingly

* fix(autoSkill): deduplicate concurrent skill-review tasks per projectRoot

scheduleSkillReview() was launching a new background task every time the
threshold was reached for the same project, with no guard against multiple
in-flight reviews running concurrently.

Fix: add skillReviewInFlightByProject Map that tracks the taskId of any
running review per projectRoot. A second call while one is in-flight returns
{ status: 'skipped', skippedReason: 'already_running', taskId: <existing> }.
The map entry is cleared in a finally block inside runSkillReview() so the
next session can schedule a fresh review after the current one completes.

Also extend SkillReviewScheduleResult.skippedReason union to include
'already_running', and add a unit test covering the full lifecycle:
first call schedules, second call is skipped with existing taskId, and a
third call after completion schedules a new task.

* fix(autoSkill): address all critical review comments

1. hasAutoSkillSource: narrow catch to ENOENT only (EISDIR/EACCES etc.
   return false to deny); tighten frontmatter regex to match opening block only.

2. evaluateScopedDecision: add explicit allow for READ_FILE and LS so they
   don't fall to 'default' which the base PermissionManager might widen;
   EDIT/WRITE_FILE now call assertRealProjectSkillPath() (async realpath guard)
   in addition to the lexical check, closing the symlink traversal hole.

3. isScopedTool / getScopedDenyRule: cover READ_FILE and LS so hasRelevantRules
   returns true and findMatchingDenyRule is correctly consulted for them.

4. recordCompletedToolCall (client.ts): broaden tool name set to match
   WRITE_TOOL_NAMES in manager.ts (write_file, edit, replace, create_file) and
   inspect all three arg keys (file_path, path, target_file). Signature changed
   from (name, filePath?) to (name, args?) to carry all args through.

5. client.ts hardcoded literals: replace threshold/maxTurns/timeoutMs with the
   named constants AUTO_SKILL_THRESHOLD / DEFAULT_AUTO_SKILL_MAX_TURNS /
   DEFAULT_AUTO_SKILL_TIMEOUT_MS imported from manager.ts and
   skillReviewAgentPlanner.ts.

6. toolCallCount / skillsModifiedInSession reset: only reset when skill review
   is actually scheduled (status === 'scheduled'), not every turn, so the
   counter correctly accumulates across turns within a session as per design doc.

7. runSkillReview (manager.ts): rethrow after marking record failed, consistent
   with runExtract behavior.

8. skillReviewNudge.integration.test.ts test 5: rewrite to reflect the
   in-flight dedup contract (second same-project call returns already_running
   with existing taskId; third call after completion gets a new task). Add
   vi.mock for runSkillReviewByAgent so the test does not need a full Config.

* fix(autoSkill): address all review comments

- skill-paths: detect dangling symlinks with lstat before treating ENOENT as safe
- skill-paths: fix isProjectSkillPath relative path resolution to use projectRoot
- skillReviewAgentPlanner: restrict READ_FILE/LS to project root only
- skillReviewAgentPlanner: remove SHELL tool from review agent tool list
- skillReviewAgentPlanner: add path import; remove unused shell imports
- skillReviewAgentPlanner: add comment for buildAgentHistory trailing user message
- client: fix runManagedAutoMemoryBackgroundTasks gate widening
- client: fix skillsModifiedInSession deadlock
- client: add .catch() to skill review promise
- client: hoist SKILL_WRITE_TOOL_NAMES to module-level ReadonlySet
- agent-core: use full EXCLUDED_TOOLS_FOR_SUBAGENTS for explicit tool list subagents
- manager: extend notify() signature to accept 'skill-review' taskType
- config: fix JSDoc default value comment (false, not true)

* fix(autoSkill): address second round review comments

- client: reset toolCallCount when scheduleSkillReview returns already_running
  and count >= threshold, preventing immediate cascade after in-flight review
- client.test: add autoSkill branch tests (scheduled/already_running/skills_modified)
- client.test: add full recordCompletedToolCall unit tests (skillsModifiedInSession,
  toolCallCount increment, skill path detection for write_file/edit/read_file)
- client.test: add scheduleSkillReview mock to mockMemoryManager
- nonInteractiveCli.test: add assertions for recordCompletedToolCall and
  consumePendingMemoryTaskPromises in tool-call integration test
2026-05-09 14:25:02 +08:00
..
skill-nudge.md feat(memory): add autoSkill background project skill extraction (#3673) 2026-05-09 14:25:02 +08:00